Add initial code to support policy.json implementation
We don't currently support a policy.json file like other openstack services, so this code (mostly copied from glance, then modified a bit) will allow us to add policy-based authorization to out APIs fairly easily Change-Id: I5ad9f55b3d0979e2526953bdce8b8227852e4b72 Signed-off-by: Steven Hardy <shardy@redhat.com>
This commit is contained in:
142
heat/common/policy.py
Normal file
142
heat/common/policy.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# 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)
|
||||
15
heat/tests/policy/deny_stack_user.json
Normal file
15
heat/tests/policy/deny_stack_user.json
Normal file
@@ -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"
|
||||
}
|
||||
14
heat/tests/policy/notallowed.json
Normal file
14
heat/tests/policy/notallowed.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"cloudformation:ListStacks": "!",
|
||||
"cloudformation:CreateStack": "!",
|
||||
"cloudformation:DescribeStacks": "!",
|
||||
"cloudformation:DeleteStack": "!",
|
||||
"cloudformation:UpdateStack": "!",
|
||||
"cloudformation:DescribeStackEvents": "!",
|
||||
"cloudformation:ValidateTemplate": "!",
|
||||
"cloudformation:GetTemplate": "!",
|
||||
"cloudformation:EstimateTemplateCost": "!",
|
||||
"cloudformation:DescribeStackResource": "!",
|
||||
"cloudformation:DescribeStackResources": "!",
|
||||
"cloudformation:ListStackResources": "!"
|
||||
}
|
||||
107
heat/tests/test_common_policy.py
Normal file
107
heat/tests/test_common_policy.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user