diff --git a/etc/heat/policy.json b/etc/heat/policy.json index 5c1f6a9686..7ea27b1134 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -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", diff --git a/heat/api/middleware/fault.py b/heat/api/middleware/fault.py index ec6403557f..06769acfaa 100644 --- a/heat/api/middleware/fault.py +++ b/heat/api/middleware/fault.py @@ -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, diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index dd1b5336c4..d1902e656f 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -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): """ diff --git a/heat/engine/service.py b/heat/engine/service.py index e7b6428fe6..2386338d37 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -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': diff --git a/heat/rpc/client.py b/heat/rpc/client.py index 1531a594e6..589aeca31b 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -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): """ diff --git a/heat/tests/test_api_cfn_v1.py b/heat/tests/test_api_cfn_v1.py index 379fc7d9e1..56ea398336 100644 --- a/heat/tests/test_api_cfn_v1.py +++ b/heat/tests/test_api_cfn_v1.py @@ -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', diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index a432fb1047..46defa2184 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -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', diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index c49c76d36a..f9d4fa4c3e 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -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') diff --git a/heat/tests/test_rpc_client.py b/heat/tests/test_rpc_client.py index 2caba730dc..305c756577 100644 --- a/heat/tests/test_rpc_client.py +++ b/heat/tests/test_rpc_client.py @@ -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')