Unscoped List Stacks
This allows non tenant-scoped list of stacks for the management api blueprint. A user allowed by the policy stacks:global_index will be able to make a call to "/:tenant_id/stacks?global_tenant=true" and get all stacks in Heat, and not just the ones owned by the user, effectively bypassing tenant scoping. If the user is not authorized by the policy and they pass the global_tenant param, the request will get denied by the policy enforcer and 403 Forbidden is returned. Co-Authored-By: Richard Lee <rblee88@gmail.com> Implements: blueprint management-api Change-Id: I973a7ad2726eb6d9c972d08abe15e241aa120ec3
This commit is contained in:
parent
f329dfdd98
commit
3261c58e73
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"context_is_admin": "role:admin",
|
||||
"deny_stack_user": "not role:heat_stack_user",
|
||||
"deny_everybody": "!",
|
||||
|
||||
"cloudformation:ListStacks": "rule:deny_stack_user",
|
||||
"cloudformation:CreateStack": "rule:deny_stack_user",
|
||||
|
@ -40,6 +41,7 @@
|
|||
"stacks:delete": "rule:deny_stack_user",
|
||||
"stacks:detail": "rule:deny_stack_user",
|
||||
"stacks:generate_template": "rule:deny_stack_user",
|
||||
"stacks:global_index": "rule:deny_everybody",
|
||||
"stacks:index": "rule:deny_stack_user",
|
||||
"stacks:list_resource_types": "rule:deny_stack_user",
|
||||
"stacks:lookup": "rule:deny_stack_user",
|
||||
|
|
|
@ -67,6 +67,7 @@ class FaultWrapper(wsgi.Middleware):
|
|||
'ResourceNotAvailable': webob.exc.HTTPNotFound,
|
||||
'PhysicalResourceNotFound': webob.exc.HTTPNotFound,
|
||||
'InvalidTenant': webob.exc.HTTPForbidden,
|
||||
'Forbidden': webob.exc.HTTPForbidden,
|
||||
'StackExists': webob.exc.HTTPConflict,
|
||||
'StackValidationFailed': webob.exc.HTTPBadRequest,
|
||||
'InvalidTemplateReference': webob.exc.HTTPBadRequest,
|
||||
|
|
|
@ -152,11 +152,7 @@ class StackController(object):
|
|||
def default(self, req, **args):
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
@util.policy_enforce
|
||||
def index(self, req):
|
||||
"""
|
||||
Lists summary information for all stacks
|
||||
"""
|
||||
def _index(self, req, tenant_safe=True):
|
||||
filter_whitelist = {
|
||||
'status': 'mixed',
|
||||
'name': 'mixed',
|
||||
|
@ -169,9 +165,9 @@ class StackController(object):
|
|||
}
|
||||
params = util.get_allowed_params(req.params, whitelist)
|
||||
filter_params = util.get_allowed_params(req.params, filter_whitelist)
|
||||
|
||||
stacks = self.rpc_client.list_stacks(req.context,
|
||||
filters=filter_params,
|
||||
tenant_safe=tenant_safe,
|
||||
**params)
|
||||
|
||||
count = None
|
||||
|
@ -180,12 +176,28 @@ class StackController(object):
|
|||
# Check if engine has been updated to a version with
|
||||
# support to count_stacks before trying to use it.
|
||||
count = self.rpc_client.count_stacks(req.context,
|
||||
filters=filter_params)
|
||||
filters=filter_params,
|
||||
tenant_safe=tenant_safe)
|
||||
except AttributeError as exc:
|
||||
logger.warning("Old Engine Version: %s" % str(exc))
|
||||
|
||||
return stacks_view.collection(req, stacks=stacks, count=count)
|
||||
|
||||
@util.policy_enforce
|
||||
def global_index(self, req):
|
||||
return self._index(req, tenant_safe=False)
|
||||
|
||||
@util.policy_enforce
|
||||
def index(self, req):
|
||||
"""
|
||||
Lists summary information for all stacks
|
||||
"""
|
||||
global_tenant = bool(req.params.get('global_tenant', False))
|
||||
if global_tenant:
|
||||
return self.global_index(req, req.context.tenant_id)
|
||||
|
||||
return self._index(req)
|
||||
|
||||
@util.policy_enforce
|
||||
def detail(self, req):
|
||||
"""
|
||||
|
|
|
@ -303,7 +303,7 @@ class EngineService(service.Service):
|
|||
|
||||
@request_context
|
||||
def list_stacks(self, cnxt, limit=None, marker=None, sort_keys=None,
|
||||
sort_dir=None, filters=None):
|
||||
sort_dir=None, filters=None, tenant_safe=True):
|
||||
"""
|
||||
The list_stacks method returns attributes of all stacks. It supports
|
||||
pagination (``limit`` and ``marker``), sorting (``sort_keys`` and
|
||||
|
@ -315,6 +315,7 @@ class EngineService(service.Service):
|
|||
:param sort_keys: an array of fields used to sort the list
|
||||
:param sort_dir: the direction of the sort ('asc' or 'desc')
|
||||
:param filters: a dict with attribute:value to filter the list
|
||||
:param tenant_safe: if true, scope the request by the current tenant
|
||||
:returns: a list of formatted stacks
|
||||
"""
|
||||
|
||||
|
@ -331,18 +332,19 @@ class EngineService(service.Service):
|
|||
yield api.format_stack(stack)
|
||||
|
||||
stacks = db_api.stack_get_all(cnxt, limit, sort_keys, marker,
|
||||
sort_dir, filters) or []
|
||||
sort_dir, filters, tenant_safe) or []
|
||||
return list(format_stack_details(stacks))
|
||||
|
||||
@request_context
|
||||
def count_stacks(self, cnxt, filters=None):
|
||||
def count_stacks(self, cnxt, filters=None, tenant_safe=True):
|
||||
"""
|
||||
Return the number of stacks that match the given filters
|
||||
:param ctxt: RPC context.
|
||||
:param filters: a dict of ATTR:VALUE to match against stacks
|
||||
:returns: a integer representing the number of matched stacks
|
||||
"""
|
||||
return db_api.stack_count_all(cnxt, filters=filters)
|
||||
return db_api.stack_count_all(cnxt, filters=filters,
|
||||
tenant_safe=tenant_safe)
|
||||
|
||||
def _validate_deferred_auth_context(self, cnxt, stack):
|
||||
if cfg.CONF.deferred_auth_method != 'password':
|
||||
|
|
|
@ -52,7 +52,7 @@ class EngineClient(heat.openstack.common.rpc.proxy.RpcProxy):
|
|||
stack_name=stack_name))
|
||||
|
||||
def list_stacks(self, ctxt, limit=None, marker=None, sort_keys=None,
|
||||
sort_dir=None, filters=None):
|
||||
sort_dir=None, filters=None, tenant_safe=True):
|
||||
"""
|
||||
The list_stacks method returns attributes of all stacks. It supports
|
||||
pagination (``limit`` and ``marker``), sorting (``sort_keys`` and
|
||||
|
@ -64,21 +64,25 @@ class EngineClient(heat.openstack.common.rpc.proxy.RpcProxy):
|
|||
:param sort_keys: an array of fields used to sort the list
|
||||
:param sort_dir: the direction of the sort ('asc' or 'desc')
|
||||
:param filters: a dict with attribute:value to filter the list
|
||||
:param tenant_safe: if true, scope the request by the current tenant
|
||||
:returns: a list of stacks
|
||||
"""
|
||||
return self.call(ctxt, self.make_msg('list_stacks', limit=limit,
|
||||
sort_keys=sort_keys, marker=marker,
|
||||
sort_dir=sort_dir, filters=filters))
|
||||
sort_dir=sort_dir, filters=filters,
|
||||
tenant_safe=tenant_safe))
|
||||
|
||||
def count_stacks(self, ctxt, filters=None):
|
||||
def count_stacks(self, ctxt, filters=None, tenant_safe=True):
|
||||
"""
|
||||
Return the number of stacks that match the given filters
|
||||
:param ctxt: RPC context.
|
||||
:param filters: a dict of ATTR:VALUE to match against stacks
|
||||
:param tenant_safe: if true, scope the request by the current tenant
|
||||
:returns: a integer representing the number of matched stacks
|
||||
"""
|
||||
return self.call(ctxt, self.make_msg('count_stacks',
|
||||
filters=filters))
|
||||
filters=filters,
|
||||
tenant_safe=tenant_safe))
|
||||
|
||||
def show_stack(self, ctxt, stack_identity):
|
||||
"""
|
||||
|
|
|
@ -159,7 +159,7 @@ class CfnStackControllerTest(HeatTestCase):
|
|||
u'StackStatus': u'CREATE_COMPLETE'}]}}}
|
||||
self.assertEqual(expected, result)
|
||||
default_args = {'limit': None, 'sort_keys': None, 'marker': None,
|
||||
'sort_dir': None, 'filters': None}
|
||||
'sort_dir': None, 'filters': None, 'tenant_safe': True}
|
||||
mock_call.assert_called_once_with(dummy_req.context, self.topic,
|
||||
{'namespace': None,
|
||||
'method': 'list_stacks',
|
||||
|
|
|
@ -377,7 +377,7 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
|||
}
|
||||
self.assertEqual(expected, result)
|
||||
default_args = {'limit': None, 'sort_keys': None, 'marker': None,
|
||||
'sort_dir': None, 'filters': {}}
|
||||
'sort_dir': None, 'filters': {}, 'tenant_safe': True}
|
||||
mock_call.assert_called_once_with(req.context, self.topic,
|
||||
{'namespace': None,
|
||||
'method': 'list_stacks',
|
||||
|
@ -402,12 +402,13 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
|||
|
||||
rpc_call_args, _ = mock_call.call_args
|
||||
engine_args = rpc_call_args[2]['args']
|
||||
self.assertEqual(5, len(engine_args))
|
||||
self.assertEqual(6, len(engine_args))
|
||||
self.assertIn('limit', engine_args)
|
||||
self.assertIn('sort_keys', engine_args)
|
||||
self.assertIn('marker', engine_args)
|
||||
self.assertIn('sort_dir', engine_args)
|
||||
self.assertIn('filters', engine_args)
|
||||
self.assertIn('tenant_safe', engine_args)
|
||||
self.assertNotIn('balrog', engine_args)
|
||||
|
||||
@mock.patch.object(rpc, 'call')
|
||||
|
@ -474,6 +475,31 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
|||
result = self.controller.index(req, tenant_id=self.tenant)
|
||||
self.assertNotIn('count', result)
|
||||
|
||||
def test_index_enforces_global_index_if_global_tenant(self, mock_enforce):
|
||||
params = {'global_tenant': 'True'}
|
||||
req = self._get('/stacks', params=params)
|
||||
rpc_client = self.controller.rpc_client
|
||||
|
||||
rpc_client.list_stacks = mock.Mock(return_value=[])
|
||||
rpc_client.count_stacks = mock.Mock()
|
||||
|
||||
self.controller.index(req, tenant_id=self.tenant)
|
||||
mock_enforce.assert_called_with(action='global_index',
|
||||
scope=self.controller.REQUEST_SCOPE,
|
||||
context=self.context)
|
||||
|
||||
def test_global_index_sets_tenant_safe_to_false(self, mock_enforce):
|
||||
rpc_client = self.controller.rpc_client
|
||||
rpc_client.list_stacks = mock.Mock(return_value=[])
|
||||
rpc_client.count_stacks = mock.Mock()
|
||||
|
||||
params = {'global_tenant': 'True'}
|
||||
req = self._get('/stacks', params=params)
|
||||
self.controller.index(req, tenant_id=self.tenant)
|
||||
rpc_client.list_stacks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
tenant_safe=False)
|
||||
|
||||
@mock.patch.object(rpc, 'call')
|
||||
def test_detail(self, mock_call, mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'detail', True)
|
||||
|
@ -529,7 +555,7 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
|||
|
||||
self.assertEqual(expected, result)
|
||||
default_args = {'limit': None, 'sort_keys': None, 'marker': None,
|
||||
'sort_dir': None, 'filters': None}
|
||||
'sort_dir': None, 'filters': None, 'tenant_safe': True}
|
||||
mock_call.assert_called_once_with(req.context, self.topic,
|
||||
{'namespace': None,
|
||||
'method': 'list_stacks',
|
||||
|
|
|
@ -1657,9 +1657,55 @@ class StackServiceTest(HeatTestCase):
|
|||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
filters
|
||||
filters,
|
||||
mock.ANY,
|
||||
)
|
||||
|
||||
@mock.patch.object(db_api, 'stack_get_all')
|
||||
def test_stack_list_tenant_safe_defaults_to_true(self, mock_stack_get_all):
|
||||
self.eng.list_stacks(self.ctx)
|
||||
mock_stack_get_all.assert_called_once_with(mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
True,
|
||||
)
|
||||
|
||||
@mock.patch.object(db_api, 'stack_get_all')
|
||||
def test_stack_list_passes_tenant_safe_info(self, mock_stack_get_all):
|
||||
self.eng.list_stacks(self.ctx, tenant_safe=False)
|
||||
mock_stack_get_all.assert_called_once_with(mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
False,
|
||||
)
|
||||
|
||||
@mock.patch.object(db_api, 'stack_count_all')
|
||||
def test_count_stacks_passes_filter_info(self, mock_stack_count_all):
|
||||
self.eng.count_stacks(self.ctx, filters={'foo': 'bar'})
|
||||
mock_stack_count_all.assert_called_once_with(mock.ANY,
|
||||
filters={'foo': 'bar'},
|
||||
tenant_safe=mock.ANY)
|
||||
|
||||
@mock.patch.object(db_api, 'stack_count_all')
|
||||
def test_count_stacks_tenant_safe_default_true(self, mock_stack_count_all):
|
||||
self.eng.count_stacks(self.ctx)
|
||||
mock_stack_count_all.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
tenant_safe=True)
|
||||
|
||||
@mock.patch.object(db_api, 'stack_count_all')
|
||||
def test_count_stacks_passes_tenant_safe_info(self, mock_stack_count_all):
|
||||
self.eng.count_stacks(self.ctx, tenant_safe=False)
|
||||
mock_stack_count_all.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
tenant_safe=False)
|
||||
|
||||
@stack_context('service_abandon_stack')
|
||||
def test_abandon_stack(self):
|
||||
self.m.StubOutWithMock(parser.Stack, 'load')
|
||||
|
|
|
@ -88,10 +88,18 @@ class EngineRpcAPITestCase(testtools.TestCase):
|
|||
'sort_keys': mock.ANY,
|
||||
'marker': mock.ANY,
|
||||
'sort_dir': mock.ANY,
|
||||
'filters': mock.ANY
|
||||
'filters': mock.ANY,
|
||||
'tenant_safe': mock.ANY,
|
||||
}
|
||||
self._test_engine_api('list_stacks', 'call', **default_args)
|
||||
|
||||
def test_count_stacks(self):
|
||||
default_args = {
|
||||
'filters': mock.ANY,
|
||||
'tenant_safe': mock.ANY,
|
||||
}
|
||||
self._test_engine_api('count_stacks', 'call', **default_args)
|
||||
|
||||
def test_identify_stack(self):
|
||||
self._test_engine_api('identify_stack', 'call',
|
||||
stack_name='wordpress')
|
||||
|
|
Loading…
Reference in New Issue