Merge "[RBAC] Enforce check for share updates"
This commit is contained in:
commit
398722be59
@ -1237,8 +1237,17 @@ class AdminActionsMixin(object):
|
|||||||
return update
|
return update
|
||||||
|
|
||||||
@Controller.authorize('reset_status')
|
@Controller.authorize('reset_status')
|
||||||
def _reset_status(self, req, id, body, status_attr='status'):
|
def _reset_status(self, req, id, body, status_attr='status',
|
||||||
"""Reset the status_attr specified on the resource."""
|
resource=None):
|
||||||
|
"""Reset the status_attr specified on the resource.
|
||||||
|
|
||||||
|
:param req: API request object
|
||||||
|
:param id: ID of the resource
|
||||||
|
:param body: API request body
|
||||||
|
:param status_attr: Attribute on the resource denoting the status
|
||||||
|
to be reset
|
||||||
|
:param resource: Resource model or dict if we need to avoid fetching it
|
||||||
|
"""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
body_attr = self.body_attributes[status_attr]
|
body_attr = self.body_attributes[status_attr]
|
||||||
update = self.validate_update(
|
update = self.validate_update(
|
||||||
@ -1248,9 +1257,17 @@ class AdminActionsMixin(object):
|
|||||||
LOG.debug(msg, {'resource': self.resource_name, 'id': id,
|
LOG.debug(msg, {'resource': self.resource_name, 'id': id,
|
||||||
'update': update})
|
'update': update})
|
||||||
try:
|
try:
|
||||||
self._update(context, id, update)
|
resource = resource or self._get(context, id)
|
||||||
except exception.NotFound as e:
|
except exception.NotFound as e:
|
||||||
raise webob.exc.HTTPNotFound(e.message)
|
raise webob.exc.HTTPNotFound(e.message)
|
||||||
|
try:
|
||||||
|
policy.check_policy(context,
|
||||||
|
self.resource_name,
|
||||||
|
"reset_status",
|
||||||
|
target_obj=resource)
|
||||||
|
except exception.NotAuthorized as e:
|
||||||
|
raise webob.exc.HTTPForbidden(e.message)
|
||||||
|
self._update(context, id, update)
|
||||||
return webob.Response(status_int=http_client.ACCEPTED)
|
return webob.Response(status_int=http_client.ACCEPTED)
|
||||||
|
|
||||||
@Controller.authorize('force_delete')
|
@Controller.authorize('force_delete')
|
||||||
@ -1261,6 +1278,10 @@ class AdminActionsMixin(object):
|
|||||||
resource = self._get(context, id)
|
resource = self._get(context, id)
|
||||||
except exception.NotFound as e:
|
except exception.NotFound as e:
|
||||||
raise webob.exc.HTTPNotFound(e.message)
|
raise webob.exc.HTTPNotFound(e.message)
|
||||||
|
policy.check_policy(context,
|
||||||
|
self.resource_name,
|
||||||
|
"force_delete",
|
||||||
|
target_obj=resource)
|
||||||
self._delete(context, resource, force=True)
|
self._delete(context, resource, force=True)
|
||||||
return webob.Response(status_int=http_client.ACCEPTED)
|
return webob.Response(status_int=http_client.ACCEPTED)
|
||||||
|
|
||||||
|
@ -59,6 +59,9 @@ class ShareNetworkController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
'status': set(constants.SHARE_NETWORK_STATUSES)
|
'status': set(constants.SHARE_NETWORK_STATUSES)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _get(self, *args, **kwargs):
|
||||||
|
return db_api.share_network_get(*args, **kwargs)
|
||||||
|
|
||||||
def show(self, req, id):
|
def show(self, req, id):
|
||||||
"""Return data about the requested network info."""
|
"""Return data about the requested network info."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
|
@ -47,6 +47,9 @@ class ShareServerController(share_servers.ShareServerController,
|
|||||||
'task_state': set(constants.SERVER_TASK_STATE_STATUSES),
|
'task_state': set(constants.SERVER_TASK_STATE_STATUSES),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _get(self, *args, **kwargs):
|
||||||
|
return db_api.share_server_get(*args, **kwargs)
|
||||||
|
|
||||||
def _update(self, context, id, update):
|
def _update(self, context, id, update):
|
||||||
db_api.share_server_update(context, id, update)
|
db_api.share_server_update(context, id, update)
|
||||||
|
|
||||||
|
@ -81,6 +81,9 @@ class ShareSnapshotInstancesController(wsgi.Controller,
|
|||||||
def _update(self, *args, **kwargs):
|
def _update(self, *args, **kwargs):
|
||||||
db.share_snapshot_instance_update(*args, **kwargs)
|
db.share_snapshot_instance_update(*args, **kwargs)
|
||||||
|
|
||||||
|
def _get(self, *args, **kwargs):
|
||||||
|
db.share_snapshot_instance_get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def create_resource():
|
def create_resource():
|
||||||
return wsgi.Resource(ShareSnapshotInstancesController())
|
return wsgi.Resource(ShareSnapshotInstancesController())
|
||||||
|
@ -233,12 +233,12 @@ class ShareController(wsgi.Controller,
|
|||||||
try:
|
try:
|
||||||
share = self.share_api.get(context, id)
|
share = self.share_api.get(context, id)
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
raise exception.ShareNotFound(share_id=id)
|
raise exc.HTTPNotFound("Share %s not found" % id)
|
||||||
if share.get('is_soft_deleted'):
|
if share.get('is_soft_deleted'):
|
||||||
msg = _("status cannot be reset for share '%s' "
|
msg = _("status cannot be reset for share '%s' "
|
||||||
"since it has been soft deleted.") % id
|
"since it has been soft deleted.") % id
|
||||||
raise exc.HTTPForbidden(explanation=msg)
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
return self._reset_status(req, id, body)
|
return self._reset_status(req, id, body, resource=share)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.7')
|
@wsgi.Controller.api_version('2.7')
|
||||||
@wsgi.action('reset_status')
|
@wsgi.action('reset_status')
|
||||||
@ -248,12 +248,12 @@ class ShareController(wsgi.Controller,
|
|||||||
try:
|
try:
|
||||||
share = self.share_api.get(context, id)
|
share = self.share_api.get(context, id)
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
raise exception.ShareNotFound(share_id=id)
|
raise exc.HTTPNotFound("Share %s not found" % id)
|
||||||
if share.get('is_soft_deleted'):
|
if share.get('is_soft_deleted'):
|
||||||
msg = _("status cannot be reset for share '%s' "
|
msg = _("status cannot be reset for share '%s' "
|
||||||
"since it has been soft deleted.") % id
|
"since it has been soft deleted.") % id
|
||||||
raise exc.HTTPForbidden(explanation=msg)
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
return self._reset_status(req, id, body)
|
return self._reset_status(req, id, body, resource=share)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.0', '2.6')
|
@wsgi.Controller.api_version('2.0', '2.6')
|
||||||
@wsgi.action('os-force_delete')
|
@wsgi.action('os-force_delete')
|
||||||
@ -455,7 +455,8 @@ class ShareController(wsgi.Controller,
|
|||||||
msg = _("task state cannot be reset for share '%s' "
|
msg = _("task state cannot be reset for share '%s' "
|
||||||
"since it has been soft deleted.") % id
|
"since it has been soft deleted.") % id
|
||||||
raise exc.HTTPForbidden(explanation=msg)
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
return self._reset_status(req, id, body, status_attr='task_state')
|
return self._reset_status(req, id, body, status_attr='task_state',
|
||||||
|
resource=share)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.0', '2.6')
|
@wsgi.Controller.api_version('2.0', '2.6')
|
||||||
@wsgi.action('os-allow_access')
|
@wsgi.action('os-allow_access')
|
||||||
|
@ -54,14 +54,22 @@ class ShareServerControllerTest(test.TestCase):
|
|||||||
body = {'reset_status': {'status': status}}
|
body = {'reset_status': {'status': status}}
|
||||||
|
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
|
self.mock_object(self.controller, '_get', mock.Mock(
|
||||||
|
return_value={'share_server': 'object'}))
|
||||||
mock_update = self.mock_object(db_api, 'share_server_update')
|
mock_update = self.mock_object(db_api, 'share_server_update')
|
||||||
|
|
||||||
result = self.controller.share_server_reset_status(
|
result = self.controller.share_server_reset_status(
|
||||||
req, 'fake_server_id', body)
|
req, 'fake_server_id', body)
|
||||||
|
|
||||||
self.assertEqual(202, result.status_int)
|
self.assertEqual(202, result.status_int)
|
||||||
policy.check_policy.assert_called_once_with(
|
policy.check_policy.assert_has_calls([
|
||||||
context, self.resource_name, 'reset_status')
|
mock.call(context, self.resource_name, 'reset_status'),
|
||||||
|
mock.call(context,
|
||||||
|
self.resource_name,
|
||||||
|
'reset_status',
|
||||||
|
target_obj={'share_server': 'object'}),
|
||||||
|
])
|
||||||
|
|
||||||
mock_update.assert_called_once_with(
|
mock_update.assert_called_once_with(
|
||||||
context, 'fake_server_id', {'status': status})
|
context, 'fake_server_id', {'status': status})
|
||||||
|
|
||||||
@ -881,16 +889,18 @@ class ShareServerControllerTest(test.TestCase):
|
|||||||
update = {'task_state': constants.TASK_STATE_MIGRATION_ERROR}
|
update = {'task_state': constants.TASK_STATE_MIGRATION_ERROR}
|
||||||
body = {'reset_task_state': update}
|
body = {'reset_task_state': update}
|
||||||
|
|
||||||
self.mock_object(db_api, 'share_server_update',
|
self.mock_object(db_api, 'share_server_get',
|
||||||
mock.Mock(side_effect=exception.ShareServerNotFound(
|
mock.Mock(side_effect=exception.ShareServerNotFound(
|
||||||
share_server_id='fake_server_id')))
|
share_server_id='fake_server_id')))
|
||||||
|
self.mock_object(db_api, 'share_server_update')
|
||||||
|
|
||||||
self.assertRaises(webob.exc.HTTPNotFound,
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
self.controller.share_server_reset_task_state,
|
self.controller.share_server_reset_task_state,
|
||||||
req, server['id'], body)
|
req, server['id'], body)
|
||||||
|
|
||||||
db_api.share_server_update.assert_called_once_with(utils.IsAMatcher(
|
db_api.share_server_get.assert_called_once_with(utils.IsAMatcher(
|
||||||
ctx_api.RequestContext), server['id'], update)
|
ctx_api.RequestContext), server['id'])
|
||||||
|
db_api.share_server_update.assert_not_called()
|
||||||
|
|
||||||
def test_share_server_migration_complete(self):
|
def test_share_server_migration_complete(self):
|
||||||
server = db_utils.create_share_server(
|
server = db_utils.create_share_server(
|
||||||
|
@ -1138,15 +1138,42 @@ class ShareAPITest(test.TestCase):
|
|||||||
update = {'task_state': constants.TASK_STATE_MIGRATION_ERROR}
|
update = {'task_state': constants.TASK_STATE_MIGRATION_ERROR}
|
||||||
body = {'reset_task_state': update}
|
body = {'reset_task_state': update}
|
||||||
|
|
||||||
self.mock_object(db, 'share_update',
|
self.mock_object(share_api.API, 'get',
|
||||||
mock.Mock(side_effect=exception.NotFound()))
|
mock.Mock(side_effect=exception.NotFound))
|
||||||
|
self.mock_object(db, 'share_update')
|
||||||
|
|
||||||
self.assertRaises(webob.exc.HTTPNotFound,
|
self.assertRaises(exception.NotFound,
|
||||||
self.controller.reset_task_state, req, share['id'],
|
self.controller.reset_task_state, req, share['id'],
|
||||||
body)
|
body)
|
||||||
|
|
||||||
db.share_update.assert_called_once_with(utils.IsAMatcher(
|
share_api.API.get.assert_called_once_with(utils.IsAMatcher(
|
||||||
context.RequestContext), share['id'], update)
|
context.RequestContext), share['id'])
|
||||||
|
db.share_update.assert_not_called()
|
||||||
|
|
||||||
|
def test_reset_task_state_share_other_project_public_share(self):
|
||||||
|
share = db_utils.create_share(is_public=True)
|
||||||
|
req = fakes.HTTPRequest.blank(
|
||||||
|
'/v2/fake/shares/%s/action' % share['id'],
|
||||||
|
use_admin_context=True, version=LATEST_MICROVERSION)
|
||||||
|
req.method = 'POST'
|
||||||
|
req.headers['content-type'] = 'application/json'
|
||||||
|
req.api_version_request.experimental = True
|
||||||
|
update = {'task_state': constants.TASK_STATE_MIGRATION_ERROR}
|
||||||
|
body = {'reset_task_state': update}
|
||||||
|
|
||||||
|
# NOTE(gouthamr): we're testing a scenario where someone has access
|
||||||
|
# to the RBAC rule share:reset_task_state, but doesn't own the share.
|
||||||
|
# Ideally we'd override the default policy, but it's a shared
|
||||||
|
# resource and we'll bleed into other tests, so we'll mock the
|
||||||
|
# policy check to return False instead
|
||||||
|
rbac_checks = [None, None, exception.NotAuthorized]
|
||||||
|
with mock.patch.object(policy, 'check_policy',
|
||||||
|
side_effect=rbac_checks):
|
||||||
|
self.mock_object(share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=share))
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.reset_task_state,
|
||||||
|
req, share['id'], body)
|
||||||
|
|
||||||
def test_reset_task_state_share_has_been_soft_deleted(self):
|
def test_reset_task_state_share_has_been_soft_deleted(self):
|
||||||
share = self.share_in_recycle_bin
|
share = self.share_in_recycle_bin
|
||||||
@ -2717,19 +2744,18 @@ class ShareAdminActionsAPITest(test.TestCase):
|
|||||||
req.headers['X-Openstack-Manila-Api-Version'] = version
|
req.headers['X-Openstack-Manila-Api-Version'] = version
|
||||||
req.body = jsonutils.dumps(body).encode("utf-8")
|
req.body = jsonutils.dumps(body).encode("utf-8")
|
||||||
req.environ['manila.context'] = ctxt
|
req.environ['manila.context'] = ctxt
|
||||||
self.mock_object(share_api.API, 'get', mock.Mock(return_value=model))
|
|
||||||
|
|
||||||
resp = req.get_response(fakes.app())
|
resp = req.get_response(fakes.app(), catch_exc_info=True)
|
||||||
|
|
||||||
# validate response code and model status
|
# validate response code and model status
|
||||||
self.assertEqual(valid_code, resp.status_int)
|
self.assertEqual(valid_code, resp.status_int)
|
||||||
|
|
||||||
if valid_code == 404:
|
if valid_code == 404 and db_access_method is not None:
|
||||||
self.assertRaises(exception.NotFound,
|
self.assertRaises(exception.NotFound,
|
||||||
db_access_method,
|
db_access_method,
|
||||||
ctxt,
|
ctxt,
|
||||||
model['id'])
|
model['id'])
|
||||||
else:
|
elif db_access_method:
|
||||||
actual_model = db_access_method(ctxt, model['id'])
|
actual_model = db_access_method(ctxt, model['id'])
|
||||||
self.assertEqual(valid_status, actual_model['status'])
|
self.assertEqual(valid_status, actual_model['status'])
|
||||||
|
|
||||||
@ -2739,6 +2765,7 @@ class ShareAdminActionsAPITest(test.TestCase):
|
|||||||
valid_status, version):
|
valid_status, version):
|
||||||
share, req = self._setup_share_data(version=version)
|
share, req = self._setup_share_data(version=version)
|
||||||
ctxt = self._get_context(role)
|
ctxt = self._get_context(role)
|
||||||
|
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||||
|
|
||||||
self._reset_status(ctxt, share, req, db.share_get, valid_code,
|
self._reset_status(ctxt, share, req, db.share_get, valid_code,
|
||||||
valid_status, version=version)
|
valid_status, version=version)
|
||||||
@ -2747,6 +2774,7 @@ class ShareAdminActionsAPITest(test.TestCase):
|
|||||||
def test_share_invalid_reset_status_body(self, body):
|
def test_share_invalid_reset_status_body(self, body):
|
||||||
share, req = self._setup_share_data(version='2.6')
|
share, req = self._setup_share_data(version='2.6')
|
||||||
ctxt = self.admin_context
|
ctxt = self.admin_context
|
||||||
|
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||||
|
|
||||||
self._reset_status(ctxt, share, req, db.share_get, 400,
|
self._reset_status(ctxt, share, req, db.share_get, 400,
|
||||||
constants.STATUS_AVAILABLE, body, version='2.6')
|
constants.STATUS_AVAILABLE, body, version='2.6')
|
||||||
@ -2758,7 +2786,23 @@ class ShareAdminActionsAPITest(test.TestCase):
|
|||||||
'/v2/fake/shares/%s/action' % fake_share['id'], version=version)
|
'/v2/fake/shares/%s/action' % fake_share['id'], version=version)
|
||||||
|
|
||||||
self._reset_status(self.admin_context, fake_share, req,
|
self._reset_status(self.admin_context, fake_share, req,
|
||||||
db.share_snapshot_get, 404, version=version)
|
db.share_get, 404, version=version)
|
||||||
|
|
||||||
|
@ddt.data('2.6', '2.7')
|
||||||
|
def test_reset_status_other_project_public_share(self, version):
|
||||||
|
# NOTE(gouthamr): we're testing a scenario where someone has access
|
||||||
|
# to the RBAC rule share:reset_status, but doesn't own the share.
|
||||||
|
# Ideally we'd override the default policy, but it's a shared
|
||||||
|
# resource and we'll bleed into other tests, so we'll mock the
|
||||||
|
# policy check to return False instead
|
||||||
|
share, req = self._setup_share_data(version=version)
|
||||||
|
share['is_public'] = True
|
||||||
|
rbac_checks = [None, exception.NotAuthorized]
|
||||||
|
with mock.patch.object(policy, 'authorize', side_effect=rbac_checks):
|
||||||
|
self.mock_object(share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=share))
|
||||||
|
self._reset_status(
|
||||||
|
self.member_context, share, req, None, 403, version=version)
|
||||||
|
|
||||||
def _force_delete(self, ctxt, model, req, db_access_method, valid_code,
|
def _force_delete(self, ctxt, model, req, db_access_method, valid_code,
|
||||||
check_model_in_db=False, version='2.7'):
|
check_model_in_db=False, version='2.7'):
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Role based access control is enforced on the POST /shares/{share_id}/action
|
||||||
|
API to reset status, task state, replica state and similar fields. This
|
||||||
|
prevents the situation where deployments allow some users access to
|
||||||
|
these APIs, but they don't belong to projects where the resources exist.
|
||||||
|
See `bug 1955627 <https://launchpad.net/bugs/1955627>`_ for more context.
|
Loading…
Reference in New Issue
Block a user