[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
|
||||
heat_integrationtests/requirements.txt
|
||||
|
||||
# generated policy file
|
||||
etc/heat/policy.json.sample
|
||||
|
||||
# Files created by 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:CreateStack": "rule:deny_stack_user",
|
||||
"cloudformation:DescribeStacks": "rule:deny_stack_user",
|
||||
|
@ -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):
|
||||
|
@ -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
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)
|
||||
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):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
4
tox.ini
4
tox.ini
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user