[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 # integration tests requirements are auto-generated from stub file
heat_integrationtests/requirements.txt heat_integrationtests/requirements.txt
# generated policy file
etc/heat/policy.json.sample
# Files created by releasenotes build # Files created by releasenotes build
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:ListStacks": "rule:deny_stack_user",
"cloudformation:CreateStack": "rule:deny_stack_user", "cloudformation:CreateStack": "rule:deny_stack_user",
"cloudformation:DescribeStacks": "rule:deny_stack_user", "cloudformation:DescribeStacks": "rule:deny_stack_user",

View File

@ -22,17 +22,34 @@ def policy_enforce(handler):
"""Decorator that enforces policies. """Decorator that enforces policies.
Checks the path matches the request context and enforce policy defined in 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. 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) @six.wraps(handler)
def handle_stack_method(controller, req, tenant_id, **kwargs): def handle_stack_method(controller, req, tenant_id, **kwargs):
if req.context.tenant_id != tenant_id and not req.context.is_admin: if req.context.tenant_id != tenant_id and not req.context.is_admin:
raise exc.HTTPForbidden() raise exc.HTTPForbidden()
allowed = req.context.policy.enforce(context=req.context, allowed = req.context.policy.enforce(
action=handler.__name__, context=req.context,
scope=controller.REQUEST_SCOPE) action=handler.__name__,
scope=controller.REQUEST_SCOPE,
is_registered_policy=is_registered_policy)
if not allowed: if not allowed:
raise exc.HTTPForbidden() raise exc.HTTPForbidden()
return handler(controller, req, **kwargs) return handler(controller, req, **kwargs)
@ -45,7 +62,21 @@ def identified_stack(handler):
This is a handler method decorator. 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) @six.wraps(handler)
def handle_stack_method(controller, req, stack_name, stack_id, **kwargs): def handle_stack_method(controller, req, stack_name, stack_id, **kwargs):
stack_identity = identifier.HeatIdentifier(req.context.tenant_id, stack_identity = identifier.HeatIdentifier(req.context.tenant_id,
@ -53,7 +84,8 @@ def identified_stack(handler):
stack_id) stack_id)
return handler(controller, req, dict(stack_identity), **kwargs) 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): def make_url(req, identity):

View File

@ -20,9 +20,12 @@
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_policy import policy from oslo_policy import policy
from oslo_utils import excutils
import six import six
from heat.common import exception from heat.common import exception
from heat.common.i18n import _
from heat import policies
CONF = cfg.CONF CONF = cfg.CONF
@ -45,6 +48,9 @@ class Enforcer(object):
self.enforcer = policy.Enforcer( self.enforcer = policy.Enforcer(
CONF, default_rule=default_rule, policy_file=policy_file) 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): def set_rules(self, rules, overwrite=True):
"""Create a new Rules object based on the provided dict of rules.""" """Create a new Rules object based on the provided dict of rules."""
rules_obj = policy.Rules(rules, self.default_rule) 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.""" """Set the rules found in the json file on disk."""
self.enforcer.load_rules(force_reload) 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. """Verifies that the action is valid on the target in this context.
:param context: Heat request context :param context: Heat request context
@ -65,10 +72,20 @@ class Enforcer(object):
""" """
do_raise = False if not exc else True do_raise = False if not exc else True
credentials = context.to_policy_values() credentials = context.to_policy_values()
return self.enforcer.enforce(rule, target, credentials, if is_registered_policy:
do_raise, exc=exc, *args, **kwargs) 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. """Verifies that the action is valid on the target in this context.
:param context: Heat request context :param context: Heat request context
@ -79,10 +96,11 @@ class Enforcer(object):
""" """
_action = '%s:%s' % (scope or self.scope, action) _action = '%s:%s' % (scope or self.scope, action)
_target = target or {} _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): 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 By default the rule will check whether or not roles contains
'admin' role and is admin project. '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) self.controller.index(req, tenant_id=self.tenant)
mock_enforce.assert_called_with(action='global_index', mock_enforce.assert_called_with(action='global_index',
scope=self.controller.REQUEST_SCOPE, scope=self.controller.REQUEST_SCOPE,
is_registered_policy=False,
context=self.context) context=self.context)
def test_global_index_uses_admin_context(self, mock_enforce): 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from oslo_messaging._drivers import common as rpc_common from oslo_messaging._drivers import common as rpc_common
@ -117,7 +118,9 @@ class ControllerTest(object):
self.mock_enforce.assert_called_with( self.mock_enforce.assert_called_with(
action=self.action, action=self.action,
context=self.context, context=self.context,
scope=self.controller.REQUEST_SCOPE) scope=self.controller.REQUEST_SCOPE,
is_registered_policy=mock.ANY
)
self.assertEqual(self.expected_request_count, self.assertEqual(self.expected_request_count,
len(self.mock_enforce.call_args_list)) len(self.mock_enforce.call_args_list))
super(ControllerTest, self).tearDown() super(ControllerTest, self).tearDown()

View File

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

View File

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