--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2011 OpenStack, LLC.
+# All Rights Reserved.
+#
+# 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.
+
+# Based on glance/api/policy.py
+"""Policy Engine For Heat"""
+
+import json
+import os.path
+
+from heat.common import exception
+from heat.openstack.common import cfg
+import heat.openstack.common.log as logging
+from heat.openstack.common import policy
+
+LOG = logging.getLogger(__name__)
+
+policy_opts = [
+ cfg.StrOpt('policy_file', default='policy.json'),
+ cfg.StrOpt('policy_default_rule', default='default'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(policy_opts)
+
+
+DEFAULT_RULES = {
+ 'default': policy.TrueCheck(),
+}
+
+
+class Enforcer(object):
+ """Responsible for loading and enforcing rules"""
+
+ def __init__(self, scope='heat', exc=exception.Forbidden):
+ self.scope = scope
+ self.exc = exc
+ self.default_rule = CONF.policy_default_rule
+ self.policy_path = self._find_policy_file()
+ self.policy_file_mtime = None
+ self.policy_file_contents = None
+
+ def set_rules(self, rules):
+ """Create a new Rules object based on the provided dict of rules"""
+ rules_obj = policy.Rules(rules, self.default_rule)
+ policy.set_rules(rules_obj)
+
+ def load_rules(self):
+ """Set the rules found in the json file on disk"""
+ if self.policy_path:
+ rules = self._read_policy_file()
+ rule_type = ""
+ else:
+ rules = DEFAULT_RULES
+ rule_type = "default "
+
+ text_rules = dict((k, str(v)) for k, v in rules.items())
+ LOG.debug(_('Loaded %(rule_type)spolicy rules: %(text_rules)s') %
+ locals())
+
+ self.set_rules(rules)
+
+ @staticmethod
+ def _find_policy_file():
+ """Locate the policy json data file"""
+ policy_file = CONF.find_file(CONF.policy_file)
+ if policy_file:
+ return policy_file
+ else:
+ LOG.warn(_('Unable to find policy file'))
+ return None
+
+ def _read_policy_file(self):
+ """Read contents of the policy file
+
+ This re-caches policy data if the file has been changed.
+ """
+ mtime = os.path.getmtime(self.policy_path)
+ if not self.policy_file_contents or mtime != self.policy_file_mtime:
+ LOG.debug(_("Loading policy from %s") % self.policy_path)
+ with open(self.policy_path) as fap:
+ raw_contents = fap.read()
+ rules_dict = json.loads(raw_contents)
+ self.policy_file_contents = dict(
+ (k, policy.parse_rule(v))
+ for k, v in rules_dict.items())
+ self.policy_file_mtime = mtime
+ return self.policy_file_contents
+
+ def _check(self, context, rule, target, *args, **kwargs):
+ """Verifies that the action is valid on the target in this context.
+
+ :param context: Heat request context
+ :param rule: String representing the action to be checked
+ :param object: Dictionary representing the object of the action.
+ :raises: self.exc (defaults to heat.common.exception.Forbidden)
+ :returns: A non-False value if access is allowed.
+ """
+ self.load_rules()
+
+ credentials = {
+ 'roles': context.roles,
+ 'user': context.username,
+ 'tenant': context.tenant,
+ }
+
+ return policy.check(rule, target, credentials, *args, **kwargs)
+
+ def enforce(self, context, action, target):
+ """Verifies that the action is valid on the target in this context.
+
+ :param context: Heat request context
+ :param action: String representing the action to be checked
+ :param object: Dictionary representing the object of the action.
+ :raises: self.exc (defaults to heat.common.exception.Forbidden)
+ :returns: A non-False value if access is allowed.
+ """
+ _action = '%s:%s' % (self.scope, action)
+ return self._check(context, _action, target, self.exc, action=action)
+
+ def check(self, context, action, target):
+ """Verifies that the action is valid on the target in this context.
+
+ :param context: Heat request context
+ :param action: String representing the action to be checked
+ :param object: Dictionary representing the object of the action.
+ :returns: A non-False value if access is allowed.
+ """
+ return self._check(context, action, target)
--- /dev/null
+{
+ "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"
+}
--- /dev/null
+{
+ "cloudformation:ListStacks": "!",
+ "cloudformation:CreateStack": "!",
+ "cloudformation:DescribeStacks": "!",
+ "cloudformation:DeleteStack": "!",
+ "cloudformation:UpdateStack": "!",
+ "cloudformation:DescribeStackEvents": "!",
+ "cloudformation:ValidateTemplate": "!",
+ "cloudformation:GetTemplate": "!",
+ "cloudformation:EstimateTemplateCost": "!",
+ "cloudformation:DescribeStackResource": "!",
+ "cloudformation:DescribeStackResources": "!",
+ "cloudformation:ListStackResources": "!"
+}
--- /dev/null
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+# 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.
+
+import mox
+import json
+import unittest
+from nose.plugins.attrib import attr
+
+import os.path
+
+import heat.api
+from heat.common import context
+from heat.common import policy
+from heat.common import exception
+from heat.openstack.common import cfg
+
+
+@attr(tag=['unit', 'common-policy', 'Enforcer'])
+@attr(speed='fast')
+class TestPolicyEnforcer(unittest.TestCase):
+ cfn_actions = ("ListStacks", "CreateStack", "DescribeStacks",
+ "DeleteStack", "UpdateStack", "DescribeStackEvents",
+ "ValidateTemplate", "GetTemplate",
+ "EstimateTemplateCost", "DescribeStackResource",
+ "DescribeStackResources")
+
+ def setUp(self):
+ self.path = os.path.dirname(os.path.realpath(__file__)) + "/policy/"
+ self.m = mox.Mox()
+ opts = [
+ cfg.StrOpt('config_dir', default=self.path),
+ cfg.StrOpt('config_file', default='foo'),
+ cfg.StrOpt('project', default='heat'),
+ ]
+ cfg.CONF.register_opts(opts)
+ print "setup complete"
+
+ def tearDown(self):
+ self.m.UnsetStubs()
+ print "teardown complete"
+
+ def test_policy_cfn_default(self):
+ enforcer = policy.Enforcer(scope='cloudformation')
+
+ ctx = context.RequestContext(roles=[])
+ for action in self.cfn_actions:
+ # Everything should be allowed
+ enforcer.enforce(ctx, action, {})
+
+ def test_policy_cfn_notallowed(self):
+ pf = self.path + 'notallowed.json'
+ self.m.StubOutWithMock(policy.Enforcer, '_find_policy_file')
+ policy.Enforcer._find_policy_file().MultipleTimes().AndReturn(pf)
+ self.m.ReplayAll()
+
+ enforcer = policy.Enforcer(scope='cloudformation')
+
+ ctx = context.RequestContext(roles=[])
+ for action in self.cfn_actions:
+ # Everything should raise the default exception.Forbidden
+ self.assertRaises(exception.Forbidden, enforcer.enforce, ctx,
+ action, {})
+ self.m.VerifyAll()
+
+ def test_policy_cfn_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='cloudformation')
+
+ ctx = context.RequestContext(roles=['heat_stack_user'])
+ for action in self.cfn_actions:
+ # Everything apart from DescribeStackResource should be Forbidden
+ if action == "DescribeStackResource":
+ enforcer.enforce(ctx, action, {})
+ else:
+ self.assertRaises(exception.Forbidden, enforcer.enforce, ctx,
+ action, {})
+ self.m.VerifyAll()
+
+ def test_policy_cfn_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='cloudformation')
+
+ ctx = context.RequestContext(roles=['not_a_stack_user'])
+ for action in self.cfn_actions:
+ # Everything should be allowed
+ enforcer.enforce(ctx, action, {})
+ self.m.VerifyAll()