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 <shardy@redhat.com>
This commit is contained in:
parent
d2223869f9
commit
1b1dd456b2
@ -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"
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user