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:
Anderson Mesquita 2014-02-03 15:41:13 -05:00
parent f329dfdd98
commit 3261c58e73
9 changed files with 122 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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