From 1b1dd456b21a32235ab0327f9fbc312892ec6c46 Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Wed, 6 Feb 2013 16:32:54 +0000 Subject: [PATCH] heat api : Add policy.json authorization to cloudwatch API Adds a basic policy.json to authorize all actions for the CW 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 PutMetricData action, which is used by cfn-push-stats to provide metric data from the instances Change-Id: I2bbb885bec98b85828cdb92d7efc0688da7be3c1 Signed-off-by: Steven Hardy --- etc/heat/policy.json | 14 +++++++- heat/api/cloudwatch/watch.py | 31 +++++++++++++++++ heat/tests/policy/deny_stack_user.json | 14 +++++++- heat/tests/test_api_cloudwatch.py | 48 +++++++++++++++++++++++++- heat/tests/test_common_policy.py | 37 ++++++++++++++++++++ 5 files changed, 141 insertions(+), 3 deletions(-) diff --git a/etc/heat/policy.json b/etc/heat/policy.json index 9ad32c9c..6c0fec87 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -11,5 +11,17 @@ "cloudformation:EstimateTemplateCost": "rule:deny_stack_user", "cloudformation:DescribeStackResource": "", "cloudformation:DescribeStackResources": "rule:deny_stack_user", - "cloudformation:ListStackResources": "rule:deny_stack_user" + "cloudformation:ListStackResources": "rule:deny_stack_user", + + "cloudwatch:DeleteAlarms": "rule:deny_stack_user", + "cloudwatch:DescribeAlarmHistory": "rule:deny_stack_user", + "cloudwatch:DescribeAlarms": "rule:deny_stack_user", + "cloudwatch:DescribeAlarmsForMetric": "rule:deny_stack_user", + "cloudwatch:DisableAlarmActions": "rule:deny_stack_user", + "cloudwatch:EnableAlarmActions": "rule:deny_stack_user", + "cloudwatch:GetMetricStatistics": "rule:deny_stack_user", + "cloudwatch:ListMetrics": "rule:deny_stack_user", + "cloudwatch:PutMetricAlarm": "rule:deny_stack_user", + "cloudwatch:PutMetricData": "", + "cloudwatch:SetAlarmState": "rule:deny_stack_user" } diff --git a/heat/api/cloudwatch/watch.py b/heat/api/cloudwatch/watch.py index 1e27bb38..2e439dbe 100644 --- a/heat/api/cloudwatch/watch.py +++ b/heat/api/cloudwatch/watch.py @@ -19,6 +19,8 @@ endpoint for heat AWS-compatible CloudWatch API from heat.api.aws import exception from heat.api.aws import utils as api_utils from heat.common import wsgi +from heat.common import policy +from heat.common import exception as heat_exception from heat.rpc import client as rpc_client from heat.rpc import api as engine_api @@ -38,6 +40,22 @@ class WatchController(object): def __init__(self, options): self.options = options self.engine_rpcapi = rpc_client.EngineClient() + self.policy = policy.Enforcer(scope='cloudwatch') + + 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 _reformat_dimensions(dims): @@ -55,18 +73,21 @@ class WatchController(object): """ Implements DeleteAlarms API action """ + self._enforce(req, 'DeleteAlarms') return exception.HeatAPINotImplementedError() def describe_alarm_history(self, req): """ Implements DescribeAlarmHistory API action """ + self._enforce(req, 'DescribeAlarmHistory') return exception.HeatAPINotImplementedError() def describe_alarms(self, req): """ Implements DescribeAlarms API action """ + self._enforce(req, 'DescribeAlarms') def format_metric_alarm(a): """ @@ -131,24 +152,28 @@ class WatchController(object): """ Implements DescribeAlarmsForMetric API action """ + self._enforce(req, 'DescribeAlarmsForMetric') return exception.HeatAPINotImplementedError() def disable_alarm_actions(self, req): """ Implements DisableAlarmActions API action """ + self._enforce(req, 'DisableAlarmActions') return exception.HeatAPINotImplementedError() def enable_alarm_actions(self, req): """ Implements EnableAlarmActions API action """ + self._enforce(req, 'EnableAlarmActions') return exception.HeatAPINotImplementedError() def get_metric_statistics(self, req): """ Implements GetMetricStatistics API action """ + self._enforce(req, 'GetMetricStatistics') return exception.HeatAPINotImplementedError() def list_metrics(self, req): @@ -157,6 +182,8 @@ class WatchController(object): Lists metric datapoints associated with a particular alarm, or all alarms if none specified """ + self._enforce(req, 'ListMetrics') + def format_metric_data(d, fil={}): """ Reformat engine output into the AWS "Metric" format @@ -219,12 +246,14 @@ class WatchController(object): """ Implements PutMetricAlarm API action """ + self._enforce(req, 'PutMetricAlarm') return exception.HeatAPINotImplementedError() def put_metric_data(self, req): """ Implements PutMetricData API action """ + self._enforce(req, 'PutMetricData') con = req.context parms = dict(req.params) @@ -283,6 +312,8 @@ class WatchController(object): """ Implements SetAlarmState API action """ + self._enforce(req, 'SetAlarmState') + # Map from AWS state names to those used in the engine state_map = {'OK': engine_api.WATCH_STATE_OK, 'ALARM': engine_api.WATCH_STATE_ALARM, diff --git a/heat/tests/policy/deny_stack_user.json b/heat/tests/policy/deny_stack_user.json index 9ad32c9c..6c0fec87 100644 --- a/heat/tests/policy/deny_stack_user.json +++ b/heat/tests/policy/deny_stack_user.json @@ -11,5 +11,17 @@ "cloudformation:EstimateTemplateCost": "rule:deny_stack_user", "cloudformation:DescribeStackResource": "", "cloudformation:DescribeStackResources": "rule:deny_stack_user", - "cloudformation:ListStackResources": "rule:deny_stack_user" + "cloudformation:ListStackResources": "rule:deny_stack_user", + + "cloudwatch:DeleteAlarms": "rule:deny_stack_user", + "cloudwatch:DescribeAlarmHistory": "rule:deny_stack_user", + "cloudwatch:DescribeAlarms": "rule:deny_stack_user", + "cloudwatch:DescribeAlarmsForMetric": "rule:deny_stack_user", + "cloudwatch:DisableAlarmActions": "rule:deny_stack_user", + "cloudwatch:EnableAlarmActions": "rule:deny_stack_user", + "cloudwatch:GetMetricStatistics": "rule:deny_stack_user", + "cloudwatch:ListMetrics": "rule:deny_stack_user", + "cloudwatch:PutMetricAlarm": "rule:deny_stack_user", + "cloudwatch:PutMetricData": "", + "cloudwatch:SetAlarmState": "rule:deny_stack_user" } diff --git a/heat/tests/test_api_cloudwatch.py b/heat/tests/test_api_cloudwatch.py index bc74ac5a..efb9b64e 100644 --- a/heat/tests/test_api_cloudwatch.py +++ b/heat/tests/test_api_cloudwatch.py @@ -12,12 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. - +import os import mox import unittest from nose.plugins.attrib import attr from heat.common import context +from heat.common import policy from heat.openstack.common import cfg from heat.openstack.common import rpc from heat.common.wsgi import Request @@ -59,6 +60,42 @@ class WatchControllerTest(unittest.TestCase): {'Name': 'Foo', 'Value': 'bar'}] self.assert_(response == expected) + def test_enforce_default(self): + self.m.ReplayAll() + params = {'Action': 'ListMetrics'} + dummy_req = self._dummy_GET_request(params) + self.controller.policy.policy_path = None + response = self.controller._enforce(dummy_req, 'ListMetrics') + self.assertEqual(response, None) + self.m.VerifyAll() + + def test_enforce_denied(self): + self.m.ReplayAll() + params = {'Action': 'ListMetrics'} + 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, 'ListMetrics') + self.m.VerifyAll() + + def test_enforce_ise(self): + params = {'Action': 'ListMetrics'} + 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, 'ListMetrics', {} + ).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, 'ListMetrics') + self.m.VerifyAll() + def test_delete(self): # Not yet implemented, should raise HeatAPINotImplementedError params = {'Action': 'DeleteAlarms'} @@ -474,6 +511,14 @@ class WatchControllerTest(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) @@ -484,6 +529,7 @@ class WatchControllerTest(unittest.TestCase): bind_port = 8003 cfgopts = DummyConfig() self.controller = watches.WatchController(options=cfgopts) + self.controller.policy.policy_path = None print "setup complete" def tearDown(self): diff --git a/heat/tests/test_common_policy.py b/heat/tests/test_common_policy.py index b34af14f..336532bb 100644 --- a/heat/tests/test_common_policy.py +++ b/heat/tests/test_common_policy.py @@ -36,6 +36,11 @@ class TestPolicyEnforcer(unittest.TestCase): "EstimateTemplateCost", "DescribeStackResource", "DescribeStackResources") + cw_actions = ("DeleteAlarms", "DescribeAlarmHistory", "DescribeAlarms", + "DescribeAlarmsForMetric", "DisableAlarmActions", + "EnableAlarmActions", "GetMetricStatistics", "ListMetrics", + "PutMetricAlarm", "PutMetricData", "SetAlarmState") + def setUp(self): self.path = os.path.dirname(os.path.realpath(__file__)) + "/policy/" self.m = mox.Mox() @@ -105,3 +110,35 @@ class TestPolicyEnforcer(unittest.TestCase): # Everything should be allowed enforcer.enforce(ctx, action, {}) self.m.VerifyAll() + + def test_policy_cw_deny_stack_user(self): + pf = self.path + 'deny_stack_user.json' + self.m.StubOutWithMock(policy.Enforcer, '_find_policy_file') + policy.Enforcer._find_policy_file().MultipleTimes().AndReturn(pf) + self.m.ReplayAll() + + enforcer = policy.Enforcer(scope='cloudwatch') + + ctx = context.RequestContext(roles=['heat_stack_user']) + for action in self.cw_actions: + # Everything apart from PutMetricData should be Forbidden + if action == "PutMetricData": + enforcer.enforce(ctx, action, {}) + else: + self.assertRaises(exception.Forbidden, enforcer.enforce, ctx, + action, {}) + self.m.VerifyAll() + + def test_policy_cw_allow_non_stack_user(self): + pf = self.path + 'deny_stack_user.json' + self.m.StubOutWithMock(policy.Enforcer, '_find_policy_file') + policy.Enforcer._find_policy_file().MultipleTimes().AndReturn(pf) + self.m.ReplayAll() + + enforcer = policy.Enforcer(scope='cloudwatch') + + ctx = context.RequestContext(roles=['not_a_stack_user']) + for action in self.cw_actions: + # Everything should be allowed + enforcer.enforce(ctx, action, {}) + self.m.VerifyAll() -- 2.45.2