diff --git a/.gitignore b/.gitignore index 073858cded..c307e065e3 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/etc/heat/heat-policy-generator.conf b/etc/heat/heat-policy-generator.conf new file mode 100644 index 0000000000..6d11632acf --- /dev/null +++ b/etc/heat/heat-policy-generator.conf @@ -0,0 +1,4 @@ +[DEFAULT] +format = json +namespace = heat +output_file = etc/heat/policy.json.sample diff --git a/etc/heat/policy.json b/etc/heat/policy.json index f805f2c545..c3a3bd58f6 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -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", diff --git a/heat/api/openstack/v1/util.py b/heat/api/openstack/v1/util.py index 6782a32190..3bf6dab13e 100644 --- a/heat/api/openstack/v1/util.py +++ b/heat/api/openstack/v1/util.py @@ -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): diff --git a/heat/common/policy.py b/heat/common/policy.py index 605a19ebf4..767d1b5402 100644 --- a/heat/common/policy.py +++ b/heat/common/policy.py @@ -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. diff --git a/heat/policies/__init__.py b/heat/policies/__init__.py new file mode 100644 index 0000000000..b35b935a31 --- /dev/null +++ b/heat/policies/__init__.py @@ -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(), + ) diff --git a/heat/policies/base.py b/heat/policies/base.py new file mode 100644 index 0000000000..7f4d8643d9 --- /dev/null +++ b/heat/policies/base.py @@ -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 diff --git a/heat/tests/api/openstack_v1/test_stacks.py b/heat/tests/api/openstack_v1/test_stacks.py index 7e7257b726..f7a9b7686a 100644 --- a/heat/tests/api/openstack_v1/test_stacks.py +++ b/heat/tests/api/openstack_v1/test_stacks.py @@ -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): diff --git a/heat/tests/api/openstack_v1/tools.py b/heat/tests/api/openstack_v1/tools.py index 5212cc6599..3a27b8697e 100644 --- a/heat/tests/api/openstack_v1/tools.py +++ b/heat/tests/api/openstack_v1/tools.py @@ -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() diff --git a/setup.cfg b/setup.cfg index 53c3253617..bc656ac692 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tox.ini b/tox.ini index f9af57890e..64f9b74989 100644 --- a/tox.ini +++ b/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: