Allow sharing of address groups via RBAC mechanism

Client: https://review.opendev.org/c/openstack/python-openstackclient/+/775045
Tempest tests: https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/773274

Allow sharing of address groups via RBAC mechanism

Change-Id: I9d9e2bd4add5bb6fa4105352bfda739340932571
This commit is contained in:
Miguel Lavalle 2021-01-25 18:54:33 -06:00
parent 77ee0847f5
commit 8094b524f6
16 changed files with 281 additions and 9 deletions

View File

@ -20,6 +20,7 @@ is supported by:
* Binding security groups to ports (since Stein). * Binding security groups to ports (since Stein).
* Assigning address scopes to subnet pools (since Ussuri). * Assigning address scopes to subnet pools (since Ussuri).
* Assigning subnet pools to subnets (since Ussuri). * Assigning subnet pools to subnets (since Ussuri).
* Assigning address groups to security group rules (since Wallaby).
Sharing an object with specific projects Sharing an object with specific projects
@ -444,6 +445,80 @@ the subnet pool is no longer in use:
This process can be repeated any number of times to share a subnet pool This process can be repeated any number of times to share a subnet pool
with an arbitrary number of projects. with an arbitrary number of projects.
Sharing an address group with specific projects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create an address group to share:
.. code-block:: console
$ openstack address group create test-ag --address 10.1.1.1
+-------------+--------------------------------------+
| Field | Value |
+-------------+--------------------------------------+
| addresses | ['10.1.1.1/32'] |
| description | |
| id | cdb6eb3e-f9a0-4d52-8478-358eaa2c4737 |
| name | test-ag |
| project_id | 66c77cf262454777a8f455cce48c12c0 |
+-------------+--------------------------------------+
Create the RBAC policy entry using the :command:`openstack network rbac create`
command (in this example, the ID of the project we want to share with is
``bbd82892525d4372911390b984ed3265``):
.. code-block:: console
$ openstack network rbac create --target-project \
bbd82892525d4372911390b984ed3265 --action access_as_shared \
--type address_group cdb6eb3e-f9a0-4d52-8478-358eaa2c4737
+-------------------+--------------------------------------+
| Field | Value |
+-------------------+--------------------------------------+
| action | access_as_shared |
| id | c7414ac2-9a6b-420b-84c5-4158a6cca4f9 |
| name | None |
| object_id | cdb6eb3e-f9a0-4d52-8478-358eaa2c4737 |
| object_type | address_group |
| project_id | 66c77cf262454777a8f455cce48c12c0 |
| target_project_id | bbd82892525d4372911390b984ed3265 |
+-------------------+--------------------------------------+
The ``target-project`` parameter specifies the project that requires
access to the address group. The ``action`` parameter specifies what
the project is allowed to do. The ``type`` parameter says
that the target object is an address group. The final parameter is the ID of
the address group we are granting access to.
Project ``bbd82892525d4372911390b984ed3265`` will now be able to see
the address group when running :command:`openstack address group list` and
:command:`openstack address group show` and will also be able to assign
it to its security group rules. No other users (other than admins and the
owner) will be able to see the address group.
To remove access for that project, delete the RBAC policy that allows
it using the :command:`openstack network rbac delete` command:
.. code-block:: console
$ openstack network rbac delete c7414ac2-9a6b-420b-84c5-4158a6cca4f9
If that project has security group rules with the address group applied to
them, the server will not delete the RBAC policy until the address group is no
longer in use:
.. code-block:: console
$ openstack network rbac delete c7414ac2-9a6b-420b-84c5-4158a6cca4f9
RBAC policy on object cdb6eb3e-f9a0-4d52-8478-358eaa2c4737
cannot be removed because other objects depend on it
This process can be repeated any number of times to share an address group
with an arbitrary number of projects.
How the 'shared' flag relates to these entries How the 'shared' flag relates to these entries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -16,6 +16,7 @@
import functools import functools
from neutron_lib.api.definitions import rbac_address_groups as rbac_ag_apidef
from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef
from neutron_lib.api.definitions import security_groups_normalized_cidr from neutron_lib.api.definitions import security_groups_normalized_cidr
from neutron_lib.api.definitions import security_groups_remote_address_group \ from neutron_lib.api.definitions import security_groups_remote_address_group \
@ -58,6 +59,7 @@ def disable_security_group_extension_by_config(aliases):
_disable_extension('allowed-address-pairs', aliases) _disable_extension('allowed-address-pairs', aliases)
LOG.info('Disabled address-group extension.') LOG.info('Disabled address-group extension.')
_disable_extension('address-group', aliases) _disable_extension('address-group', aliases)
_disable_extension(rbac_ag_apidef.ALIAS, aliases)
class SecurityGroupAgentRpc(object): class SecurityGroupAgentRpc(object):

View File

@ -14,6 +14,7 @@ import importlib
import inspect import inspect
import itertools import itertools
from neutron.conf.policies import address_group
from neutron.conf.policies import address_scope from neutron.conf.policies import address_scope
from neutron.conf.policies import agent from neutron.conf.policies import agent
from neutron.conf.policies import auto_allocated_topology from neutron.conf.policies import auto_allocated_topology
@ -45,6 +46,7 @@ from neutron.conf.policies import trunk
def list_rules(): def list_rules():
return itertools.chain( return itertools.chain(
base.list_rules(), base.list_rules(),
address_group.list_rules(),
address_scope.list_rules(), address_scope.list_rules(),
agent.list_rules(), agent.list_rules(),
auto_allocated_topology.list_rules(), auto_allocated_topology.list_rules(),

View File

@ -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
from neutron.conf.policies import base
AG_COLLECTION_PATH = '/address-groups'
AG_RESOURCE_PATH = '/address-groups/{id}'
rules = [
policy.RuleDefault(
'shared_address_groups',
'field:address_groups:shared=True',
'Definition of a shared address group'
),
policy.DocumentedRuleDefault(
name='get_address_group',
check_str=base.policy_or(base.RULE_ADMIN_OR_OWNER,
'rule:shared_address_groups'),
description='Get an address group',
operations=[
{
'method': 'GET',
'path': AG_COLLECTION_PATH,
},
{
'method': 'GET',
'path': AG_RESOURCE_PATH,
},
]
),
]
def list_rules():
return rules

View File

@ -1 +1 @@
1e0744e4ffea 6135a7bd4425

View File

@ -0,0 +1,46 @@
# 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 alembic import op
import sqlalchemy as sa
"""add_rbac_support_for_address_group
Revision ID: 6135a7bd4425
Revises: 1e0744e4ffea
Create Date: 2021-01-22 11:24:07.435031
"""
# revision identifiers, used by Alembic.
revision = '6135a7bd4425'
down_revision = '1e0744e4ffea'
def upgrade():
op.create_table(
'addressgrouprbacs', sa.MetaData(),
sa.Column('project_id', sa.String(length=255), nullable=True,
index=True),
sa.Column('id', sa.String(length=36), nullable=False,
primary_key=True),
sa.Column('target_tenant', sa.String(length=255), nullable=False),
sa.Column('action', sa.String(length=255), nullable=False),
sa.Column('object_id', sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['object_id'], ['address_groups.id'],
ondelete='CASCADE'),
sa.UniqueConstraint('target_tenant', 'object_id', 'action',
name='uniq_address_groups_rbacs0'
'target_tenant0object_id0action')
)

View File

@ -16,6 +16,7 @@ from neutron_lib.db import model_base
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from neutron.db import rbac_db_models
from neutron.db import standard_attr from neutron.db import standard_attr
@ -43,4 +44,8 @@ class AddressGroup(standard_attr.HasStandardAttributes,
load_on_pending=True), load_on_pending=True),
lazy='subquery', lazy='subquery',
cascade='all, delete-orphan') cascade='all, delete-orphan')
rbac_entries = sa.orm.relationship(rbac_db_models.AddressGroupRBAC,
backref='address_groups',
lazy='subquery',
cascade='all, delete, delete-orphan')
api_collections = [ag.ALIAS] api_collections = [ag.ALIAS]

View File

@ -149,3 +149,14 @@ class SubnetPoolRBAC(RBACColumns, model_base.BASEV2):
@staticmethod @staticmethod
def get_valid_actions(): def get_valid_actions():
return (ACCESS_SHARED,) return (ACCESS_SHARED,)
class AddressGroupRBAC(RBACColumns, model_base.BASEV2):
"""RBAC table for address_group."""
object_id = _object_id_column('address_groups.id')
object_type = 'address_group'
@staticmethod
def get_valid_actions():
return (ACCESS_SHARED,)

View File

@ -0,0 +1,20 @@
# 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 neutron_lib.api.definitions import rbac_address_groups as apidef
from neutron_lib.api import extensions
class Rbac_address_group(extensions.APIExtensionDescriptor):
"""Extension class supporting address group RBAC."""
api_definition = apidef

View File

@ -16,15 +16,30 @@ from oslo_utils import versionutils
from oslo_versionedobjects import fields as obj_fields from oslo_versionedobjects import fields as obj_fields
from neutron.db.models import address_group as models from neutron.db.models import address_group as models
from neutron.db import rbac_db_models
from neutron.objects import base from neutron.objects import base
from neutron.objects import rbac
from neutron.objects import rbac_db
from neutron.objects import securitygroup
@base.NeutronObjectRegistry.register @base.NeutronObjectRegistry.register
class AddressGroup(base.NeutronDbObject): class AddressGroupRBAC(rbac.RBACBaseObject):
# Version 1.0: Initial version # Version 1.0: Initial version
VERSION = '1.0' VERSION = '1.0'
db_model = rbac_db_models.AddressGroupRBAC
@base.NeutronObjectRegistry.register
class AddressGroup(rbac_db.NeutronRbacObject):
# Version 1.0: Initial version
# Version 1.1: Added standard attributes # Version 1.1: Added standard attributes
VERSION = '1.1' # Version 1.2: Added RBAC support
VERSION = '1.2'
# required by RbacNeutronMetaclass
rbac_db_cls = AddressGroupRBAC
db_model = models.AddressGroup db_model = models.AddressGroup
@ -32,6 +47,7 @@ class AddressGroup(base.NeutronDbObject):
'id': common_types.UUIDField(), 'id': common_types.UUIDField(),
'name': obj_fields.StringField(nullable=True), 'name': obj_fields.StringField(nullable=True),
'project_id': obj_fields.StringField(), 'project_id': obj_fields.StringField(),
'shared': obj_fields.BooleanField(default=False),
'addresses': obj_fields.ListOfObjectsField('AddressAssociation', 'addresses': obj_fields.ListOfObjectsField('AddressAssociation',
nullable=True) nullable=True)
} }
@ -43,6 +59,14 @@ class AddressGroup(base.NeutronDbObject):
standard_fields = ['revision_number', 'created_at', 'updated_at'] standard_fields = ['revision_number', 'created_at', 'updated_at']
for f in standard_fields: for f in standard_fields:
primitive.pop(f, None) primitive.pop(f, None)
if _target_version < (1, 2):
primitive.pop('shared', None)
@classmethod
def get_bound_tenant_ids(cls, context, obj_id):
ag_objs = securitygroup.SecurityGroupRule.get_objects(
context, remote_address_group_id=[obj_id])
return {ag.tenant_id for ag in ag_objs}
@base.NeutronObjectRegistry.register @base.NeutronObjectRegistry.register

View File

@ -46,6 +46,7 @@ from neutron_lib.api.definitions import port_security as psec
from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import portbindings_extended as pbe_ext from neutron_lib.api.definitions import portbindings_extended as pbe_ext
from neutron_lib.api.definitions import provider_net from neutron_lib.api.definitions import provider_net
from neutron_lib.api.definitions import rbac_address_groups as rbac_ag_apidef
from neutron_lib.api.definitions import rbac_address_scope from neutron_lib.api.definitions import rbac_address_scope
from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef
from neutron_lib.api.definitions import rbac_subnetpool from neutron_lib.api.definitions import rbac_subnetpool
@ -192,6 +193,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
external_net.ALIAS, portbindings.ALIAS, external_net.ALIAS, portbindings.ALIAS,
"quotas", "security-group", "quotas", "security-group",
rbac_address_scope.ALIAS, rbac_address_scope.ALIAS,
rbac_ag_apidef.ALIAS,
rbac_sg_apidef.ALIAS, rbac_sg_apidef.ALIAS,
rbac_subnetpool.ALIAS, rbac_subnetpool.ALIAS,
agent_apidef.ALIAS, agent_apidef.ALIAS,

View File

@ -46,9 +46,10 @@ NETWORK_API_EXTENSIONS+=",qos-gateway-ip"
NETWORK_API_EXTENSIONS+=",quotas" NETWORK_API_EXTENSIONS+=",quotas"
NETWORK_API_EXTENSIONS+=",quota_details" NETWORK_API_EXTENSIONS+=",quota_details"
NETWORK_API_EXTENSIONS+=",rbac-policies" NETWORK_API_EXTENSIONS+=",rbac-policies"
NETWORK_API_EXTENSIONS+=",rbac-address-scope"" NETWORK_API_EXTENSIONS+=",rbac-address-group"
NETWORK_API_EXTENSIONS+=",rbac-security-groups"" NETWORK_API_EXTENSIONS+=",rbac-address-scope"
NETWORK_API_EXTENSIONS+=",rbac-subnetpool"" NETWORK_API_EXTENSIONS+=",rbac-security-groups"
NETWORK_API_EXTENSIONS+=",rbac-subnetpool"
NETWORK_API_EXTENSIONS+=",router" NETWORK_API_EXTENSIONS+=",router"
NETWORK_API_EXTENSIONS+=",router-admin-state-down-before-update" NETWORK_API_EXTENSIONS+=",router-admin-state-down-before-update"
NETWORK_API_EXTENSIONS+=",router_availability_zone" NETWORK_API_EXTENSIONS+=",router_availability_zone"

View File

@ -94,7 +94,9 @@ class SecurityGroupServerAPIShimTestCase(base.BaseTestCase):
self.rcache.record_resource_update(self.ctx, 'Port', p) self.rcache.record_resource_update(self.ctx, 'Port', p)
return p return p
def _make_address_group_ovo(self): @mock.patch.object(address_group.AddressGroup, 'is_shared_with_tenant',
return_value=False)
def _make_address_group_ovo(self, *args, **kwargs):
id = uuidutils.generate_uuid() id = uuidutils.generate_uuid()
address_associations = [ address_associations = [
address_group.AddressAssociation( address_group.AddressAssociation(

View File

@ -12,6 +12,7 @@
from neutron.objects import address_group from neutron.objects import address_group
from neutron.tests.unit.objects import test_base as obj_test_base from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit.objects import test_rbac
from neutron.tests.unit import testlib_api from neutron.tests.unit import testlib_api
@ -46,6 +47,36 @@ class AddressGroupDbObjectTestCase(
self.assertIn('description', self.assertIn('description',
ag_obj_1_0['versioned_object.data']) ag_obj_1_0['versioned_object.data'])
def test_object_version_degradation_1_2_to_1_1_no_shared(self):
ag_obj = self._create_test_address_group()
ag_obj_1_1 = ag_obj.obj_to_primitive('1.1')
self.assertNotIn('shared', ag_obj_1_1['versioned_object.data'])
class AddressGroupRBACDbObjectTestCase(test_rbac.TestRBACObjectMixin,
obj_test_base.BaseDbObjectTestCase,
testlib_api.SqlTestCase):
_test_class = address_group.AddressGroupRBAC
def setUp(self):
super(AddressGroupRBACDbObjectTestCase, self).setUp()
for obj in self.db_objs:
ag_obj = address_group.AddressGroup(self.context,
id=obj['object_id'],
project_id=obj['project_id'])
ag_obj.create()
def _create_test_address_group_rbac(self):
self.objs[0].create()
return self.objs[0]
class AddressGroupRBACIfaceObjectTestCase(
test_rbac.TestRBACObjectMixin, obj_test_base.BaseObjectIfaceTestCase):
_test_class = address_group.AddressGroupRBAC
class AddressAssociationIfaceObjectTestCase( class AddressAssociationIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase): obj_test_base.BaseObjectIfaceTestCase):

View File

@ -27,7 +27,8 @@ from neutron.tests import base as test_base
# alphabetic order. # alphabetic order.
object_data = { object_data = {
'AddressAssociation': '1.0-b92160a3dd2fb7b951adcd2e6ae1665a', 'AddressAssociation': '1.0-b92160a3dd2fb7b951adcd2e6ae1665a',
'AddressGroup': '1.1-78c35b6ac495407be56b8fcdbeda4d67', 'AddressGroup': '1.2-1ddbf0a9f61785033ce31818ac62687e',
'AddressGroupRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d',
'AddressScope': '1.1-dd0dfdb67775892d3adc090e28e43bd8', 'AddressScope': '1.1-dd0dfdb67775892d3adc090e28e43bd8',
'AddressScopeRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d', 'AddressScopeRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d',
'Agent': '1.1-64b670752d57b3c7602cb136e0338507', 'Agent': '1.1-64b670752d57b3c7602cb136e0338507',

View File

@ -13,6 +13,7 @@ import random
from unittest import mock from unittest import mock
from neutron.objects import address_group
from neutron.objects import address_scope from neutron.objects import address_scope
from neutron.objects import network from neutron.objects import network
from neutron.objects.qos import policy from neutron.objects.qos import policy
@ -37,7 +38,8 @@ class TestRBACObjectMixin(object):
class RBACBaseObjectTestCase(neutron_test_base.BaseTestCase): class RBACBaseObjectTestCase(neutron_test_base.BaseTestCase):
def test_get_type_class_map(self): def test_get_type_class_map(self):
class_map = {'address_scope': address_scope.AddressScopeRBAC, class_map = {'address_group': address_group.AddressGroupRBAC,
'address_scope': address_scope.AddressScopeRBAC,
'qos_policy': policy.QosPolicyRBAC, 'qos_policy': policy.QosPolicyRBAC,
'network': network.NetworkRBAC, 'network': network.NetworkRBAC,
'security_group': securitygroup.SecurityGroupRBAC, 'security_group': securitygroup.SecurityGroupRBAC,