From 8094b524f693180fe9f435ef0d1f23b6a44259ae Mon Sep 17 00:00:00 2001 From: Miguel Lavalle Date: Mon, 25 Jan 2021 18:54:33 -0600 Subject: [PATCH] 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 --- doc/source/admin/config-rbac.rst | 75 +++++++++++++++++++ neutron/agent/securitygroups_rpc.py | 2 + neutron/conf/policies/__init__.py | 2 + neutron/conf/policies/address_group.py | 48 ++++++++++++ .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...4425_add_rbac_support_for_address_group.py | 46 ++++++++++++ neutron/db/models/address_group.py | 5 ++ neutron/db/rbac_db_models.py | 11 +++ neutron/extensions/rbac_address_group.py | 20 +++++ neutron/objects/address_group.py | 28 ++++++- neutron/plugins/ml2/plugin.py | 2 + .../tests/contrib/hooks/api_all_extensions | 7 +- .../rpc/handlers/test_securitygroups_rpc.py | 4 +- .../tests/unit/objects/test_address_group.py | 31 ++++++++ neutron/tests/unit/objects/test_objects.py | 3 +- neutron/tests/unit/objects/test_rbac.py | 4 +- 16 files changed, 281 insertions(+), 9 deletions(-) create mode 100644 neutron/conf/policies/address_group.py create mode 100644 neutron/db/migration/alembic_migrations/versions/wallaby/expand/6135a7bd4425_add_rbac_support_for_address_group.py create mode 100644 neutron/extensions/rbac_address_group.py diff --git a/doc/source/admin/config-rbac.rst b/doc/source/admin/config-rbac.rst index 962ff97f4a2..3714e44702f 100644 --- a/doc/source/admin/config-rbac.rst +++ b/doc/source/admin/config-rbac.rst @@ -20,6 +20,7 @@ is supported by: * Binding security groups to ports (since Stein). * Assigning address scopes to subnet pools (since Ussuri). * Assigning subnet pools to subnets (since Ussuri). +* Assigning address groups to security group rules (since Wallaby). 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 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/neutron/agent/securitygroups_rpc.py b/neutron/agent/securitygroups_rpc.py index e2e88b86c0b..2a5b80810cb 100644 --- a/neutron/agent/securitygroups_rpc.py +++ b/neutron/agent/securitygroups_rpc.py @@ -16,6 +16,7 @@ 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 security_groups_normalized_cidr 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) LOG.info('Disabled address-group extension.') _disable_extension('address-group', aliases) + _disable_extension(rbac_ag_apidef.ALIAS, aliases) class SecurityGroupAgentRpc(object): diff --git a/neutron/conf/policies/__init__.py b/neutron/conf/policies/__init__.py index 40ec41b18ca..5fe2153b18b 100644 --- a/neutron/conf/policies/__init__.py +++ b/neutron/conf/policies/__init__.py @@ -14,6 +14,7 @@ import importlib import inspect import itertools +from neutron.conf.policies import address_group from neutron.conf.policies import address_scope from neutron.conf.policies import agent from neutron.conf.policies import auto_allocated_topology @@ -45,6 +46,7 @@ from neutron.conf.policies import trunk def list_rules(): return itertools.chain( base.list_rules(), + address_group.list_rules(), address_scope.list_rules(), agent.list_rules(), auto_allocated_topology.list_rules(), diff --git a/neutron/conf/policies/address_group.py b/neutron/conf/policies/address_group.py new file mode 100644 index 00000000000..25d4184118d --- /dev/null +++ b/neutron/conf/policies/address_group.py @@ -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 diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 7000cdb47ff..30dc6562ae9 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -1e0744e4ffea +6135a7bd4425 diff --git a/neutron/db/migration/alembic_migrations/versions/wallaby/expand/6135a7bd4425_add_rbac_support_for_address_group.py b/neutron/db/migration/alembic_migrations/versions/wallaby/expand/6135a7bd4425_add_rbac_support_for_address_group.py new file mode 100644 index 00000000000..6cfb1ab1c9b --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/wallaby/expand/6135a7bd4425_add_rbac_support_for_address_group.py @@ -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') + ) diff --git a/neutron/db/models/address_group.py b/neutron/db/models/address_group.py index e0e17f3751a..f279507c17b 100644 --- a/neutron/db/models/address_group.py +++ b/neutron/db/models/address_group.py @@ -16,6 +16,7 @@ from neutron_lib.db import model_base import sqlalchemy as sa from sqlalchemy import orm +from neutron.db import rbac_db_models from neutron.db import standard_attr @@ -43,4 +44,8 @@ class AddressGroup(standard_attr.HasStandardAttributes, load_on_pending=True), lazy='subquery', 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] diff --git a/neutron/db/rbac_db_models.py b/neutron/db/rbac_db_models.py index 7f6d147c5af..149a5f9e345 100644 --- a/neutron/db/rbac_db_models.py +++ b/neutron/db/rbac_db_models.py @@ -149,3 +149,14 @@ class SubnetPoolRBAC(RBACColumns, model_base.BASEV2): @staticmethod def get_valid_actions(): 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,) diff --git a/neutron/extensions/rbac_address_group.py b/neutron/extensions/rbac_address_group.py new file mode 100644 index 00000000000..5a02576c057 --- /dev/null +++ b/neutron/extensions/rbac_address_group.py @@ -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 diff --git a/neutron/objects/address_group.py b/neutron/objects/address_group.py index c709b82e683..084dc80e13f 100644 --- a/neutron/objects/address_group.py +++ b/neutron/objects/address_group.py @@ -16,15 +16,30 @@ from oslo_utils import versionutils from oslo_versionedobjects import fields as obj_fields 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 rbac +from neutron.objects import rbac_db +from neutron.objects import securitygroup @base.NeutronObjectRegistry.register -class AddressGroup(base.NeutronDbObject): +class AddressGroupRBAC(rbac.RBACBaseObject): # Version 1.0: Initial version 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' + # Version 1.2: Added RBAC support + VERSION = '1.2' + + # required by RbacNeutronMetaclass + rbac_db_cls = AddressGroupRBAC db_model = models.AddressGroup @@ -32,6 +47,7 @@ class AddressGroup(base.NeutronDbObject): 'id': common_types.UUIDField(), 'name': obj_fields.StringField(nullable=True), 'project_id': obj_fields.StringField(), + 'shared': obj_fields.BooleanField(default=False), 'addresses': obj_fields.ListOfObjectsField('AddressAssociation', nullable=True) } @@ -43,6 +59,14 @@ class AddressGroup(base.NeutronDbObject): standard_fields = ['revision_number', 'created_at', 'updated_at'] for f in standard_fields: 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 diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index f5922920815..0eba6eb0ae9 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -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_extended as pbe_ext 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_security_groups as rbac_sg_apidef from neutron_lib.api.definitions import rbac_subnetpool @@ -192,6 +193,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, external_net.ALIAS, portbindings.ALIAS, "quotas", "security-group", rbac_address_scope.ALIAS, + rbac_ag_apidef.ALIAS, rbac_sg_apidef.ALIAS, rbac_subnetpool.ALIAS, agent_apidef.ALIAS, diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index 5f44bd1e508..f1e538e397d 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -46,9 +46,10 @@ NETWORK_API_EXTENSIONS+=",qos-gateway-ip" NETWORK_API_EXTENSIONS+=",quotas" NETWORK_API_EXTENSIONS+=",quota_details" NETWORK_API_EXTENSIONS+=",rbac-policies" -NETWORK_API_EXTENSIONS+=",rbac-address-scope"" -NETWORK_API_EXTENSIONS+=",rbac-security-groups"" -NETWORK_API_EXTENSIONS+=",rbac-subnetpool"" +NETWORK_API_EXTENSIONS+=",rbac-address-group" +NETWORK_API_EXTENSIONS+=",rbac-address-scope" +NETWORK_API_EXTENSIONS+=",rbac-security-groups" +NETWORK_API_EXTENSIONS+=",rbac-subnetpool" NETWORK_API_EXTENSIONS+=",router" NETWORK_API_EXTENSIONS+=",router-admin-state-down-before-update" NETWORK_API_EXTENSIONS+=",router_availability_zone" diff --git a/neutron/tests/unit/api/rpc/handlers/test_securitygroups_rpc.py b/neutron/tests/unit/api/rpc/handlers/test_securitygroups_rpc.py index ba0666cb9bb..80510c109ca 100644 --- a/neutron/tests/unit/api/rpc/handlers/test_securitygroups_rpc.py +++ b/neutron/tests/unit/api/rpc/handlers/test_securitygroups_rpc.py @@ -94,7 +94,9 @@ class SecurityGroupServerAPIShimTestCase(base.BaseTestCase): self.rcache.record_resource_update(self.ctx, 'Port', 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() address_associations = [ address_group.AddressAssociation( diff --git a/neutron/tests/unit/objects/test_address_group.py b/neutron/tests/unit/objects/test_address_group.py index a744a55e90f..c932f20577d 100644 --- a/neutron/tests/unit/objects/test_address_group.py +++ b/neutron/tests/unit/objects/test_address_group.py @@ -12,6 +12,7 @@ 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_rbac from neutron.tests.unit import testlib_api @@ -46,6 +47,36 @@ class AddressGroupDbObjectTestCase( self.assertIn('description', 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( obj_test_base.BaseObjectIfaceTestCase): diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 1d890fec463..0c7e975b9f4 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -27,7 +27,8 @@ from neutron.tests import base as test_base # alphabetic order. object_data = { 'AddressAssociation': '1.0-b92160a3dd2fb7b951adcd2e6ae1665a', - 'AddressGroup': '1.1-78c35b6ac495407be56b8fcdbeda4d67', + 'AddressGroup': '1.2-1ddbf0a9f61785033ce31818ac62687e', + 'AddressGroupRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d', 'AddressScope': '1.1-dd0dfdb67775892d3adc090e28e43bd8', 'AddressScopeRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d', 'Agent': '1.1-64b670752d57b3c7602cb136e0338507', diff --git a/neutron/tests/unit/objects/test_rbac.py b/neutron/tests/unit/objects/test_rbac.py index 3298b66f546..8775a4046bc 100644 --- a/neutron/tests/unit/objects/test_rbac.py +++ b/neutron/tests/unit/objects/test_rbac.py @@ -13,6 +13,7 @@ import random from unittest import mock +from neutron.objects import address_group from neutron.objects import address_scope from neutron.objects import network from neutron.objects.qos import policy @@ -37,7 +38,8 @@ class TestRBACObjectMixin(object): class RBACBaseObjectTestCase(neutron_test_base.BaseTestCase): 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, 'network': network.NetworkRBAC, 'security_group': securitygroup.SecurityGroupRBAC,