]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
heat api : Add policy.json authorization to cloudwatch API
authorSteven Hardy <shardy@redhat.com>
Wed, 6 Feb 2013 16:32:54 +0000 (16:32 +0000)
committerSteven Hardy <shardy@redhat.com>
Thu, 7 Feb 2013 10:22:14 +0000 (10:22 +0000)
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 <shardy@redhat.com>
etc/heat/policy.json
heat/api/cloudwatch/watch.py
heat/tests/policy/deny_stack_user.json
heat/tests/test_api_cloudwatch.py
heat/tests/test_common_policy.py

index 9ad32c9c63e46db0b4195423bbad472af9d35be3..6c0fec87fabae4f5552c41eaa9d4b82e69c2aa49 100644 (file)
     "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"
 }
index 1e27bb38b9e94c1e9e56512b14e40c40b4f4791f..2e439dbecb21ea27cf6eaf1c74e4cdcfd649da83 100644 (file)
@@ -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,
index 9ad32c9c63e46db0b4195423bbad472af9d35be3..6c0fec87fabae4f5552c41eaa9d4b82e69c2aa49 100644 (file)
     "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"
 }
index bc74ac5ac4e149aa185618d432f2be064f98680e..efb9b64eae1ed36f43ad9b1724ded7ecc5c67ab7 100644 (file)
 #    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):
index b34af14fef8cf5c330469bb7a52011c0b1f6e36a..336532bb147a3368641be5c9bc64f02e20de1a0a 100644 (file)
@@ -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()