Add policy granularity to the Flavors API

The same policy rule (os_compute_api:os-flavor-manage) is being used
for the create and delete actions of the flavors REST API. It is thus
impossible to provide different RBAC for the create and delete actions
based on roles. To address this, changes are made to have separate
policy rules for each action.

Most other places in nova (and OpenStack in general) have separate
policy rules for each action. This affords the ultimate flexibility
to deployers, who can obviously use the same rule if that is what they
want.

To address backwards compatibility, the new rules added to the
flavor_manage.py policy file, default to the existing rule
(os_compute_api:os-flavor-manage). That way across upgrades this
should ensure if an existing admin has customised the rule, it keeps
working, but folks that know about the new setting can override the
default rule. In addtion, a verify_deprecated_policy method is added
to see if the old policy action is being configured instead of the
new actions.

Closes-Bug: #1675147

Co-Authored-By: Felipe Monteiro <felipe.monteiro@att.com>
Change-Id: Ic67b52ebac3a47e9fb7e3c0d6c3ce8a6bc539e11
This commit is contained in:
Rick Bartra 2017-07-18 17:38:52 -04:00
parent 430ec6504b
commit a8fd8731d2
7 changed files with 295 additions and 8 deletions

View File

@ -12,6 +12,8 @@
import webob import webob
from oslo_log import log as logging
from nova.api.openstack.compute.schemas import flavor_manage from nova.api.openstack.compute.schemas import flavor_manage
from nova.api.openstack.compute.views import flavors as flavors_view from nova.api.openstack.compute.views import flavors as flavors_view
from nova.api.openstack import extensions from nova.api.openstack import extensions
@ -21,8 +23,12 @@ from nova.compute import flavors
from nova import exception from nova import exception
from nova.i18n import _ from nova.i18n import _
from nova import objects from nova import objects
from nova.policies import base
from nova.policies import flavor_manage as fm_policies from nova.policies import flavor_manage as fm_policies
from nova import policy
LOG = logging.getLogger(__name__)
ALIAS = "os-flavor-manage" ALIAS = "os-flavor-manage"
@ -41,7 +47,15 @@ class FlavorManageController(wsgi.Controller):
@wsgi.action("delete") @wsgi.action("delete")
def _delete(self, req, id): def _delete(self, req, id):
context = req.environ['nova.context'] context = req.environ['nova.context']
context.can(fm_policies.BASE_POLICY_NAME) # TODO(rb560u): remove this check in future release
using_old_action = \
policy.verify_deprecated_policy(fm_policies.BASE_POLICY_NAME,
fm_policies.POLICY_ROOT % 'delete',
base.RULE_ADMIN_API,
context)
if not using_old_action:
context.can(fm_policies.POLICY_ROOT % 'delete')
flavor = objects.Flavor(context=context, flavorid=id) flavor = objects.Flavor(context=context, flavorid=id)
try: try:
@ -57,7 +71,15 @@ class FlavorManageController(wsgi.Controller):
@validation.schema(flavor_manage.create, '2.1') @validation.schema(flavor_manage.create, '2.1')
def _create(self, req, body): def _create(self, req, body):
context = req.environ['nova.context'] context = req.environ['nova.context']
context.can(fm_policies.BASE_POLICY_NAME) # TODO(rb560u): remove this check in future release
using_old_action = \
policy.verify_deprecated_policy(fm_policies.BASE_POLICY_NAME,
fm_policies.POLICY_ROOT % 'create',
base.RULE_ADMIN_API,
context)
if not using_old_action:
context.can(fm_policies.POLICY_ROOT % 'create')
vals = body['flavor'] vals = body['flavor']

View File

@ -20,13 +20,17 @@ from nova.policies import base
BASE_POLICY_NAME = 'os_compute_api:os-flavor-manage' BASE_POLICY_NAME = 'os_compute_api:os-flavor-manage'
POLICY_ROOT = 'os_compute_api:os-flavor-manage:%s'
BASE_POLICY_RULE = 'rule:%s' % BASE_POLICY_NAME
flavor_manage_policies = [ flavor_manage_policies = [
# TODO(rb560u): remove this rule in future release
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
BASE_POLICY_NAME, BASE_POLICY_NAME,
base.RULE_ADMIN_API, base.RULE_ADMIN_API,
"Create and delete Flavors", "Create and delete Flavors. Deprecated in Pike and will be "
"removed in future release",
[ [
{ {
'method': 'POST', 'method': 'POST',
@ -38,6 +42,26 @@ flavor_manage_policies = [
}, },
]), ]),
policy.DocumentedRuleDefault(
POLICY_ROOT % 'create',
BASE_POLICY_RULE,
"Create a flavor",
[
{
'method': 'POST',
'path': '/flavors'
}
]),
policy.DocumentedRuleDefault(
POLICY_ROOT % 'delete',
BASE_POLICY_RULE,
"Delete a flavor",
[
{
'method': 'DELETE',
'path': '/flavors/{flavor_id}'
}
]),
] ]

View File

@ -225,3 +225,33 @@ def get_enforcer():
cfg.CONF(conf_args, project='nova') cfg.CONF(conf_args, project='nova')
init() init()
return _ENFORCER return _ENFORCER
def verify_deprecated_policy(old_policy, new_policy, default_rule, context):
"""Check the rule of the deprecated policy action
If the current rule of the deprecated policy action is set to a non-default
value, then a warning message is logged stating that the new policy
action should be used to dictate permissions as the old policy action is
being deprecated.
:param old_policy: policy action that is being deprecated
:param new_policy: policy action that is replacing old_policy
:param default_rule: the old_policy action default rule value
:param context: the nova context
"""
if _ENFORCER:
current_rule = str(_ENFORCER.rules[old_policy])
else:
current_rule = None
if current_rule != default_rule:
LOG.warning("Start using the new action '{0}'. The existing "
"action '{1}' is being deprecated and will be "
"removed in future release.".format(new_policy,
old_policy))
context.can(old_policy)
return True
else:
return False

View File

@ -23,6 +23,7 @@ from nova.api.openstack.compute import flavor_manage as flavormanage_v21
from nova.compute import flavors from nova.compute import flavors
from nova import db from nova import db
from nova import exception from nova import exception
from nova import policy
from nova import test from nova import test
from nova.tests.unit.api.openstack import fakes from nova.tests.unit.api.openstack import fakes
@ -373,19 +374,20 @@ class PrivateFlavorManageTestV21(test.TestCase):
self.assertEqual(body["flavor"][key], self.expected["flavor"][key]) self.assertEqual(body["flavor"][key], self.expected["flavor"][key])
class FlavorManagerPolicyEnforcementV21(test.NoDBTestCase): class FlavorManagerPolicyEnforcementV21(test.TestCase):
def setUp(self): def setUp(self):
super(FlavorManagerPolicyEnforcementV21, self).setUp() super(FlavorManagerPolicyEnforcementV21, self).setUp()
self.controller = flavormanage_v21.FlavorManageController() self.controller = flavormanage_v21.FlavorManageController()
self.adm_req = fakes.HTTPRequest.blank('', use_admin_context=True)
self.req = fakes.HTTPRequest.blank('')
def test_create_policy_failed(self): def test_create_policy_failed(self):
rule_name = "os_compute_api:os-flavor-manage" rule_name = "os_compute_api:os-flavor-manage"
self.policy.set_rules({rule_name: "project:non_fake"}) self.policy.set_rules({rule_name: "project:non_fake"})
req = fakes.HTTPRequest.blank('')
exc = self.assertRaises( exc = self.assertRaises(
exception.PolicyNotAuthorized, exception.PolicyNotAuthorized,
self.controller._create, req, self.controller._create, self.req,
body={"flavor": { body={"flavor": {
"name": "test", "name": "test",
"ram": 512, "ram": 512,
@ -394,6 +396,8 @@ class FlavorManagerPolicyEnforcementV21(test.NoDBTestCase):
"swap": 512, "swap": 512,
"rxtx_factor": 1, "rxtx_factor": 1,
}}) }})
# The deprecated action is being enforced since the rule that is
# configured is different than the default rule
self.assertEqual( self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name, "Policy doesn't allow %s to be performed." % rule_name,
exc.format_message()) exc.format_message())
@ -401,11 +405,172 @@ class FlavorManagerPolicyEnforcementV21(test.NoDBTestCase):
def test_delete_policy_failed(self): def test_delete_policy_failed(self):
rule_name = "os_compute_api:os-flavor-manage" rule_name = "os_compute_api:os-flavor-manage"
self.policy.set_rules({rule_name: "project:non_fake"}) self.policy.set_rules({rule_name: "project:non_fake"})
req = fakes.HTTPRequest.blank('')
exc = self.assertRaises( exc = self.assertRaises(
exception.PolicyNotAuthorized, exception.PolicyNotAuthorized,
self.controller._delete, req, self.controller._delete, self.req,
fakes.FAKE_UUID) fakes.FAKE_UUID)
# The deprecated action is being enforced since the rule that is
# configured is different than the default rule
self.assertEqual( self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name, "Policy doesn't allow %s to be performed." % rule_name,
exc.format_message()) exc.format_message())
@mock.patch.object(policy.LOG, 'warning')
def test_create_policy_rbac_inherit_default(self, mock_warning):
"""Test to verify inherited rule is working. The rule of the
deprecated action is not set to the default, so the deprecated
action is being enforced
"""
default_flavor_policy = "os_compute_api:os-flavor-manage"
create_flavor_policy = "os_compute_api:os-flavor-manage:create"
rules = {default_flavor_policy: 'is_admin:True',
create_flavor_policy: 'rule:%s' % default_flavor_policy}
self.policy.set_rules(rules)
body = {
"flavor": {
"name": "azAZ09. -_",
"ram": 512,
"vcpus": 2,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 1,
"id": six.text_type('1234'),
"swap": 512,
"rxtx_factor": 1,
"os-flavor-access:is_public": True,
}
}
# check for success as admin
self.controller._create(self.adm_req, body=body)
# check for failure as non-admin
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller._create, self.req,
body=body)
# The deprecated action is being enforced since the rule that is
# configured is different than the default rule
self.assertEqual(
"Policy doesn't allow %s to be performed." % default_flavor_policy,
exc.format_message())
mock_warning.assert_called_with("Start using the new "
"action '{0}'. The existing action '{1}' is being deprecated and "
"will be removed in future release.".format(create_flavor_policy,
default_flavor_policy))
@mock.patch.object(policy.LOG, 'warning')
def test_delete_policy_rbac_inherit_default(self, mock_warning):
"""Test to verify inherited rule is working. The rule of the
deprecated action is not set to the default, so the deprecated
action is being enforced
"""
default_flavor_policy = "os_compute_api:os-flavor-manage"
create_flavor_policy = "os_compute_api:os-flavor-manage:create"
delete_flavor_policy = "os_compute_api:os-flavor-manage:delete"
rules = {default_flavor_policy: 'is_admin:True',
create_flavor_policy: 'rule:%s' % default_flavor_policy,
delete_flavor_policy: 'rule:%s' % default_flavor_policy}
self.policy.set_rules(rules)
body = {
"flavor": {
"name": "azAZ09. -_",
"ram": 512,
"vcpus": 2,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 1,
"id": six.text_type('1234'),
"swap": 512,
"rxtx_factor": 1,
"os-flavor-access:is_public": True,
}
}
self.flavor = self.controller._create(self.adm_req, body=body)
mock_warning.assert_called_once_with("Start using the new "
"action '{0}'. The existing action '{1}' is being deprecated and "
"will be removed in future release.".format(create_flavor_policy,
default_flavor_policy))
# check for success as admin
flavor = self.flavor
self.controller._delete(self.adm_req, flavor['flavor']['id'])
# check for failure as non-admin
flavor = self.flavor
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller._delete, self.req,
flavor['flavor']['id'])
# The deprecated action is being enforced since the rule that is
# configured is different than the default rule
self.assertEqual(
"Policy doesn't allow %s to be performed." % default_flavor_policy,
exc.format_message())
mock_warning.assert_called_with("Start using the new "
"action '{0}'. The existing action '{1}' is being deprecated and "
"will be removed in future release.".format(delete_flavor_policy,
default_flavor_policy))
def test_create_policy_rbac_no_change_to_default_action_rule(self):
"""Test to verify the correct action is being enforced. When the
rule configured for the deprecated action is the same as the
default, the new action should be enforced.
"""
default_flavor_policy = "os_compute_api:os-flavor-manage"
create_flavor_policy = "os_compute_api:os-flavor-manage:create"
# The default rule of the deprecated action is admin_api
rules = {default_flavor_policy: 'rule:admin_api',
create_flavor_policy: 'rule:%s' % default_flavor_policy}
self.policy.set_rules(rules)
body = {
"flavor": {
"name": "azAZ09. -_",
"ram": 512,
"vcpus": 2,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 1,
"id": six.text_type('1234'),
"swap": 512,
"rxtx_factor": 1,
"os-flavor-access:is_public": True,
}
}
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller._create, self.req,
body=body)
self.assertEqual(
"Policy doesn't allow %s to be performed." % create_flavor_policy,
exc.format_message())
def test_delete_policy_rbac_change_to_default_action_rule(self):
"""Test to verify the correct action is being enforced. When the
rule configured for the deprecated action is the same as the
default, the new action should be enforced.
"""
default_flavor_policy = "os_compute_api:os-flavor-manage"
create_flavor_policy = "os_compute_api:os-flavor-manage:create"
delete_flavor_policy = "os_compute_api:os-flavor-manage:delete"
# The default rule of the deprecated action is admin_api
# Set the rule of the create flavor action to is_admin:True so that
# admin context can be used to create a flavor
rules = {default_flavor_policy: 'rule:admin_api',
create_flavor_policy: 'is_admin:True',
delete_flavor_policy: 'rule:%s' % default_flavor_policy}
self.policy.set_rules(rules)
body = {
"flavor": {
"name": "azAZ09. -_",
"ram": 512,
"vcpus": 2,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 1,
"id": six.text_type('1234'),
"swap": 512,
"rxtx_factor": 1,
"os-flavor-access:is_public": True,
}
}
flavor = self.controller._create(self.adm_req, body=body)
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller._delete, self.req,
flavor['flavor']['id'])
self.assertEqual(
"Policy doesn't allow %s to be performed." % delete_flavor_policy,
exc.format_message())

View File

@ -51,6 +51,8 @@ policy_data = """
"os_compute_api:os-flavor-extra-specs:index": "", "os_compute_api:os-flavor-extra-specs:index": "",
"os_compute_api:os-flavor-extra-specs:show": "", "os_compute_api:os-flavor-extra-specs:show": "",
"os_compute_api:os-flavor-manage": "", "os_compute_api:os-flavor-manage": "",
"os_compute_api:os-flavor-manage:create": "",
"os_compute_api:os-flavor-manage:delete": "",
"os_compute_api:os-floating-ip-dns": "", "os_compute_api:os-floating-ip-dns": "",
"os_compute_api:os-floating-ip-dns:domain:update": "", "os_compute_api:os-floating-ip-dns:domain:update": "",
"os_compute_api:os-floating-ip-dns:domain:delete": "", "os_compute_api:os-floating-ip-dns:domain:delete": "",

View File

@ -72,6 +72,9 @@ class PolicyTestCase(test.NoDBTestCase):
oslo_policy.RuleDefault("true", '@'), oslo_policy.RuleDefault("true", '@'),
oslo_policy.RuleDefault("example:allowed", '@'), oslo_policy.RuleDefault("example:allowed", '@'),
oslo_policy.RuleDefault("example:denied", "!"), oslo_policy.RuleDefault("example:denied", "!"),
oslo_policy.RuleDefault("old_action_not_default", "@"),
oslo_policy.RuleDefault("new_action", "@"),
oslo_policy.RuleDefault("old_action_default", "rule:admin_api"),
oslo_policy.RuleDefault("example:get_http", oslo_policy.RuleDefault("example:get_http",
"http://www.example.com"), "http://www.example.com"),
oslo_policy.RuleDefault("example:my_file", oslo_policy.RuleDefault("example:my_file",
@ -181,6 +184,32 @@ class PolicyTestCase(test.NoDBTestCase):
"project_id:%(project_id)s")]) "project_id:%(project_id)s")])
mock_warning.assert_not_called() mock_warning.assert_not_called()
@mock.patch.object(policy.LOG, 'warning')
def test_verify_deprecated_policy_using_old_action(self, mock_warning):
old_policy = "old_action_not_default"
new_policy = "new_action"
default_rule = "rule:admin_api"
using_old_action = policy.verify_deprecated_policy(
old_policy, new_policy, default_rule, self.context)
mock_warning.assert_called_once_with("Start using the new "
"action '{0}'. The existing action '{1}' is being deprecated and "
"will be removed in future release.".format(new_policy,
old_policy))
self.assertTrue(using_old_action)
def test_verify_deprecated_policy_using_new_action(self):
old_policy = "old_action_default"
new_policy = "new_action"
default_rule = "rule:admin_api"
using_old_action = policy.verify_deprecated_policy(
old_policy, new_policy, default_rule, self.context)
self.assertFalse(using_old_action)
class IsAdminCheckTestCase(test.NoDBTestCase): class IsAdminCheckTestCase(test.NoDBTestCase):
def setUp(self): def setUp(self):
@ -283,6 +312,8 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
"os_compute_api:os-flavor-extra-specs:update", "os_compute_api:os-flavor-extra-specs:update",
"os_compute_api:os-flavor-extra-specs:delete", "os_compute_api:os-flavor-extra-specs:delete",
"os_compute_api:os-flavor-manage", "os_compute_api:os-flavor-manage",
"os_compute_api:os-flavor-manage:create",
"os_compute_api:os-flavor-manage:delete",
"os_compute_api:os-floating-ips-bulk", "os_compute_api:os-floating-ips-bulk",
"os_compute_api:os-floating-ip-dns:domain:delete", "os_compute_api:os-floating-ip-dns:domain:delete",
"os_compute_api:os-floating-ip-dns:domain:update", "os_compute_api:os-floating-ip-dns:domain:update",

View File

@ -0,0 +1,13 @@
---
features:
- |
Add granularity to the ``os_compute_api:os-flavor-manage`` policy
with the addition of distinct actions for create and delete:
- ``os_compute_api:os-flavor-manage:create``
- ``os_compute_api:os-flavor-manage:delete``
To address backwards compatibility, the new rules added to the
flavor_manage.py policy file, default to the existing rule,
``os_compute_api:os-flavor-manage``, if it is set to a non-default
value.