Add ext_parent policy check
Add common parent owner check for the resources which introduced by service plugin. Then port forwarding resource will share the same tenant_id with floatingip. That means only the fip owner can create/update/get/delete the associated port forwarding resource. Partially-Implements: blueprint port-forwarding Partial-Bug: #1491317 Change-Id: I450c674e55ca15e1d9a6a6224138f3305427da68
This commit is contained in:
parent
4088461ed6
commit
35d945e92f
|
@ -180,7 +180,13 @@ overrides the generic check performed by oslo_policy in this case.
|
||||||
It uses for those cases where neutron needs to check whether the project
|
It uses for those cases where neutron needs to check whether the project
|
||||||
submitting a request for a new resource owns the parent resource of the one
|
submitting a request for a new resource owns the parent resource of the one
|
||||||
being created. Current usages of ``OwnerCheck`` include, for instance,
|
being created. Current usages of ``OwnerCheck`` include, for instance,
|
||||||
creating and updating a subnet.
|
creating and updating a subnet. This class supports the extension parent
|
||||||
|
resources owner check which the parent resource introduced by
|
||||||
|
service plugins. Such as router and floatingip owner check for ``router``
|
||||||
|
service plugin. Developers can register the extension resource name and service
|
||||||
|
plugin name which were registered in neutron-lib into
|
||||||
|
``EXT_PARENT_RESOURCE_MAPPING`` which is located in
|
||||||
|
``neutron.common.constants``.
|
||||||
|
|
||||||
The check, performed in the ``__call__`` method, works as follows:
|
The check, performed in the ``__call__`` method, works as follows:
|
||||||
|
|
||||||
|
@ -192,7 +198,9 @@ The check, performed in the ``__call__`` method, works as follows:
|
||||||
* if the previous check failed, extract a parent resource type and a
|
* if the previous check failed, extract a parent resource type and a
|
||||||
parent field name from the target field. For instance
|
parent field name from the target field. For instance
|
||||||
``networks:tenant_id`` identifies the ``tenant_id`` attribute of the
|
``networks:tenant_id`` identifies the ``tenant_id`` attribute of the
|
||||||
``network`` resource;
|
``network`` resource. For extension parent resource case,
|
||||||
|
``ext_parent:tenant_id`` identifies the ``tenant_id`` attribute of the
|
||||||
|
registered extension resource in ``EXT_PARENT_RESOURCE_MAPPING``;
|
||||||
* if no parent resource or target field could be identified raise a
|
* if no parent resource or target field could be identified raise a
|
||||||
``PolicyCheckError`` exception;
|
``PolicyCheckError`` exception;
|
||||||
* Retrieve a 'parent foreign key' from the ``_RESOURCE_FOREIGN_KEYS`` data
|
* Retrieve a 'parent foreign key' from the ``_RESOURCE_FOREIGN_KEYS`` data
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"shared_address_scopes": "field:address_scopes:shared=True",
|
"shared_address_scopes": "field:address_scopes:shared=True",
|
||||||
"external": "field:networks:router:external=True",
|
"external": "field:networks:router:external=True",
|
||||||
"default": "rule:admin_or_owner",
|
"default": "rule:admin_or_owner",
|
||||||
|
"admin_or_ext_parent_owner": "rule:context_is_admin or tenant_id:%(ext_parent:tenant_id)s",
|
||||||
|
|
||||||
"create_subnet": "rule:admin_or_network_owner",
|
"create_subnet": "rule:admin_or_network_owner",
|
||||||
"create_subnet:segment_id": "rule:admin_only",
|
"create_subnet:segment_id": "rule:admin_only",
|
||||||
|
@ -250,9 +251,9 @@
|
||||||
"update_log": "rule:admin_only",
|
"update_log": "rule:admin_only",
|
||||||
"delete_log": "rule:admin_only",
|
"delete_log": "rule:admin_only",
|
||||||
|
|
||||||
"create_floatingip_port_forwarding": "rule:regular_user",
|
"create_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner",
|
||||||
"get_floatingip_port_forwarding": "rule:regular_user",
|
"get_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner",
|
||||||
"get_floatingip_port_forwardings": "rule:regular_user",
|
"get_floatingip_port_forwardings": "rule:admin_or_ext_parent_owner",
|
||||||
"update_floatingip_port_forwarding": "rule:regular_user",
|
"update_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner",
|
||||||
"delete_floatingip_port_forwarding": "rule:regular_user"
|
"delete_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -310,12 +310,15 @@ class Controller(object):
|
||||||
# FIXME(salvatore-orlando): obj_getter might return references to
|
# FIXME(salvatore-orlando): obj_getter might return references to
|
||||||
# other resources. Must check authZ on them too.
|
# other resources. Must check authZ on them too.
|
||||||
# Omit items from list that should not be visible
|
# Omit items from list that should not be visible
|
||||||
obj_list = [obj for obj in obj_list
|
tmp_list = []
|
||||||
if policy.check(request.context,
|
for obj in obj_list:
|
||||||
self._plugin_handlers[self.SHOW],
|
self._set_parent_id_into_ext_resources_request(
|
||||||
obj,
|
request, obj, parent_id, is_get=True)
|
||||||
plugin=self._plugin,
|
if policy.check(
|
||||||
pluralized=self._collection)]
|
request.context, self._plugin_handlers[self.SHOW],
|
||||||
|
obj, plugin=self._plugin, pluralized=self._collection):
|
||||||
|
tmp_list.append(obj)
|
||||||
|
obj_list = tmp_list
|
||||||
# Use the first element in the list for discriminating which attributes
|
# Use the first element in the list for discriminating which attributes
|
||||||
# should be filtered out because of authZ policies
|
# should be filtered out because of authZ policies
|
||||||
# fields_to_add contains a list of attributes added for request policy
|
# fields_to_add contains a list of attributes added for request policy
|
||||||
|
@ -346,6 +349,8 @@ class Controller(object):
|
||||||
kwargs[self._parent_id_name] = parent_id
|
kwargs[self._parent_id_name] = parent_id
|
||||||
obj_getter = getattr(self._plugin, action)
|
obj_getter = getattr(self._plugin, action)
|
||||||
obj = obj_getter(request.context, id, **kwargs)
|
obj = obj_getter(request.context, id, **kwargs)
|
||||||
|
self._set_parent_id_into_ext_resources_request(
|
||||||
|
request, obj, parent_id, is_get=True)
|
||||||
# Check authz
|
# Check authz
|
||||||
# FIXME(salvatore-orlando): obj_getter might return references to
|
# FIXME(salvatore-orlando): obj_getter might return references to
|
||||||
# other resources. Must check authZ on them too.
|
# other resources. Must check authZ on them too.
|
||||||
|
@ -457,6 +462,11 @@ class Controller(object):
|
||||||
for item in items:
|
for item in items:
|
||||||
self._validate_network_tenant_ownership(request,
|
self._validate_network_tenant_ownership(request,
|
||||||
item[self._resource])
|
item[self._resource])
|
||||||
|
# For ext resources policy check, we support two types, such as
|
||||||
|
# parent_id is in request body, another type is parent_id is in
|
||||||
|
# request url, which we can get from kwargs.
|
||||||
|
self._set_parent_id_into_ext_resources_request(
|
||||||
|
request, item[self._resource], parent_id)
|
||||||
policy.enforce(request.context,
|
policy.enforce(request.context,
|
||||||
action,
|
action,
|
||||||
item[self._resource],
|
item[self._resource],
|
||||||
|
@ -632,6 +642,10 @@ class Controller(object):
|
||||||
# Ensure policy engine is initialized
|
# Ensure policy engine is initialized
|
||||||
policy.init()
|
policy.init()
|
||||||
parent_id = kwargs.get(self._parent_id_name)
|
parent_id = kwargs.get(self._parent_id_name)
|
||||||
|
# If the parent_id exist, we should get orig_obj with
|
||||||
|
# self._parent_id_name field.
|
||||||
|
if parent_id and self._parent_id_name not in field_list:
|
||||||
|
field_list.append(self._parent_id_name)
|
||||||
orig_obj = self._item(request, id, field_list=field_list,
|
orig_obj = self._item(request, id, field_list=field_list,
|
||||||
parent_id=parent_id)
|
parent_id=parent_id)
|
||||||
orig_object_copy = copy.copy(orig_obj)
|
orig_object_copy = copy.copy(orig_obj)
|
||||||
|
@ -640,6 +654,10 @@ class Controller(object):
|
||||||
# which attributes are set explicitly so that it can distinguish them
|
# which attributes are set explicitly so that it can distinguish them
|
||||||
# from the ones that are set to their default values.
|
# from the ones that are set to their default values.
|
||||||
orig_obj[n_const.ATTRIBUTES_TO_UPDATE] = body[self._resource].keys()
|
orig_obj[n_const.ATTRIBUTES_TO_UPDATE] = body[self._resource].keys()
|
||||||
|
# Then get the ext_parent_id, format to ext_parent_parent_resource_id
|
||||||
|
if self._parent_id_name in orig_obj:
|
||||||
|
self._set_parent_id_into_ext_resources_request(
|
||||||
|
request, orig_obj, parent_id)
|
||||||
try:
|
try:
|
||||||
policy.enforce(request.context,
|
policy.enforce(request.context,
|
||||||
action,
|
action,
|
||||||
|
@ -759,6 +777,37 @@ class Controller(object):
|
||||||
msg = _('The resource could not be found.')
|
msg = _('The resource could not be found.')
|
||||||
raise webob.exc.HTTPNotFound(msg)
|
raise webob.exc.HTTPNotFound(msg)
|
||||||
|
|
||||||
|
def _set_parent_id_into_ext_resources_request(
|
||||||
|
self, request, resource_item, parent_id, is_get=False):
|
||||||
|
if not parent_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# This will pass most create/update/delete cases
|
||||||
|
if not is_get and (request.context.is_admin or
|
||||||
|
request.context.is_advsvc or
|
||||||
|
self.parent['member_name'] not in
|
||||||
|
n_const.EXT_PARENT_RESOURCE_MAPPING or
|
||||||
|
resource_item.get(self._parent_id_name)):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Then we arrive here, that means the request or get obj contains
|
||||||
|
# ext_parent. If this func is called by list/get, and it contains
|
||||||
|
# _parent_id_name. We need to re-add the ex_parent prefix to policy.
|
||||||
|
if is_get:
|
||||||
|
if (not request.context.is_admin or
|
||||||
|
not request.context.is_advsvc and
|
||||||
|
self.parent['member_name'] in
|
||||||
|
n_const.EXT_PARENT_RESOURCE_MAPPING):
|
||||||
|
resource_item.setdefault(
|
||||||
|
"%s_%s" % (n_const.EXT_PARENT_PREFIX,
|
||||||
|
self._parent_id_name),
|
||||||
|
parent_id)
|
||||||
|
# If this func is called by create/update/delete, we just add.
|
||||||
|
else:
|
||||||
|
resource_item.setdefault(
|
||||||
|
"%s_%s" % (n_const.EXT_PARENT_PREFIX, self._parent_id_name),
|
||||||
|
parent_id)
|
||||||
|
|
||||||
|
|
||||||
def create_resource(collection, resource, plugin, params, allow_bulk=False,
|
def create_resource(collection, resource, plugin, params, allow_bulk=False,
|
||||||
member_actions=None, parent=None, allow_pagination=False,
|
member_actions=None, parent=None, allow_pagination=False,
|
||||||
|
|
|
@ -13,8 +13,9 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from neutron_lib.api.definitions import l3
|
||||||
from neutron_lib import constants as lib_constants
|
from neutron_lib import constants as lib_constants
|
||||||
|
from neutron_lib.plugins import constants as plugin_consts
|
||||||
|
|
||||||
ROUTER_PORT_OWNERS = lib_constants.ROUTER_INTERFACE_OWNERS_SNAT + \
|
ROUTER_PORT_OWNERS = lib_constants.ROUTER_INTERFACE_OWNERS_SNAT + \
|
||||||
(lib_constants.DEVICE_OWNER_ROUTER_GW,)
|
(lib_constants.DEVICE_OWNER_ROUTER_GW,)
|
||||||
|
@ -230,3 +231,16 @@ IEC_BASE = 1024
|
||||||
|
|
||||||
# Port bindings handling
|
# Port bindings handling
|
||||||
NO_ACTIVE_BINDING = 'no_active_binding'
|
NO_ACTIVE_BINDING = 'no_active_binding'
|
||||||
|
|
||||||
|
# Registered extension parent resource check mapping
|
||||||
|
# If we want to register some service plugin resources into policy and check
|
||||||
|
# the owner when operating their subresources. We can write here to use
|
||||||
|
# existing policy engine for parent resource owner check.
|
||||||
|
# Each entry here should be PARENT_RESOURCE_NAME: SERVICE_PLUGIN_NAME,
|
||||||
|
# PARENT_RESOURCE_NAME is usually from api definition.
|
||||||
|
# SERVICE_PLUGIN_NAME is the service plugin which introduced the resource and
|
||||||
|
# registered the service plugin name in neutron-lib.
|
||||||
|
EXT_PARENT_RESOURCE_MAPPING = {
|
||||||
|
l3.FLOATINGIP: plugin_consts.L3
|
||||||
|
}
|
||||||
|
EXT_PARENT_PREFIX = 'ext_parent'
|
||||||
|
|
|
@ -224,7 +224,11 @@ class OwnerCheck(policy.Check):
|
||||||
# resource is handled by the core plugin. It might be worth
|
# resource is handled by the core plugin. It might be worth
|
||||||
# having a way to map resources to plugins so to make this
|
# having a way to map resources to plugins so to make this
|
||||||
# check more general
|
# check more general
|
||||||
f = getattr(directory.get_plugin(), 'get_%s' % resource_type)
|
plugin = directory.get_plugin()
|
||||||
|
if resource_type in const.EXT_PARENT_RESOURCE_MAPPING:
|
||||||
|
plugin = directory.get_plugin(
|
||||||
|
const.EXT_PARENT_RESOURCE_MAPPING[resource_type])
|
||||||
|
f = getattr(plugin, 'get_%s' % resource_type)
|
||||||
# f *must* exist, if not found it is better to let neutron
|
# f *must* exist, if not found it is better to let neutron
|
||||||
# explode. Check will be performed with admin context
|
# explode. Check will be performed with admin context
|
||||||
try:
|
try:
|
||||||
|
@ -272,6 +276,13 @@ class OwnerCheck(policy.Check):
|
||||||
reason=err_reason)
|
reason=err_reason)
|
||||||
parent_foreign_key = _RESOURCE_FOREIGN_KEYS.get(
|
parent_foreign_key = _RESOURCE_FOREIGN_KEYS.get(
|
||||||
"%ss" % parent_res, None)
|
"%ss" % parent_res, None)
|
||||||
|
if parent_res == const.EXT_PARENT_PREFIX:
|
||||||
|
for resource in const.EXT_PARENT_RESOURCE_MAPPING:
|
||||||
|
key = "%s_%s_id" % (const.EXT_PARENT_PREFIX, resource)
|
||||||
|
if key in target:
|
||||||
|
parent_foreign_key = key
|
||||||
|
parent_res = resource
|
||||||
|
break
|
||||||
if not parent_foreign_key:
|
if not parent_foreign_key:
|
||||||
err_reason = (_("Unable to verify match:%(match)s as the "
|
err_reason = (_("Unable to verify match:%(match)s as the "
|
||||||
"parent resource: %(res)s was not found") %
|
"parent resource: %(res)s was not found") %
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"shared_address_scopes": "field:address_scopes:shared=True",
|
"shared_address_scopes": "field:address_scopes:shared=True",
|
||||||
"external": "field:networks:router:external=True",
|
"external": "field:networks:router:external=True",
|
||||||
"default": "rule:admin_or_owner",
|
"default": "rule:admin_or_owner",
|
||||||
|
"admin_or_ext_parent_owner": "rule:context_is_admin or tenant_id:%(ext_parent:tenant_id)s",
|
||||||
|
|
||||||
"create_subnet": "rule:admin_or_network_owner",
|
"create_subnet": "rule:admin_or_network_owner",
|
||||||
"create_subnet:segment_id": "rule:admin_only",
|
"create_subnet:segment_id": "rule:admin_only",
|
||||||
|
@ -250,9 +251,9 @@
|
||||||
"update_log": "rule:admin_only",
|
"update_log": "rule:admin_only",
|
||||||
"delete_log": "rule:admin_only",
|
"delete_log": "rule:admin_only",
|
||||||
|
|
||||||
"create_floatingip_port_forwarding": "rule:regular_user",
|
"create_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner",
|
||||||
"get_floatingip_port_forwarding": "rule:regular_user",
|
"get_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner",
|
||||||
"get_floatingip_port_forwardings": "rule:regular_user",
|
"get_floatingip_port_forwardings": "rule:admin_or_ext_parent_owner",
|
||||||
"update_floatingip_port_forwarding": "rule:regular_user",
|
"update_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner",
|
||||||
"delete_floatingip_port_forwarding": "rule:regular_user"
|
"delete_floatingip_port_forwarding": "rule:admin_or_ext_parent_owner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -599,3 +599,26 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
||||||
result = policy._is_attribute_explicitly_set(
|
result = policy._is_attribute_explicitly_set(
|
||||||
attr, resource, target, action)
|
attr, resource, target, action)
|
||||||
self.assertFalse(result)
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
@mock.patch("neutron.common.constants.EXT_PARENT_RESOURCE_MAPPING",
|
||||||
|
{'parentresource': 'registered_plugin_name'})
|
||||||
|
@mock.patch("neutron_lib.plugins.directory.get_plugin")
|
||||||
|
def test_enforce_tenant_id_check_parent_resource_owner(
|
||||||
|
self, mock_get_plugin):
|
||||||
|
|
||||||
|
def fakegetparent(*args, **kwargs):
|
||||||
|
return {'tenant_id': 'fake'}
|
||||||
|
mock_plugin = mock.Mock()
|
||||||
|
mock_plugin.get_parentresource = fakegetparent
|
||||||
|
mock_get_plugin.return_value = mock_plugin
|
||||||
|
|
||||||
|
self._set_rules(
|
||||||
|
admin_or_ext_parent_owner="rule:context_is_admin or "
|
||||||
|
"tenant_id:%(ext_parent:tenant_id)s",
|
||||||
|
create_parentresource_subresource="rule:admin_or_ext_parent_owner")
|
||||||
|
self.fakepolicyinit()
|
||||||
|
action = 'create_parentresource_subresource'
|
||||||
|
target = {'ext_parent_parentresource_id': 'whatever', 'foo': 'bar'}
|
||||||
|
result = policy.enforce(self.context, action, target)
|
||||||
|
mock_get_plugin.assert_called_with('registered_plugin_name')
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Introduces extension parent resources owner check in
|
||||||
|
``neutron.policy.OwnerCheck``. It can be used by registering an extension
|
||||||
|
parent resource and service plugin which introduced the corresponding
|
||||||
|
parent resource into ``EXT_PARENT_RESOURCE_MAPPING`` located in
|
||||||
|
``neutron.common.constants``. And introduces a new policy role
|
||||||
|
``admin_or_ext_parent_owner`` into ``policy.json`` for this function.
|
Loading…
Reference in New Issue