]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
heat api : Add policy.json authorization to CFN API
authorSteven Hardy <shardy@redhat.com>
Tue, 5 Feb 2013 19:23:38 +0000 (19:23 +0000)
committerSteven Hardy <shardy@redhat.com>
Wed, 6 Feb 2013 14:58:23 +0000 (14:58 +0000)
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 <shardy@redhat.com>
etc/heat/policy.json [new file with mode: 0644]
heat/api/cfn/v1/stacks.py
heat/tests/test_api_cfn_v1.py

diff --git a/etc/heat/policy.json b/etc/heat/policy.json
new file mode 100644 (file)
index 0000000..9ad32c9
--- /dev/null
@@ -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"
+}
index fdb3a5e2021b6708bee6d001c7d0e1f55c98930c..1b645b7d6a62f46336ace3ec97a99020ab523e3b 100644 (file)
@@ -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
index 8378c92ab8093697cc1d03be0b8a4b000d500600..0c3f27f55c8469588d2ec4773ec75cf186b01e83 100644 (file)
@@ -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)