From dfacae65b5711ad960128c337295e756f6234b31 Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Tue, 5 Feb 2013 19:23:38 +0000 Subject: [PATCH] heat api : Add policy.json authorization to CFN API Adds a basic policy.json to authorize all actions for the CFN API - this will deny access to the in-instance users defined in stack templates (which are assigned the heat_stack_user role) to all API actions apart from DescribeStackResource, which is used for metadata updates ref bug 1115758 Change-Id: I1431c1f23593fffd0f911f71ef9c186a43e5063a Signed-off-by: Steven Hardy --- etc/heat/policy.json | 15 ++++++++++++ heat/api/cfn/v1/stacks.py | 33 +++++++++++++++++++++++++ heat/tests/test_api_cfn_v1.py | 46 +++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 etc/heat/policy.json diff --git a/etc/heat/policy.json b/etc/heat/policy.json new file mode 100644 index 00000000..9ad32c9c --- /dev/null +++ b/etc/heat/policy.json @@ -0,0 +1,15 @@ +{ + "deny_stack_user": "not role:heat_stack_user", + "cloudformation:ListStacks": "rule:deny_stack_user", + "cloudformation:CreateStack": "rule:deny_stack_user", + "cloudformation:DescribeStacks": "rule:deny_stack_user", + "cloudformation:DeleteStack": "rule:deny_stack_user", + "cloudformation:UpdateStack": "rule:deny_stack_user", + "cloudformation:DescribeStackEvents": "rule:deny_stack_user", + "cloudformation:ValidateTemplate": "rule:deny_stack_user", + "cloudformation:GetTemplate": "rule:deny_stack_user", + "cloudformation:EstimateTemplateCost": "rule:deny_stack_user", + "cloudformation:DescribeStackResource": "", + "cloudformation:DescribeStackResources": "rule:deny_stack_user", + "cloudformation:ListStackResources": "rule:deny_stack_user" +} diff --git a/heat/api/cfn/v1/stacks.py b/heat/api/cfn/v1/stacks.py index fdb3a5e2..1b645b7d 100644 --- a/heat/api/cfn/v1/stacks.py +++ b/heat/api/cfn/v1/stacks.py @@ -23,11 +23,13 @@ import socket from heat.api.aws import exception from heat.api.aws import utils as api_utils from heat.common import wsgi +from heat.common import exception as heat_exception from heat.rpc import client as rpc_client from heat.common import template_format from heat.rpc import api as engine_api from heat.common import identifier from heat.common import urlfetch +from heat.common import policy import heat.openstack.common.rpc.common as rpc_common @@ -47,6 +49,22 @@ class StackController(object): def __init__(self, options): self.options = options self.engine_rpcapi = rpc_client.EngineClient() + self.policy = policy.Enforcer(scope='cloudformation') + + def _enforce(self, req, action): + """Authorize an action against the policy.json""" + try: + self.policy.enforce(req.context, action, {}) + except heat_exception.Forbidden: + raise exception.HeatAccessDeniedError("Action %s not allowed " % + action + "for user") + except Exception as ex: + # We expect policy.enforce to either pass or raise Forbidden + # however, if anything else happens, we want to raise + # HeatInternalFailureError, failure to do this results in + # the user getting a big stacktrace spew as an API response + raise exception.HeatInternalFailureError("Error authorizing " + + "action %s" % action) @staticmethod def _id_format(resp): @@ -95,6 +113,7 @@ class StackController(object): Implements ListStacks API action Lists summary information for all stacks """ + self._enforce(req, 'ListStacks') def format_stack_summary(s): """ @@ -136,6 +155,8 @@ class StackController(object): Implements DescribeStacks API action Gets detailed information for a stack (or all stacks) """ + self._enforce(req, 'DescribeStacks') + def format_stack_outputs(o): keymap = { engine_api.OUTPUT_DESCRIPTION: 'Description', @@ -328,6 +349,7 @@ class StackController(object): Implements the GetTemplate API action Get the template body for an existing stack """ + self._enforce(req, 'GetTemplate') con = req.context try: @@ -348,6 +370,8 @@ class StackController(object): Implements the EstimateTemplateCost API action Get the estimated monthly cost of a template """ + self._enforce(req, 'EstimateTemplateCost') + return api_utils.format_response('EstimateTemplateCost', {'Url': 'http://en.wikipedia.org/wiki/Gratis' @@ -359,6 +383,7 @@ class StackController(object): Implements the ValidateTemplate API action Validates the specified template """ + self._enforce(req, 'ValidateTemplate') con = req.context try: @@ -388,6 +413,8 @@ class StackController(object): Implements the DeleteStack API action Deletes the specified stack """ + self._enforce(req, 'DeleteStack') + con = req.context try: identity = self._get_identity(con, req.params['StackName']) @@ -406,6 +433,8 @@ class StackController(object): Implements the DescribeStackEvents API action Returns events related to a specified stack (or all stacks) """ + self._enforce(req, 'DescribeStackEvents') + def format_stack_event(e): """ Reformat engine output into the AWS "StackEvent" format @@ -447,6 +476,7 @@ class StackController(object): Implements the DescribeStackResource API action Return the details of the given resource belonging to the given stack. """ + self._enforce(req, 'DescribeStackResource') def format_resource_detail(r): """ @@ -502,6 +532,7 @@ class StackController(object): `LogicalResourceId`: filter the resources list by the logical resource id. """ + self._enforce(req, 'DescribeStackResources') def format_stack_resource(r): """ @@ -555,6 +586,8 @@ class StackController(object): Implements the ListStackResources API action Return summary of the resources belonging to the specified stack. """ + self._enforce(req, 'ListStackResources') + def format_resource_summary(r): """ Reformat engine output into the AWS "StackResourceSummary" format diff --git a/heat/tests/test_api_cfn_v1.py b/heat/tests/test_api_cfn_v1.py index 8378c92a..0c3f27f5 100644 --- a/heat/tests/test_api_cfn_v1.py +++ b/heat/tests/test_api_cfn_v1.py @@ -15,6 +15,7 @@ import mox import json +import os import unittest from nose.plugins.attrib import attr @@ -22,6 +23,7 @@ import json from heat.common import context from heat.common import identifier +from heat.common import policy from heat.openstack.common import cfg from heat.openstack.common import rpc import heat.openstack.common.rpc.common as rpc_common @@ -72,6 +74,42 @@ class StackControllerTest(unittest.TestCase): self.assertEqual(response, expected) self.m.VerifyAll() + def test_enforce_default(self): + self.m.ReplayAll() + params = {'Action': 'ListStacks'} + dummy_req = self._dummy_GET_request(params) + self.controller.policy.policy_path = None + response = self.controller._enforce(dummy_req, 'ListStacks') + self.assertEqual(response, None) + self.m.VerifyAll() + + def test_enforce_denied(self): + self.m.ReplayAll() + params = {'Action': 'ListStacks'} + dummy_req = self._dummy_GET_request(params) + dummy_req.context.roles = ['heat_stack_user'] + self.controller.policy.policy_path = (self.policy_path + + 'deny_stack_user.json') + self.assertRaises(exception.HeatAccessDeniedError, + self.controller._enforce, dummy_req, 'ListStacks') + self.m.VerifyAll() + + def test_enforce_ise(self): + params = {'Action': 'ListStacks'} + dummy_req = self._dummy_GET_request(params) + dummy_req.context.roles = ['heat_stack_user'] + + self.m.StubOutWithMock(policy.Enforcer, 'enforce') + policy.Enforcer.enforce(dummy_req.context, 'ListStacks', {} + ).AndRaise(AttributeError) + self.m.ReplayAll() + + self.controller.policy.policy_path = (self.policy_path + + 'deny_stack_user.json') + self.assertRaises(exception.HeatInternalFailureError, + self.controller._enforce, dummy_req, 'ListStacks') + self.m.VerifyAll() + def test_list(self): # Format a dummy GET request to pass into the WSGI handler params = {'Action': 'ListStacks'} @@ -1360,6 +1398,14 @@ class StackControllerTest(unittest.TestCase): self.maxDiff = None self.m = mox.Mox() + self.path = os.path.dirname(os.path.realpath(__file__)) + self.policy_path = self.path + "/policy/" + opts = [ + cfg.StrOpt('config_dir', default=self.policy_path), + cfg.StrOpt('config_file', default='foo'), + cfg.StrOpt('project', default='heat'), + ] + cfg.CONF.register_opts(opts) cfg.CONF.set_default('engine_topic', 'engine') cfg.CONF.set_default('host', 'host') self.topic = '%s.%s' % (cfg.CONF.engine_topic, cfg.CONF.host) -- 2.45.2