[policy in code] Part 1 Base framework

This adds the basic framework for registering and using default policy
rules. Rules should be defined and returned from a module in
heat/policies/, and then added to the list in heat/policies/__init__.py.

new policy wrapers `registered_identified_stack` and
`registered_policy_enforce` has been added for policy enforcement of
registered rules with same parameter as `identified_stack` and
`policy_enforce` besides set `is_registered_policy` flag to true.
This flag will decide to use new policy framework or not.

Now we can use `tox -e genpolicy` to check and generate policy file.

Change-Id: I7a232b3ea7ce0f69a5b7ffa278ceace7a76b666f
Partially-Implements: bp policy-in-code
This commit is contained in:
ricolin 2017-10-06 13:01:40 +08:00
parent 121627bce7
commit b171490450
11 changed files with 151 additions and 18 deletions

3
.gitignore vendored
View File

@ -25,5 +25,8 @@ etc/heat/heat.conf.sample
# integration tests requirements are auto-generated from stub file
heat_integrationtests/requirements.txt
# generated policy file
etc/heat/policy.json.sample
# Files created by releasenotes build
releasenotes/build

View File

@ -0,0 +1,4 @@
[DEFAULT]
format = json
namespace = heat
output_file = etc/heat/policy.json.sample

View File

@ -1,9 +1,4 @@
{
"context_is_admin": "role:admin and is_admin_project:True",
"project_admin": "role:admin",
"deny_stack_user": "not role:heat_stack_user",
"deny_everybody": "!",
"cloudformation:ListStacks": "rule:deny_stack_user",
"cloudformation:CreateStack": "rule:deny_stack_user",
"cloudformation:DescribeStacks": "rule:deny_stack_user",

View File

@ -22,17 +22,34 @@ def policy_enforce(handler):
"""Decorator that enforces policies.
Checks the path matches the request context and enforce policy defined in
policy.json.
policy.json or in policies.
This is a handler method decorator.
"""
return _policy_enforce(handler)
def registered_policy_enforce(handler):
"""Decorator that enforces policies.
Checks the path matches the request context and enforce policy defined in
policies.
This is a handler method decorator.
"""
return _policy_enforce(handler, is_registered_policy=True)
def _policy_enforce(handler, is_registered_policy=False):
@six.wraps(handler)
def handle_stack_method(controller, req, tenant_id, **kwargs):
if req.context.tenant_id != tenant_id and not req.context.is_admin:
raise exc.HTTPForbidden()
allowed = req.context.policy.enforce(context=req.context,
action=handler.__name__,
scope=controller.REQUEST_SCOPE)
allowed = req.context.policy.enforce(
context=req.context,
action=handler.__name__,
scope=controller.REQUEST_SCOPE,
is_registered_policy=is_registered_policy)
if not allowed:
raise exc.HTTPForbidden()
return handler(controller, req, **kwargs)
@ -45,7 +62,21 @@ def identified_stack(handler):
This is a handler method decorator.
"""
@policy_enforce
return _identified_stack(handler)
def registered_identified_stack(handler):
"""Decorator that passes a stack identifier instead of path components.
This is a handler method decorator.
"""
return _identified_stack(handler, is_registered_policy=True)
def _identified_stack(handler, is_registered_policy=False):
@six.wraps(handler)
def handle_stack_method(controller, req, stack_name, stack_id, **kwargs):
stack_identity = identifier.HeatIdentifier(req.context.tenant_id,
@ -53,7 +84,8 @@ def identified_stack(handler):
stack_id)
return handler(controller, req, dict(stack_identity), **kwargs)
return handle_stack_method
return _policy_enforce(handle_stack_method,
is_registered_policy=is_registered_policy)
def make_url(req, identity):

View File

@ -20,9 +20,12 @@
from oslo_config import cfg
from oslo_log import log as logging
from oslo_policy import policy
from oslo_utils import excutils
import six
from heat.common import exception
from heat.common.i18n import _
from heat import policies
CONF = cfg.CONF
@ -45,6 +48,9 @@ class Enforcer(object):
self.enforcer = policy.Enforcer(
CONF, default_rule=default_rule, policy_file=policy_file)
# register rules
self.enforcer.register_defaults(policies.list_rules())
def set_rules(self, rules, overwrite=True):
"""Create a new Rules object based on the provided dict of rules."""
rules_obj = policy.Rules(rules, self.default_rule)
@ -54,7 +60,8 @@ class Enforcer(object):
"""Set the rules found in the json file on disk."""
self.enforcer.load_rules(force_reload)
def _check(self, context, rule, target, exc, *args, **kwargs):
def _check(self, context, rule, target, exc,
is_registered_policy=False, *args, **kwargs):
"""Verifies that the action is valid on the target in this context.
:param context: Heat request context
@ -65,10 +72,20 @@ class Enforcer(object):
"""
do_raise = False if not exc else True
credentials = context.to_policy_values()
return self.enforcer.enforce(rule, target, credentials,
do_raise, exc=exc, *args, **kwargs)
if is_registered_policy:
try:
return self.enforcer.authorize(rule, target, credentials,
do_raise=do_raise,
exc=exc, action=rule)
except policy.PolicyNotRegistered:
with excutils.save_and_reraise_exception():
LOG.exception(_('Policy not registered.'))
else:
return self.enforcer.enforce(rule, target, credentials,
do_raise, exc=exc, *args, **kwargs)
def enforce(self, context, action, scope=None, target=None):
def enforce(self, context, action, scope=None, target=None,
is_registered_policy=False):
"""Verifies that the action is valid on the target in this context.
:param context: Heat request context
@ -79,10 +96,11 @@ class Enforcer(object):
"""
_action = '%s:%s' % (scope or self.scope, action)
_target = target or {}
return self._check(context, _action, _target, self.exc, action=action)
return self._check(context, _action, _target, self.exc, action=action,
is_registered_policy=is_registered_policy)
def check_is_admin(self, context):
"""Whether or not is admin according to policy.json.
"""Whether or not is admin according to policy.
By default the rule will check whether or not roles contains
'admin' role and is admin project.

22
heat/policies/__init__.py Normal file
View File

@ -0,0 +1,22 @@
# 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 itertools
from heat.policies import base
def list_rules():
return itertools.chain(
base.list_rules(),
)

48
heat/policies/base.py Normal file
View File

@ -0,0 +1,48 @@
# 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
RULE_CONTEXT_IS_ADMIN = 'rule:context_is_admin'
RULE_PROJECT_ADMIN = 'rule:project_admin'
RULE_DENY_STACK_USER = 'rule:deny_stack_user'
RULE_DENY_EVERYBODY = 'rule:deny_everybody'
RULE_ALLOW_EVERYBODY = 'rule:allow_everybody'
rules = [
policy.RuleDefault(
name="context_is_admin",
check_str="role:admin and is_admin_project:True",
description="Decides what is required for the 'is_admin:True' check "
"to succeed."),
policy.RuleDefault(
name="project_admin",
check_str="role:admin",
description="Default rule for project admin."),
policy.RuleDefault(
name="deny_stack_user",
check_str="not role:heat_stack_user",
description="Default rule for deny stack user."),
policy.RuleDefault(
name="deny_everybody",
check_str="!",
description="Default rule for deny everybody."),
policy.RuleDefault(
name="allow_everybody",
check_str="",
description="Default rule for allow everybody.")
]
def list_rules():
return rules

View File

@ -473,6 +473,7 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase):
self.controller.index(req, tenant_id=self.tenant)
mock_enforce.assert_called_with(action='global_index',
scope=self.controller.REQUEST_SCOPE,
is_registered_policy=False,
context=self.context)
def test_global_index_uses_admin_context(self, mock_enforce):

View File

@ -11,6 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslo_config import cfg
from oslo_log import log
from oslo_messaging._drivers import common as rpc_common
@ -117,7 +118,9 @@ class ControllerTest(object):
self.mock_enforce.assert_called_with(
action=self.action,
context=self.context,
scope=self.controller.REQUEST_SCOPE)
scope=self.controller.REQUEST_SCOPE,
is_registered_policy=mock.ANY
)
self.assertEqual(self.expected_request_count,
len(self.mock_enforce.call_args_list))
super(ControllerTest, self).tearDown()

View File

@ -63,6 +63,9 @@ oslo.config.opts =
oslo.config.opts.defaults =
heat.common.config = heat.common.config:set_config_defaults
oslo.policy.policies =
heat = heat.policies:list_rules
heat.clients =
aodh = heat.engine.clients.os.aodh:AodhClientPlugin
barbican = heat.engine.clients.os.barbican:BarbicanClientPlugin

View File

@ -72,6 +72,10 @@ commands =
commands =
oslo-config-generator --config-file=config-generator.conf
[testenv:genpolicy]
commands =
oslopolicy-sample-generator --config-file etc/heat/heat-policy-generator.conf
[testenv:bandit]
deps = -r{toxinidir}/test-requirements.txt
# The following bandit tests are being skipped: