From 575a45b1c0debd1cdad186fa1675ac88f0f16541 Mon Sep 17 00:00:00 2001 From: ricolin Date: Fri, 6 Oct 2017 13:05:11 +0800 Subject: [PATCH] [policy in code] part 2 (stacks) Allow use policy in code to stacks's rule. Also convert check_is_admin to use new mechanism. Partially-Implements: bp policy-in-code Change-Id: I398ed162790294d0d4453f7f12c77b38e95a5580 --- etc/heat/policy.json | 30 -- heat/api/openstack/v1/stacks.py | 63 ++-- heat/common/policy.py | 3 +- heat/policies/__init__.py | 2 + heat/policies/stacks.py | 370 +++++++++++++++++++++ heat/tests/api/openstack_v1/test_stacks.py | 2 +- heat/tests/test_common_policy.py | 8 +- 7 files changed, 408 insertions(+), 70 deletions(-) create mode 100644 heat/policies/stacks.py diff --git a/etc/heat/policy.json b/etc/heat/policy.json index c3a3bd58f6..3c85e1df28 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -34,36 +34,6 @@ "resource:signal": "", "resource:mark_unhealthy": "rule:deny_stack_user", "resource:show": "rule:deny_stack_user", - "stacks:abandon": "rule:deny_stack_user", - "stacks:create": "rule:deny_stack_user", - "stacks:delete": "rule:deny_stack_user", - "stacks:detail": "rule:deny_stack_user", - "stacks:export": "rule:deny_stack_user", - "stacks:generate_template": "rule:deny_stack_user", - "stacks:global_index": "rule:deny_everybody", - "stacks:index": "rule:deny_stack_user", - "stacks:list_resource_types": "rule:deny_stack_user", - "stacks:list_template_versions": "rule:deny_stack_user", - "stacks:list_template_functions": "rule:deny_stack_user", - "stacks:lookup": "", - "stacks:preview": "rule:deny_stack_user", - "stacks:resource_schema": "rule:deny_stack_user", - "stacks:show": "rule:deny_stack_user", - "stacks:template": "rule:deny_stack_user", - "stacks:environment": "rule:deny_stack_user", - "stacks:files": "rule:deny_stack_user", - "stacks:update": "rule:deny_stack_user", - "stacks:update_patch": "rule:deny_stack_user", - "stacks:preview_update": "rule:deny_stack_user", - "stacks:preview_update_patch": "rule:deny_stack_user", - "stacks:validate_template": "rule:deny_stack_user", - "stacks:snapshot": "rule:deny_stack_user", - "stacks:show_snapshot": "rule:deny_stack_user", - "stacks:delete_snapshot": "rule:deny_stack_user", - "stacks:list_snapshots": "rule:deny_stack_user", - "stacks:restore_snapshot": "rule:deny_stack_user", - "stacks:list_outputs": "rule:deny_stack_user", - "stacks:show_output": "rule:deny_stack_user", "software_configs:global_index": "rule:deny_everybody", "software_configs:index": "rule:deny_stack_user", diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 11520ac09c..cbbbded381 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -168,7 +168,8 @@ class StackController(object): Implements the API actions. """ - # Define request scope (must match what is in policy.json) + # Define request scope (must match what is in policy.json or policies in + # code) REQUEST_SCOPE = 'stacks' def __init__(self, options): @@ -329,11 +330,11 @@ class StackController(object): count=count, include_project=cnxt.is_admin) - @util.policy_enforce + @util.registered_policy_enforce def global_index(self, req): return self._index(req, use_admin_cnxt=True) - @util.policy_enforce + @util.registered_policy_enforce def index(self, req): """Lists summary information for all stacks.""" global_tenant = False @@ -348,14 +349,14 @@ class StackController(object): return self._index(req) - @util.policy_enforce + @util.registered_policy_enforce def detail(self, req): """Lists detailed information for all stacks.""" stacks = self.rpc_client.list_stacks(req.context) return {'stacks': [stacks_view.format_stack(req, s) for s in stacks]} - @util.policy_enforce + @util.registered_policy_enforce def preview(self, req, body): """Preview the outcome of a template and its params.""" @@ -389,7 +390,7 @@ class StackController(object): raise exc.HTTPBadRequest(six.text_type(msg)) return args - @util.policy_enforce + @util.registered_policy_enforce def create(self, req, body): """Create a new stack.""" data = InstantiationData(body) @@ -410,7 +411,7 @@ class StackController(object): ) return {'stack': formatted_stack} - @util.policy_enforce + @util.registered_policy_enforce def lookup(self, req, stack_name, path='', body=None): """Redirect to the canonical URL for a stack.""" try: @@ -429,7 +430,7 @@ class StackController(object): raise exc.HTTPFound(location=location) - @util.identified_stack + @util.registered_identified_stack def show(self, req, identity): """Gets detailed information for a stack.""" params = req.params @@ -450,7 +451,7 @@ class StackController(object): return {'stack': stacks_view.format_stack(req, stack)} - @util.identified_stack + @util.registered_identified_stack def template(self, req, identity): """Get the template body for an existing stack.""" @@ -460,19 +461,19 @@ class StackController(object): # TODO(zaneb): always set Content-type to application/json return templ - @util.identified_stack + @util.registered_identified_stack def environment(self, req, identity): """Get the environment for an existing stack.""" env = self.rpc_client.get_environment(req.context, identity) return env - @util.identified_stack + @util.registered_identified_stack def files(self, req, identity): """Get the files for an existing stack.""" return self.rpc_client.get_files(req.context, identity) - @util.identified_stack + @util.registered_identified_stack def update(self, req, identity, body): """Update an existing stack with a new template and/or parameters.""" data = InstantiationData(body) @@ -489,7 +490,7 @@ class StackController(object): raise exc.HTTPAccepted() - @util.identified_stack + @util.registered_identified_stack def update_patch(self, req, identity, body): """Update an existing stack with a new template. @@ -518,7 +519,7 @@ class StackController(object): if p_name in params: return self._extract_bool_param(p_name, params[p_name]) - @util.identified_stack + @util.registered_identified_stack def preview_update(self, req, identity, body): """Preview update for existing stack with a new template/parameters.""" data = InstantiationData(body) @@ -538,7 +539,7 @@ class StackController(object): return {'resource_changes': changes} - @util.identified_stack + @util.registered_identified_stack def preview_update_patch(self, req, identity, body): """Preview PATCH update for existing stack.""" data = InstantiationData(body, patch=True) @@ -558,7 +559,7 @@ class StackController(object): return {'resource_changes': changes} - @util.identified_stack + @util.registered_identified_stack def delete(self, req, identity): """Delete the specified stack.""" @@ -567,7 +568,7 @@ class StackController(object): cast=False) raise exc.HTTPNoContent() - @util.identified_stack + @util.registered_identified_stack def abandon(self, req, identity): """Abandons specified stack. @@ -577,7 +578,7 @@ class StackController(object): return self.rpc_client.abandon_stack(req.context, identity) - @util.identified_stack + @util.registered_identified_stack def export(self, req, identity): """Export specified stack. @@ -585,7 +586,7 @@ class StackController(object): """ return self.rpc_client.export_stack(req.context, identity) - @util.policy_enforce + @util.registered_policy_enforce def validate_template(self, req, body): """Implements the ValidateTemplate API action. @@ -623,7 +624,7 @@ class StackController(object): return result - @util.policy_enforce + @util.registered_policy_enforce def list_resource_types(self, req): """Returns a resource types list which may be used in template.""" support_status = req.params.get('support_status') @@ -646,7 +647,7 @@ class StackController(object): heat_version=version, with_description=with_description)} - @util.policy_enforce + @util.registered_policy_enforce def list_template_versions(self, req): """Returns a list of available template versions.""" return { @@ -654,7 +655,7 @@ class StackController(object): self.rpc_client.list_template_versions(req.context) } - @util.policy_enforce + @util.registered_policy_enforce def list_template_functions(self, req, template_version): """Returns a list of available functions in a given template.""" if req.params.get('with_condition_func') is not None: @@ -671,14 +672,14 @@ class StackController(object): with_condition) } - @util.policy_enforce + @util.registered_policy_enforce def resource_schema(self, req, type_name, with_description=False): """Returns the schema of the given resource type.""" return self.rpc_client.resource_schema( req.context, type_name, self._extract_bool_param('with_description', with_description)) - @util.policy_enforce + @util.registered_policy_enforce def generate_template(self, req, type_name): """Generates a template based on the specified type.""" template_type = 'cfn' @@ -694,42 +695,42 @@ class StackController(object): type_name, template_type) - @util.identified_stack + @util.registered_identified_stack def snapshot(self, req, identity, body): name = body.get('name') return self.rpc_client.stack_snapshot(req.context, identity, name) - @util.identified_stack + @util.registered_identified_stack def show_snapshot(self, req, identity, snapshot_id): snapshot = self.rpc_client.show_snapshot( req.context, identity, snapshot_id) return {'snapshot': snapshot} - @util.identified_stack + @util.registered_identified_stack def delete_snapshot(self, req, identity, snapshot_id): self.rpc_client.delete_snapshot(req.context, identity, snapshot_id) raise exc.HTTPNoContent() - @util.identified_stack + @util.registered_identified_stack def list_snapshots(self, req, identity): return { 'snapshots': self.rpc_client.stack_list_snapshots( req.context, identity) } - @util.identified_stack + @util.registered_identified_stack def restore_snapshot(self, req, identity, snapshot_id): self.rpc_client.stack_restore(req.context, identity, snapshot_id) raise exc.HTTPAccepted() - @util.identified_stack + @util.registered_identified_stack def list_outputs(self, req, identity): return { 'outputs': self.rpc_client.list_outputs( req.context, identity) } - @util.identified_stack + @util.registered_identified_stack def show_output(self, req, identity, output_key): return {'output': self.rpc_client.show_output(req.context, identity, diff --git a/heat/common/policy.py b/heat/common/policy.py index 767d1b5402..b504e0289d 100644 --- a/heat/common/policy.py +++ b/heat/common/policy.py @@ -108,7 +108,8 @@ class Enforcer(object): :param context: Heat request context :returns: A non-False value if the user is admin according to policy """ - return self._check(context, 'context_is_admin', target={}, exc=None) + return self._check(context, 'context_is_admin', target={}, exc=None, + is_registered_policy=True) def get_enforcer(): diff --git a/heat/policies/__init__.py b/heat/policies/__init__.py index b35b935a31..0a2c9c6bf5 100644 --- a/heat/policies/__init__.py +++ b/heat/policies/__init__.py @@ -14,9 +14,11 @@ import itertools from heat.policies import base +from heat.policies import stacks def list_rules(): return itertools.chain( base.list_rules(), + stacks.list_rules(), ) diff --git a/heat/policies/stacks.py b/heat/policies/stacks.py new file mode 100644 index 0000000000..7332a69a34 --- /dev/null +++ b/heat/policies/stacks.py @@ -0,0 +1,370 @@ +# 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 + +from heat.policies import base + +POLICY_ROOT = 'stacks:%s' + +stacks_policies = [ + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'abandon', + check_str=base.RULE_DENY_STACK_USER, + description='Abandon stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'abandon', + 'method': 'DELETE' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'create', + check_str=base.RULE_DENY_STACK_USER, + description='Create stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'delete', + check_str=base.RULE_DENY_STACK_USER, + description='Delete stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}', + 'method': 'DELETE' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'detail', + check_str=base.RULE_DENY_STACK_USER, + description='List stacks in detail.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'export', + check_str=base.RULE_DENY_STACK_USER, + description='Export stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'export', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'generate_template', + check_str=base.RULE_DENY_STACK_USER, + description='Generate stack template.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'template', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'global_index', + check_str=base.RULE_DENY_EVERYBODY, + description='List stacks globally.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'index', + check_str=base.RULE_DENY_STACK_USER, + description='List stacks.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'list_resource_types', + check_str=base.RULE_DENY_STACK_USER, + description='List resource types.', + operations=[ + { + 'path': '/v1/{tenant_id}/resource_types', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'list_template_versions', + check_str=base.RULE_DENY_STACK_USER, + description='List template versions.', + operations=[ + { + 'path': '/v1/{tenant_id}/template_versions', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'list_template_functions', + check_str=base.RULE_DENY_STACK_USER, + description='List template functions.', + operations=[ + { + 'path': '/v1/{tenant_id}/template_versions/' + '{template_version}/functions', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'lookup', + check_str=base.RULE_ALLOW_EVERYBODY, + description='Find stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_identity}', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'preview', + check_str=base.RULE_DENY_STACK_USER, + description='Preview stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/preview', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'resource_schema', + check_str=base.RULE_DENY_STACK_USER, + description='Show resource type schema.', + operations=[ + { + 'path': '/v1/{tenant_id}/resource_types/{type_name}', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'show', + check_str=base.RULE_DENY_STACK_USER, + description='Show stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_identity}', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'template', + check_str=base.RULE_DENY_STACK_USER, + description='Get stack template.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'template', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'environment', + check_str=base.RULE_DENY_STACK_USER, + description='Get stack environment.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'environment', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'files', + check_str=base.RULE_DENY_STACK_USER, + description='Get stack files.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'files', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'update', + check_str=base.RULE_DENY_STACK_USER, + description='Update stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}', + 'method': 'PUT' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'update_patch', + check_str=base.RULE_DENY_STACK_USER, + description='Update stack (PATCH).', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}', + 'method': 'PATCH' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'preview_update', + check_str=base.RULE_DENY_STACK_USER, + description='Preview update stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'preview', + 'method': 'PUT' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'preview_update_patch', + check_str=base.RULE_DENY_STACK_USER, + description='Preview update stack (PATCH).', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'preview', + 'method': 'PATCH' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'validate_template', + check_str=base.RULE_DENY_STACK_USER, + description='Validate template.', + operations=[ + { + 'path': '/v1/{tenant_id}/validate', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'snapshot', + check_str=base.RULE_DENY_STACK_USER, + description='Snapshot Stack.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'snapshots', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'show_snapshot', + check_str=base.RULE_DENY_STACK_USER, + description='Show snapshot.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'snapshots/{snapshot_id}', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'delete_snapshot', + check_str=base.RULE_DENY_STACK_USER, + description='Delete snapshot.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'snapshots/{snapshot_id}', + 'method': 'DELETE' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'list_snapshots', + check_str=base.RULE_DENY_STACK_USER, + description='List snapshots.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'snapshots', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'restore_snapshot', + check_str=base.RULE_DENY_STACK_USER, + description='Restore snapshot.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'snapshots/{snapshot_id}/restore', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'list_outputs', + check_str=base.RULE_DENY_STACK_USER, + description='List outputs.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'outputs', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'show_output', + check_str=base.RULE_DENY_STACK_USER, + description='Show outputs.', + operations=[ + { + 'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/' + 'outputs/{output_key}', + 'method': 'GET' + } + ] + ) +] + + +def list_rules(): + return stacks_policies diff --git a/heat/tests/api/openstack_v1/test_stacks.py b/heat/tests/api/openstack_v1/test_stacks.py index f7a9b7686a..8b0593918f 100644 --- a/heat/tests/api/openstack_v1/test_stacks.py +++ b/heat/tests/api/openstack_v1/test_stacks.py @@ -473,7 +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, + is_registered_policy=True, context=self.context) def test_global_index_uses_admin_context(self, mock_enforce): diff --git a/heat/tests/test_common_policy.py b/heat/tests/test_common_policy.py index 672d0b9639..7c256dd15d 100644 --- a/heat/tests/test_common_policy.py +++ b/heat/tests/test_common_policy.py @@ -159,8 +159,7 @@ class TestPolicyEnforcer(common.HeatTestCase): self.assertFalse(enforcer.enforce(ctx, action)) def test_check_admin(self): - enforcer = policy.Enforcer( - policy_file=self.get_policy_file('check_admin.json')) + enforcer = policy.Enforcer() ctx = utils.dummy_context(roles=[]) self.assertFalse(enforcer.check_is_admin(ctx)) @@ -174,11 +173,6 @@ class TestPolicyEnforcer(common.HeatTestCase): def test_enforce_creds(self): enforcer = policy.Enforcer() ctx = utils.dummy_context(roles=['admin']) - self.m.StubOutWithMock(base_policy.Enforcer, 'enforce') - base_policy.Enforcer.enforce('context_is_admin', {}, - ctx.to_policy_values(), - False, exc=None).AndReturn(True) - self.m.ReplayAll() self.assertTrue(enforcer.check_is_admin(ctx)) def test_resource_default_rule(self):