[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:
parent
121627bce7
commit
b171490450
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||||
|
4
etc/heat/heat-policy-generator.conf
Normal file
4
etc/heat/heat-policy-generator.conf
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
format = json
|
||||||
|
namespace = heat
|
||||||
|
output_file = etc/heat/policy.json.sample
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
context=req.context,
|
||||||
action=handler.__name__,
|
action=handler.__name__,
|
||||||
scope=controller.REQUEST_SCOPE)
|
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):
|
||||||
|
@ -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()
|
||||||
|
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,
|
return self.enforcer.enforce(rule, target, credentials,
|
||||||
do_raise, exc=exc, *args, **kwargs)
|
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
22
heat/policies/__init__.py
Normal 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
48
heat/policies/base.py
Normal 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
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
4
tox.ini
4
tox.ini
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user