diff --git a/devstack/lib/heat b/devstack/lib/heat index 7bd44d1745..fa5fed8f3b 100644 --- a/devstack/lib/heat +++ b/devstack/lib/heat @@ -121,7 +121,10 @@ function configure_heat { HEAT_POLICY_FILE=$HEAT_CONF_DIR/policy.json cp $HEAT_DIR/etc/heat/api-paste.ini $HEAT_API_PASTE_FILE - cp $HEAT_DIR/etc/heat/policy.json $HEAT_POLICY_FILE + + if [[ -f $HEAT_DIR/etc/heat/policy.json ]]; then + cp $HEAT_DIR/etc/heat/policy.json $HEAT_POLICY_FILE + fi # common options iniset_rpc_backend heat $HEAT_CONF diff --git a/etc/heat/policy.json b/etc/heat/policy.json deleted file mode 100644 index 026ab9e0aa..0000000000 --- a/etc/heat/policy.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "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:CancelUpdateStack": "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", - - "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/cfn/v1/stacks.py b/heat/api/cfn/v1/stacks.py index 8ed72ccc7c..245c8d57d9 100644 --- a/heat/api/cfn/v1/stacks.py +++ b/heat/api/cfn/v1/stacks.py @@ -49,9 +49,9 @@ class StackController(object): raise exception.HeatInvalidActionError() def _enforce(self, req, action): - """Authorize an action against the policy.json.""" + """Authorize an action against the policy.json and policies in code.""" try: - self.policy.enforce(req.context, action) + self.policy.enforce(req.context, action, is_registered_policy=True) except heat_exception.Forbidden: msg = _('Action %s not allowed for user') % action raise exception.HeatAccessDeniedError(msg) diff --git a/heat/api/cloudwatch/watch.py b/heat/api/cloudwatch/watch.py index 368b07d302..5c4dd2edaa 100644 --- a/heat/api/cloudwatch/watch.py +++ b/heat/api/cloudwatch/watch.py @@ -42,9 +42,9 @@ class WatchController(object): self.policy = policy.Enforcer(scope='cloudwatch') def _enforce(self, req, action): - """Authorize an action against the policy.json.""" + """Authorize an action against the policy.json and policies in code.""" try: - self.policy.enforce(req.context, action) + self.policy.enforce(req.context, action, is_registered_policy=True) except heat_exception.Forbidden: msg = _("Action %s not allowed for user") % action raise exception.HeatAccessDeniedError(msg) diff --git a/heat/policies/__init__.py b/heat/policies/__init__.py index b11043c454..e36fcfed11 100644 --- a/heat/policies/__init__.py +++ b/heat/policies/__init__.py @@ -16,6 +16,8 @@ import itertools from heat.policies import actions from heat.policies import base from heat.policies import build_info +from heat.policies import cloudformation +from heat.policies import cloudwatch from heat.policies import events from heat.policies import resource from heat.policies import resource_types @@ -30,6 +32,8 @@ def list_rules(): base.list_rules(), actions.list_rules(), build_info.list_rules(), + cloudformation.list_rules(), + cloudwatch.list_rules(), events.list_rules(), resource.list_rules(), resource_types.list_rules(), diff --git a/heat/policies/cloudformation.py b/heat/policies/cloudformation.py new file mode 100644 index 0000000000..aa61fa9a00 --- /dev/null +++ b/heat/policies/cloudformation.py @@ -0,0 +1,66 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from heat.policies import base + +# These policies are for AWS CloudFormation-like APIs, so we won't list out +# the URI paths in rules. + +POLICY_ROOT = 'cloudformation:%s' + +cloudformation_policies = [ + policy.RuleDefault( + name=POLICY_ROOT % 'ListStacks', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'CreateStack', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DescribeStacks', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DeleteStack', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'UpdateStack', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'CancelUpdateStack', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DescribeStackEvents', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'ValidateTemplate', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'GetTemplate', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'EstimateTemplateCost', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DescribeStackResource', + check_str=base.RULE_ALLOW_EVERYBODY), + policy.RuleDefault( + name=POLICY_ROOT % 'DescribeStackResources', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'ListStackResources', + check_str=base.RULE_DENY_STACK_USER) +] + + +def list_rules(): + return cloudformation_policies diff --git a/heat/policies/cloudwatch.py b/heat/policies/cloudwatch.py new file mode 100644 index 0000000000..37cb54013f --- /dev/null +++ b/heat/policies/cloudwatch.py @@ -0,0 +1,63 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from heat.policies import base + + +# These policies are for AWS CloudWatch-like APIs, so we won't list out the URI +# paths in rules. + + +POLICY_ROOT = 'cloudwatch:%s' + + +cloudwatch_policies = [ + policy.RuleDefault( + name=POLICY_ROOT % 'DeleteAlarms', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DescribeAlarmHistory', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DescribeAlarms', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DescribeAlarmsForMetric', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'DisableAlarmActions', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'EnableAlarmActions', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'GetMetricStatistics', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'ListMetrics', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'PutMetricAlarm', + check_str=base.RULE_DENY_STACK_USER), + policy.RuleDefault( + name=POLICY_ROOT % 'PutMetricData', + check_str=base.RULE_ALLOW_EVERYBODY), + policy.RuleDefault( + name=POLICY_ROOT % 'SetAlarmState', + check_str=base.RULE_DENY_STACK_USER) +] + + +def list_rules(): + return cloudwatch_policies diff --git a/heat/tests/api/cfn/test_api_cfn_v1.py b/heat/tests/api/cfn/test_api_cfn_v1.py index 5845e17847..e005148dd2 100644 --- a/heat/tests/api/cfn/test_api_cfn_v1.py +++ b/heat/tests/api/cfn/test_api_cfn_v1.py @@ -72,14 +72,11 @@ class CfnStackControllerTest(common.HeatTestCase): return req def _stub_enforce(self, req, action, allowed=True): - self.m.StubOutWithMock(policy.Enforcer, 'enforce') + mock_enforce = self.patchobject(policy.Enforcer, 'enforce') if allowed: - policy.Enforcer.enforce(req.context, action - ).AndReturn(True) + mock_enforce.return_value = True else: - policy.Enforcer.enforce(req.context, action - ).AndRaise(heat_exception.Forbidden) - self.m.ReplayAll() + mock_enforce.side_effect = heat_exception.Forbidden # The tests def test_stackid_addprefix(self): @@ -118,10 +115,8 @@ class CfnStackControllerTest(common.HeatTestCase): 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() + mock_enforce = self.patchobject(policy.Enforcer, 'enforce') + mock_enforce.side_effect = AttributeError self.assertRaises(exception.HeatInternalFailureError, self.controller._enforce, dummy_req, 'ListStacks') @@ -512,10 +507,9 @@ class CfnStackControllerTest(common.HeatTestCase): engine_parms, engine_args, failure, need_stub=True): if need_stub: - self.m.StubOutWithMock(policy.Enforcer, 'enforce') + mock_enforce = self.patchobject(policy.Enforcer, 'enforce') self.m.StubOutWithMock(rpc_client.EngineClient, 'call') - policy.Enforcer.enforce(req_context, - 'CreateStack').AndReturn(True) + mock_enforce.return_value = True # Insert an engine RPC error and ensure we map correctly to the # heat exception type diff --git a/heat/tests/test_common_policy.py b/heat/tests/test_common_policy.py index 1f149d4571..0157d66b3b 100644 --- a/heat/tests/test_common_policy.py +++ b/heat/tests/test_common_policy.py @@ -17,6 +17,7 @@ import os.path from oslo_config import fixture as config_fixture +from oslo_policy import policy as base_policy from heat.common import exception from heat.common import policy @@ -48,14 +49,12 @@ class TestPolicyEnforcer(common.HeatTestCase): return policy_path + filename def test_policy_cfn_default(self): - enforcer = policy.Enforcer( - scope='cloudformation', - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer(scope='cloudformation') ctx = utils.dummy_context(roles=[]) for action in self.cfn_actions: # Everything should be allowed - enforcer.enforce(ctx, action) + enforcer.enforce(ctx, action, is_registered_policy=True) def test_policy_cfn_notallowed(self): enforcer = policy.Enforcer( @@ -66,96 +65,87 @@ class TestPolicyEnforcer(common.HeatTestCase): for action in self.cfn_actions: # Everything should raise the default exception.Forbidden self.assertRaises(exception.Forbidden, enforcer.enforce, ctx, - action, {}) + action, {}, is_registered_policy=True) def test_policy_cfn_deny_stack_user(self): - enforcer = policy.Enforcer( - scope='cloudformation', - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer(scope='cloudformation') ctx = utils.dummy_context(roles=['heat_stack_user']) for action in self.cfn_actions: # Everything apart from DescribeStackResource should be Forbidden if action == "DescribeStackResource": - enforcer.enforce(ctx, action) + enforcer.enforce(ctx, action, is_registered_policy=True) else: self.assertRaises(exception.Forbidden, enforcer.enforce, ctx, - action, {}) + action, {}, is_registered_policy=True) def test_policy_cfn_allow_non_stack_user(self): - enforcer = policy.Enforcer( - scope='cloudformation', - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer(scope='cloudformation') ctx = utils.dummy_context(roles=['not_a_stack_user']) for action in self.cfn_actions: # Everything should be allowed - enforcer.enforce(ctx, action) + enforcer.enforce(ctx, action, is_registered_policy=True) def test_policy_cw_deny_stack_user(self): - enforcer = policy.Enforcer( - scope='cloudwatch', - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer(scope='cloudwatch') ctx = utils.dummy_context(roles=['heat_stack_user']) for action in self.cw_actions: # Everything apart from PutMetricData should be Forbidden if action == "PutMetricData": - enforcer.enforce(ctx, action) + enforcer.enforce(ctx, action, is_registered_policy=True) else: self.assertRaises(exception.Forbidden, enforcer.enforce, ctx, - action, {}) + action, {}, is_registered_policy=True) def test_policy_cw_allow_non_stack_user(self): - enforcer = policy.Enforcer( - scope='cloudwatch', - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer(scope='cloudwatch') ctx = utils.dummy_context(roles=['not_a_stack_user']) for action in self.cw_actions: # Everything should be allowed - enforcer.enforce(ctx, action) + enforcer.enforce(ctx, action, is_registered_policy=True) def test_set_rules_overwrite_true(self): - enforcer = policy.Enforcer( - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer() enforcer.load_rules(True) enforcer.set_rules({'test_heat_rule': 1}, True) self.assertEqual({'test_heat_rule': 1}, enforcer.enforcer.rules) def test_set_rules_overwrite_false(self): - enforcer = policy.Enforcer( - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer() enforcer.load_rules(True) enforcer.load_rules(True) enforcer.set_rules({'test_heat_rule': 1}, False) self.assertIn('test_heat_rule', enforcer.enforcer.rules) def test_load_rules_force_reload_true(self): - enforcer = policy.Enforcer( - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer() enforcer.load_rules(True) enforcer.set_rules({'test_heat_rule': 'test'}) enforcer.load_rules(True) self.assertNotIn({'test_heat_rule': 'test'}, enforcer.enforcer.rules) def test_load_rules_force_reload_false(self): - enforcer = policy.Enforcer( - policy_file=self.get_policy_file('deny_stack_user.json')) + enforcer = policy.Enforcer() enforcer.load_rules(True) enforcer.load_rules(True) enforcer.set_rules({'test_heat_rule': 'test'}) enforcer.load_rules(False) self.assertIn('test_heat_rule', enforcer.enforcer.rules) - def test_default_rule(self): + def test_no_such_action(self): ctx = utils.dummy_context(roles=['not_a_stack_user']) - enforcer = policy.Enforcer( - scope='cloudformation', - policy_file=self.get_policy_file('deny_stack_user.json'), - exc=None, default_rule='!') + enforcer = policy.Enforcer(scope='cloudformation') action = 'no_such_action' - self.assertFalse(enforcer.enforce(ctx, action)) + msg = 'cloudformation:no_such_action has not been registered' + self.assertRaisesRegex(base_policy.PolicyNotRegistered, + msg, + enforcer.enforce, + ctx, action, + None, None, + True) def test_check_admin(self): enforcer = policy.Enforcer()