Split 'action' policy into more granular controls

Allow operators to specify different policies for each action, since
each action is quite different in character.

The previous "actions:action" rule remains and is the default for each
of the new rules, so there is no effect on existing policies and no
action required by the operator unless they want to take advantage of
the additional flexibility.

Change-Id: Ic4985e8637bc4f34ea2514075b30d2ec32f3441c
Task: 37296
This commit is contained in:
Zane Bitter 2019-10-25 00:13:13 -04:00
parent 0f7ea6a0e6
commit 6f8837d84e
4 changed files with 77 additions and 38 deletions

View File

@ -42,8 +42,9 @@ class ActionController(object):
self.options = options
self.rpc_client = rpc_client.EngineClient()
@util.registered_identified_stack
def action(self, req, identity, body=None):
# Don't enforce policy on this API, as potentially differing policies
# will be enforced on individual actions.
def action(self, req, tenant_id, stack_name, stack_id, body=None):
"""Performs a specified action on a stack.
The body is expecting to contain exactly one item whose key specifies
@ -60,21 +61,36 @@ class ActionController(object):
if ac not in self.ACTIONS:
raise exc.HTTPBadRequest(_("Invalid action %s specified") % ac)
if ac == self.SUSPEND:
self.rpc_client.stack_suspend(req.context, identity)
elif ac == self.RESUME:
self.rpc_client.stack_resume(req.context, identity)
elif ac == self.CHECK:
self.rpc_client.stack_check(req.context, identity)
elif ac == self.CANCEL_UPDATE:
self.rpc_client.stack_cancel_update(req.context, identity,
cancel_with_rollback=True)
elif ac == self.CANCEL_WITHOUT_ROLLBACK:
self.rpc_client.stack_cancel_update(req.context, identity,
cancel_with_rollback=False)
else:
do_action = getattr(self, ac, None)
if do_action is None:
raise exc.HTTPInternalServerError(_("Unexpected action %s") % ac)
do_action(req, tenant_id=tenant_id,
stack_name=stack_name, stack_id=stack_id,
body=body)
@util.registered_identified_stack
def suspend(self, req, identity, body=None):
self.rpc_client.stack_suspend(req.context, identity)
@util.registered_identified_stack
def resume(self, req, identity, body=None):
self.rpc_client.stack_resume(req.context, identity)
@util.registered_identified_stack
def check(self, req, identity, body=None):
self.rpc_client.stack_check(req.context, identity)
@util.registered_identified_stack
def cancel_update(self, req, identity, body=None):
self.rpc_client.stack_cancel_update(req.context, identity,
cancel_with_rollback=True)
@util.registered_identified_stack
def cancel_without_rollback(self, req, identity, body=None):
self.rpc_client.stack_cancel_update(req.context, identity,
cancel_with_rollback=False)
def create_resource(options):
"""Actions action factory method."""

View File

@ -16,20 +16,39 @@ from heat.policies import base
POLICY_ROOT = 'actions:%s'
def _action_rule(action_name, description):
return policy.DocumentedRuleDefault(
name=POLICY_ROOT % action_name,
check_str='rule:%s' % (POLICY_ROOT % 'action'),
description=description,
operations=[{
'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/actions',
'method': 'POST',
}]
)
actions_policies = [
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'action',
check_str=base.RULE_DENY_STACK_USER,
description='Performs non-lifecycle operations on the stack '
'(Snapshot, Resume, Cancel update, or check stack resources).',
operations=[
{
'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/'
'actions',
'method': 'POST'
}
]
)
'(Snapshot, Resume, Cancel update, or check stack resources). '
'This is the default for all actions but can be overridden by more '
'specific policies for individual actions.',
operations=[{
'path': '/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/actions',
'method': 'POST',
}],
),
_action_rule('snapshot', 'Create stack snapshot.'),
_action_rule('suspend', 'Suspend a stack.'),
_action_rule('resume', 'Resume a suspended stack.'),
_action_rule('check', 'Check stack resources.'),
_action_rule('cancel_update', 'Cancel stack operation and roll back.'),
_action_rule('cancel_without_rollback',
'Cancel stack operation without rolling back.'),
]

View File

@ -45,7 +45,7 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
self.controller = actions.ActionController(options=cfgopts)
def test_action_suspend(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
self._mock_enforce_setup(mock_enforce, 'suspend', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'suspend': None}
@ -67,7 +67,7 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
)
def test_action_resume(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
self._mock_enforce_setup(mock_enforce, 'resume', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'resume': None}
@ -89,7 +89,7 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
)
def test_action_check(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
self._mock_enforce_setup(mock_enforce, 'check', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'check': None}
@ -111,13 +111,11 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
)
def _test_action_cancel_update(self, mock_enforce, with_rollback=True):
self._mock_enforce_setup(mock_enforce, 'action', True)
act = 'cancel_update' if with_rollback else 'cancel_without_rollback'
self._mock_enforce_setup(mock_enforce, act, True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
if with_rollback:
body = {'cancel_update': None}
else:
body = {'cancel_without_rollback': None}
body = {act: None}
req = self._post(stack_identity._tenant_path() + '/actions',
data=json.dumps(body))
@ -141,7 +139,6 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
self._test_action_cancel_update(mock_enforce, False)
def test_action_badaction(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'notallowed': None}
@ -155,7 +152,6 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
body=body)
def test_action_badaction_empty(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {}
@ -169,7 +165,6 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
body=body)
def test_action_badaction_multiple(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'one': None, 'two': None}
@ -183,7 +178,7 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
body=body)
def test_action_rmt_aterr(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
self._mock_enforce_setup(mock_enforce, 'suspend', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'suspend': None}
@ -211,7 +206,7 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
)
def test_action_err_denied_policy(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', False)
self._mock_enforce_setup(mock_enforce, 'suspend', False)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'suspend': None}
@ -229,7 +224,6 @@ class ActionControllerTest(tools.ControllerTest, common.HeatTestCase):
self.assertIn('403 Forbidden', six.text_type(resp))
def test_action_badaction_ise(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'action', True)
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
body = {'oops': None}

View File

@ -0,0 +1,10 @@
---
features:
- |
Operators can now apply different authorization policies to each action
supported by the action API (``actions:suspend`` for suspend,
``actions:resume`` for resume, ``actions:check`` for check,
``actions:cancel_update`` for cancel operation and roll back, and
``actions:cancel_without_rollback`` for cancel operation without rolling
back). The default for each is to use the existing ``actions:action`` rule
that was previously the only way to specify policy for actions.