From 56b971bb42c69ea6865bed9b5fbc4ff372af0027 Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Mon, 10 Feb 2020 17:20:36 +0200 Subject: [PATCH] Allow sharing of subnet pools via RBAC mechanism Neutron-lib api ref: https://review.opendev.org/705998/ Client: https://review.opendev.org/#/c/712705/ Tempest tests: https://review.opendev.org/#/c/711656/ Change-Id: I1d6125513cd8cb088b84c92497866f78955019a9 Partial-Bug: #1862032 Depends-On: https://review.opendev.org/709122 --- doc/source/admin/config-rbac.rst | 89 ++++++++++++- neutron/db/db_base_plugin_common.py | 5 +- neutron/db/db_base_plugin_v2.py | 8 +- .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...daa9591_add_rbac_support_for_subnetpool.py | 81 ++++++++++++ neutron/db/models_v2.py | 14 +- neutron/db/rbac_db_models.py | 11 ++ neutron/extensions/rbac.py | 5 + neutron/extensions/rbac_subnetpool.py | 22 ++++ neutron/objects/address_scope.py | 17 --- neutron/objects/rbac.py | 10 +- neutron/objects/rbac_db.py | 10 +- neutron/objects/subnetpool.py | 63 ++++++++- neutron/plugins/ml2/plugin.py | 2 + .../tests/contrib/hooks/api_all_extensions | 1 + .../tests/unit/db/test_db_base_plugin_v2.py | 4 +- neutron/tests/unit/objects/test_objects.py | 3 +- neutron/tests/unit/objects/test_rbac.py | 4 +- neutron/tests/unit/objects/test_subnetpool.py | 124 +++++++++++++++++- .../add-subnetpool-rbac-2eb2008bd1b27b11.yaml | 5 + 20 files changed, 440 insertions(+), 40 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/ussuri/expand/e88badaa9591_add_rbac_support_for_subnetpool.py create mode 100644 neutron/extensions/rbac_subnetpool.py create mode 100644 releasenotes/notes/add-subnetpool-rbac-2eb2008bd1b27b11.yaml diff --git a/doc/source/admin/config-rbac.rst b/doc/source/admin/config-rbac.rst index cedfdf815ed..7b0c1a1d610 100644 --- a/doc/source/admin/config-rbac.rst +++ b/doc/source/admin/config-rbac.rst @@ -19,6 +19,7 @@ is supported by: * Attaching router gateways to networks (since Mitaka). * Binding security groups to ports (since Stein). * Assigning address scopes to subnet pools (since Ussuri). +* Assigning subnet pools to subnets (since Ussuri). Sharing an object with specific projects @@ -357,12 +358,98 @@ the address scope is no longer in use: This process can be repeated any number of times to share an address scope with an arbitrary number of projects. +Sharing a subnet pool with specific projects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a subnet pool to share: + +.. code-block:: console + + $ openstack subnet pool create my_subnetpool --pool-prefix 203.0.113.0/24 + +-------------------+--------------------------------------+ + | Field | Value | + +-------------------+--------------------------------------+ + | address_scope_id | None | + | created_at | 2020-03-16T14:23:01Z | + | default_prefixlen | 8 | + | default_quota | None | + | description | | + | id | 11f79287-bc17-46b2-bfd0-2562471eb631 | + | ip_version | 4 | + | is_default | False | + | location | ... | + | max_prefixlen | 32 | + | min_prefixlen | 8 | + | name | my_subnetpool | + | project_id | 290ccedbcf594ecc8e76eff06f964f7e | + | revision_number | 0 | + | shared | False | + | tags | | + | updated_at | 2020-03-16T14:23:01Z | + +-------------------+--------------------------------------+ + + +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 +``32016615de5d43bb88de99e7f2e26a1e``): + +.. code-block:: console + + $ openstack network rbac create --target-project \ + 32016615de5d43bb88de99e7f2e26a1e --action access_as_shared \ + --type subnetpool 11f79287-bc17-46b2-bfd0-2562471eb631 + +-------------------+--------------------------------------+ + | Field | Value | + +-------------------+--------------------------------------+ + | action | access_as_shared | + | id | d54b1482-98c4-44aa-9115-ede80387ffe0 | + | location | ... | + | name | None | + | object_id | 11f79287-bc17-46b2-bfd0-2562471eb631 | + | object_type | subnetpool | + | project_id | 290ccedbcf594ecc8e76eff06f964f7e | + | target_project_id | 32016615de5d43bb88de99e7f2e26a1e | + +-------------------+--------------------------------------+ + + +The ``target-project`` parameter specifies the project that requires +access to the subnet pool. The ``action`` parameter specifies what +the project is allowed to do. The ``type`` parameter says +that the target object is a subnet pool. The final parameter is the ID of +the subnet pool we are granting access to. + +Project ``32016615de5d43bb88de99e7f2e26a1e`` will now be able to see +the subnet pool when running :command:`openstack subnet pool list` and +:command:`openstack subnet pool show` and will also be able to assign +it to its subnets. No other users (other than admins and the owner) +will be able to see the subnet pool. + +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 d54b1482-98c4-44aa-9115-ede80387ffe0 + +If that project has subnets with the subnet pool applied to them, +the server will not delete the RBAC policy until +the subnet pool is no longer in use: + +.. code-block:: console + + $ openstack network rbac delete d54b1482-98c4-44aa-9115-ede80387ffe0 + RBAC policy on object 11f79287-bc17-46b2-bfd0-2562471eb631 + cannot be removed because other objects depend on it. + +This process can be repeated any number of times to share a subnet pool +with an arbitrary number of projects. + How the 'shared' flag relates to these entries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As introduced in other guide entries, neutron provides a means of making an object (``address-scope``, ``network``, ``qos-policy``, -``security-group``) available to every project. +``security-group``, ``subnetpool``) available to every project. This is accomplished using the ``shared`` flag on the supported object: .. code-block:: console diff --git a/neutron/db/db_base_plugin_common.py b/neutron/db/db_base_plugin_common.py index 24318070bc0..7c6d92522cb 100644 --- a/neutron/db/db_base_plugin_common.py +++ b/neutron/db/db_base_plugin_common.py @@ -207,12 +207,13 @@ class DbBasePluginCommon(object): 'max_prefixlen': max_prefixlen, 'is_default': subnetpool['is_default'], 'shared': subnetpool['shared'], - 'prefixes': [prefix.cidr for prefix in subnetpool['prefixes']], + 'prefixes': [str(prefix.cidr) + for prefix in subnetpool['prefixes']], 'ip_version': subnetpool['ip_version'], 'default_quota': subnetpool['default_quota'], 'address_scope_id': subnetpool['address_scope_id']} resource_extend.apply_funcs( - subnetpool_def.COLLECTION_NAME, res, subnetpool) + subnetpool_def.COLLECTION_NAME, res, subnetpool.db_obj) return db_utils.resource_fields(res, fields) def _make_port_dict(self, port, fields=None, diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 66760d6ca35..01a4515d4ca 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -90,7 +90,7 @@ def _check_subnet_not_used(context, subnet_id): def _update_subnetpool_dict(orig_pool, new_pool): updated = dict((k, v) for k, v in orig_pool.to_dict().items() - if k not in orig_pool.synthetic_fields) + if k not in orig_pool.synthetic_fields or k == 'shared') new_pool = new_pool.copy() new_prefixes = new_pool.pop('prefixes', constants.ATTR_NOT_SPECIFIED) @@ -1249,7 +1249,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, subnetpool = subnetpool_obj.SubnetPool(context, **pool_args) subnetpool.create() - return self._make_subnetpool_dict(subnetpool.db_obj) + return self._make_subnetpool_dict(subnetpool) @db_api.retry_if_session_inactive() def update_subnetpool(self, context, id, subnetpool): @@ -1292,7 +1292,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, @db_api.retry_if_session_inactive() def get_subnetpool(self, context, id, fields=None): subnetpool = self._get_subnetpool(context, id) - return self._make_subnetpool_dict(subnetpool.db_obj, fields) + return self._make_subnetpool_dict(subnetpool, fields) @db_api.retry_if_session_inactive() def get_subnetpools(self, context, filters=None, fields=None, @@ -1303,7 +1303,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, subnetpools = subnetpool_obj.SubnetPool.get_objects( context, _pager=pager, validate_filters=False, **filters) return [ - self._make_subnetpool_dict(pool.db_obj, fields) + self._make_subnetpool_dict(pool, fields) for pool in subnetpools ] diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index b56399ec76c..4245f1d9287 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -e4e236b0e1ff +e88badaa9591 diff --git a/neutron/db/migration/alembic_migrations/versions/ussuri/expand/e88badaa9591_add_rbac_support_for_subnetpool.py b/neutron/db/migration/alembic_migrations/versions/ussuri/expand/e88badaa9591_add_rbac_support_for_subnetpool.py new file mode 100644 index 00000000000..5baa5548775 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/ussuri/expand/e88badaa9591_add_rbac_support_for_subnetpool.py @@ -0,0 +1,81 @@ +# Copyright 2020 OpenStack Foundation +# +# 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 +from oslo_utils import uuidutils +import sqlalchemy as sa +from sqlalchemy import sql + + +"""add rbac support for subnetpool + +Revision ID: e88badaa9591 +Revises: e4e236b0e1ff +Create Date: 2020-02-10 12:30:30.060646 + +""" + +# revision identifiers, used by Alembic. +revision = 'e88badaa9591' +down_revision = 'e4e236b0e1ff' +depends_on = ('7d9d8eeec6ad',) + + +def upgrade(): + subnetpool_rbacs = op.create_table( + 'subnetpoolrbacs', sa.MetaData(), + sa.Column('project_id', sa.String(length=255), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + 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'], ['subnetpools.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('target_tenant', 'object_id', 'action', + name='uniq_subnetpools_rbacs0' + 'target_tenant0object_id0action') + ) + + op.alter_column('subnetpools', 'shared', server_default=sql.false()) + + op.bulk_insert( + subnetpool_rbacs, + get_rbac_policies_for_shared_subnetpools() + ) + + op.create_index(op.f('ix_subnetpoolrbacs_project_id'), + 'subnetpoolrbacs', ['project_id'], unique=False) + + +def get_rbac_policies_for_shared_subnetpools(): + # A simple model of the subnetpools table with only the fields needed for + # the migration. + subnetpool = sa.Table( + 'subnetpools', sa.MetaData(), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('project_id', sa.String(length=255)), + sa.Column('shared', sa.Boolean(), nullable=False) + ) + + session = sa.orm.Session(bind=op.get_bind()) + values = [] + for row in session.query(subnetpool).filter(subnetpool.c.shared).all(): + values.append({'id': uuidutils.generate_uuid(), 'object_id': row[0], + 'project_id': row[1], 'target_tenant': '*', + 'action': 'access_as_shared'}) + # this commit appears to be necessary to allow further operations + session.commit() + return values diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index a9a2b13dda4..de4012a0222 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -235,7 +235,15 @@ class SubnetPool(standard_attr.HasStandardAttributes, model_base.BASEV2, default_prefixlen = sa.Column(sa.Integer, nullable=False) min_prefixlen = sa.Column(sa.Integer, nullable=False) max_prefixlen = sa.Column(sa.Integer, nullable=False) - shared = sa.Column(sa.Boolean, nullable=False) + + # TODO(imalinovskiy): drop this field when contract migrations will be + # allowed again + # NOTE(imalinovskiy): this field cannot be removed from model due to + # functional test test_models_sync, trailing underscore is required to + # prevent conflicts with RBAC code + shared_ = sa.Column("shared", sa.Boolean, nullable=False, + server_default=sql.false()) + is_default = sa.Column(sa.Boolean, nullable=False, server_default=sql.false()) default_quota = sa.Column(sa.Integer, nullable=True) @@ -245,6 +253,10 @@ class SubnetPool(standard_attr.HasStandardAttributes, model_base.BASEV2, backref='subnetpools', cascade='all, delete, delete-orphan', lazy='subquery') + rbac_entries = sa.orm.relationship(rbac_db_models.SubnetPoolRBAC, + backref='subnetpools', + lazy='subquery', + cascade='all, delete, delete-orphan') api_collections = [subnetpool_def.COLLECTION_NAME] collection_resource_map = {subnetpool_def.COLLECTION_NAME: subnetpool_def.RESOURCE_NAME} diff --git a/neutron/db/rbac_db_models.py b/neutron/db/rbac_db_models.py index 1a26276b42e..9c1aa85608a 100644 --- a/neutron/db/rbac_db_models.py +++ b/neutron/db/rbac_db_models.py @@ -137,3 +137,14 @@ class AddressScopeRBAC(RBACColumns, model_base.BASEV2): @staticmethod def get_valid_actions(): return (ACCESS_SHARED,) + + +class SubnetPoolRBAC(RBACColumns, model_base.BASEV2): + """RBAC table for subnetpool.""" + + object_id = _object_id_column('subnetpools.id') + object_type = 'subnetpool' + + @staticmethod + def get_valid_actions(): + return (ACCESS_SHARED,) diff --git a/neutron/extensions/rbac.py b/neutron/extensions/rbac.py index 71d4eabbf23..2568753568e 100644 --- a/neutron/extensions/rbac.py +++ b/neutron/extensions/rbac.py @@ -39,6 +39,11 @@ class DuplicateRbacPolicy(n_exc.Conflict): message = _("An RBAC policy already exists with those values.") +class RbacPolicyInitError(n_exc.PolicyInitError): + message = _("Failed to create RBAC policy on object %(object_id)s " + "because %(reason)s.") + + def convert_valid_object_type(otype): normalized = otype.strip().lower() if normalized in rbac_db_models.get_type_model_map(): diff --git a/neutron/extensions/rbac_subnetpool.py b/neutron/extensions/rbac_subnetpool.py new file mode 100644 index 00000000000..a268c5c2ad7 --- /dev/null +++ b/neutron/extensions/rbac_subnetpool.py @@ -0,0 +1,22 @@ +# Copyright (c) 2020 Cloudification GmbH. All rights reserved. +# +# 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_subnetpool +from neutron_lib.api import extensions + + +class Rbac_subnetpool(extensions.APIExtensionDescriptor): + """Extension class supporting subnetpool RBAC.""" + + api_definition = rbac_subnetpool diff --git a/neutron/objects/address_scope.py b/neutron/objects/address_scope.py index 22cc21a1c09..decfa7954e3 100644 --- a/neutron/objects/address_scope.py +++ b/neutron/objects/address_scope.py @@ -14,7 +14,6 @@ from neutron_lib.objects import common_types from oslo_versionedobjects import fields as obj_fields -import sqlalchemy as sa from neutron.db.models import address_scope as models from neutron.db import models_v2 @@ -32,22 +31,6 @@ class AddressScopeRBAC(rbac.RBACBaseObject): db_model = rbac_db_models.AddressScopeRBAC - @classmethod - def get_projects(cls, context, object_id=None, action=None, - target_tenant=None): - clauses = [] - - if object_id: - clauses.append(cls.db_model.object_id == object_id) - if action: - clauses.append(cls.db_model.action == action) - if target_tenant: - clauses.append(cls.db_model.target_tenant == target_tenant) - query = context.session.query(cls.db_model.target_tenant) - if clauses: - query = query.filter(sa.and_(*clauses)) - return [data[0] for data in query] - @base.NeutronObjectRegistry.register class AddressScope(rbac_db.NeutronRbacObject): diff --git a/neutron/objects/rbac.py b/neutron/objects/rbac.py index 62cfd1de742..9e3db8e01ec 100644 --- a/neutron/objects/rbac.py +++ b/neutron/objects/rbac.py @@ -20,7 +20,6 @@ from oslo_versionedobjects import fields as obj_fields from six import add_metaclass from sqlalchemy import and_ -from neutron.db import rbac_db_models as models from neutron.objects import base @@ -45,13 +44,12 @@ class RBACBaseObject(base.NeutronDbObject): target_tenant=None): clauses = [] if object_id: - clauses.append(models.NetworkRBAC.object_id == object_id) + clauses.append(cls.db_model.object_id == object_id) if action: - clauses.append(models.NetworkRBAC.action == action) + clauses.append(cls.db_model.action == action) if target_tenant: - clauses.append(models.NetworkRBAC.target_tenant == - target_tenant) - query = context.session.query(models.NetworkRBAC.target_tenant) + clauses.append(cls.db_model.target_tenant == target_tenant) + query = context.session.query(cls.db_model.target_tenant) if clauses: query = query.filter(and_(*clauses)) return [data[0] for data in query] diff --git a/neutron/objects/rbac_db.py b/neutron/objects/rbac_db.py index 8c7e4601ff1..c68173dc116 100644 --- a/neutron/objects/rbac_db.py +++ b/neutron/objects/rbac_db.py @@ -156,6 +156,13 @@ class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin, obj_id=policy['object_id'], target_tenant=target_tenant) + @classmethod + def validate_rbac_policy_create(cls, resource, event, trigger, + payload=None): + """Callback to handle RBAC_POLICY, BEFORE_CREATE callback. + """ + pass + @classmethod def validate_rbac_policy_update(cls, resource, event, trigger, payload=None): @@ -201,7 +208,8 @@ class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin, msg = _("Only admins can manipulate policies on objects " "they do not own") raise exceptions.InvalidInput(error_message=msg) - callback_map = {events.BEFORE_UPDATE: cls.validate_rbac_policy_update, + callback_map = {events.BEFORE_CREATE: cls.validate_rbac_policy_create, + events.BEFORE_UPDATE: cls.validate_rbac_policy_update, events.BEFORE_DELETE: cls.validate_rbac_policy_delete} if event in callback_map: return callback_map[event](resource, event, trigger, diff --git a/neutron/objects/subnetpool.py b/neutron/objects/subnetpool.py index 8a254e935af..73ae0c568dc 100644 --- a/neutron/objects/subnetpool.py +++ b/neutron/objects/subnetpool.py @@ -14,18 +14,39 @@ # under the License. import netaddr +from neutron_lib.db import model_query from neutron_lib.objects import common_types from oslo_versionedobjects import fields as obj_fields +import sqlalchemy as sa +from neutron._i18n import _ from neutron.db import models_v2 as models +from neutron.db import rbac_db_models +from neutron.extensions import rbac as ext_rbac from neutron.objects import base +from neutron.objects.db import api as obj_db_api +from neutron.objects import rbac +from neutron.objects import rbac_db +from neutron.objects import subnet @base.NeutronObjectRegistry.register -class SubnetPool(base.NeutronDbObject): +class SubnetPoolRBAC(rbac.RBACBaseObject): # Version 1.0: Initial version VERSION = '1.0' + db_model = rbac_db_models.SubnetPoolRBAC + + +@base.NeutronObjectRegistry.register +class SubnetPool(rbac_db.NeutronRbacObject): + # Version 1.0: Initial version + # Version 1.1: Add RBAC support + VERSION = '1.1' + + # required by RbacNeutronMetaclass + rbac_db_cls = SubnetPoolRBAC + db_model = models.SubnetPool fields = { @@ -83,6 +104,46 @@ class SubnetPool(base.NeutronDbObject): if 'prefixes' in fields: self._attach_prefixes(fields['prefixes']) + @classmethod + def get_bound_tenant_ids(cls, context, obj_id): + sn_objs = subnet.Subnet.get_objects(context, subnetpool_id=obj_id) + return {snp.project_id for snp in sn_objs} + + @classmethod + def validate_rbac_policy_create(cls, resource, event, trigger, + payload=None): + context = payload.context + policy = payload.request_body + + db_obj = obj_db_api.get_object( + cls, context.elevated(), id=policy['object_id']) + + if not db_obj["address_scope_id"]: + # Nothing to validate + return + + rbac_as_model = rbac_db_models.AddressScopeRBAC + + # Ensure that target project has access to AS + shared_to_target_project_or_to_all = ( + sa.and_( + rbac_as_model.target_tenant.in_( + ["*", policy['target_tenant']] + ), + rbac_as_model.object_id == db_obj["address_scope_id"] + ) + ) + + matching_policies = model_query.query_with_hooks( + context, rbac_db_models.AddressScopeRBAC + ).filter(shared_to_target_project_or_to_all).count() + + if matching_policies == 0: + raise ext_rbac.RbacPolicyInitError( + object_id=policy['object_id'], + reason=_("target project doesn't have access to " + "associated address scope.")) + @base.NeutronObjectRegistry.register class SubnetPoolPrefix(base.NeutronDbObject): diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index b0ae2ad8f25..7084c4621be 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -45,6 +45,7 @@ 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_scope 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 security_groups_port_filtering from neutron_lib.api.definitions import stateful_security_group from neutron_lib.api.definitions import subnet as subnet_def @@ -184,6 +185,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, "quotas", "security-group", rbac_address_scope.ALIAS, rbac_sg_apidef.ALIAS, + rbac_subnetpool.ALIAS, agent_apidef.ALIAS, dhcpagentscheduler.ALIAS, multiprovidernet.ALIAS, diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index b303f71ea67..a80c49bf900 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -47,6 +47,7 @@ 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+=",router" NETWORK_API_EXTENSIONS+=",router-admin-state-down-before-update" NETWORK_API_EXTENSIONS+=",router_availability_zone" diff --git a/neutron/tests/unit/db/test_db_base_plugin_v2.py b/neutron/tests/unit/db/test_db_base_plugin_v2.py index 72f5ec17753..53d5a824fbe 100644 --- a/neutron/tests/unit/db/test_db_base_plugin_v2.py +++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py @@ -6767,7 +6767,7 @@ class DbModelTenantTestCase(DbModelMixin, testlib_api.SqlTestCase): with db_api.CONTEXT_WRITER.using(ctx): subnetpool = models_v2.SubnetPool( ip_version=constants.IP_VERSION_4, default_prefixlen=4, - min_prefixlen=4, max_prefixlen=4, shared=False, + min_prefixlen=4, max_prefixlen=4, default_quota=4, address_scope_id='f', tenant_id='dbcheck', is_default=False ) @@ -6807,7 +6807,7 @@ class DbModelProjectTestCase(DbModelMixin, testlib_api.SqlTestCase): with db_api.CONTEXT_WRITER.using(ctx): subnetpool = models_v2.SubnetPool( ip_version=constants.IP_VERSION_4, default_prefixlen=4, - min_prefixlen=4, max_prefixlen=4, shared=False, + min_prefixlen=4, max_prefixlen=4, default_quota=4, address_scope_id='f', project_id='dbcheck', is_default=False ) diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index b4318118ea2..955659c5304 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -108,8 +108,9 @@ object_data = { 'StandardAttribute': '1.0-617d4f46524c4ce734a6fc1cc0ac6a0b', 'Subnet': '1.1-5b7e1789a1732259d1e28b4bd87eb1c2', 'SubnetDNSPublishFixedIP': '1.0-db22af6fa20b143986f0cbe06cbfe0ea', - 'SubnetPool': '1.0-a0e03895d1a6e7b9d4ab7b0ca13c3867', + 'SubnetPool': '1.1-a0e03895d1a6e7b9d4ab7b0ca13c3867', 'SubnetPoolPrefix': '1.0-13c15144135eb869faa4a76dc3ee3b6c', + 'SubnetPoolRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d', 'SubnetServiceType': '1.0-05ae4cdb2a9026a697b143926a1add8c', 'SubPort': '1.0-72c8471068db1f0491b5480fe49b52bb', 'Tag': '1.0-1a0d20379920ffa3cebfd3e016d2f7a0', diff --git a/neutron/tests/unit/objects/test_rbac.py b/neutron/tests/unit/objects/test_rbac.py index 0dd9cb1912d..d3c48b5f97b 100644 --- a/neutron/tests/unit/objects/test_rbac.py +++ b/neutron/tests/unit/objects/test_rbac.py @@ -18,6 +18,7 @@ from neutron.objects import network from neutron.objects.qos import policy from neutron.objects import rbac from neutron.objects import securitygroup +from neutron.objects import subnetpool from neutron.tests import base as neutron_test_base from neutron.tests.unit.objects import test_base @@ -39,7 +40,8 @@ class RBACBaseObjectTestCase(neutron_test_base.BaseTestCase): class_map = {'address_scope': address_scope.AddressScopeRBAC, 'qos_policy': policy.QosPolicyRBAC, 'network': network.NetworkRBAC, - 'security_group': securitygroup.SecurityGroupRBAC} + 'security_group': securitygroup.SecurityGroupRBAC, + 'subnetpool': subnetpool.SubnetPoolRBAC} self.assertEqual(class_map, rbac.RBACBaseObject.get_type_class_map()) diff --git a/neutron/tests/unit/objects/test_subnetpool.py b/neutron/tests/unit/objects/test_subnetpool.py index cd1662bca36..89f0bd47bdc 100644 --- a/neutron/tests/unit/objects/test_subnetpool.py +++ b/neutron/tests/unit/objects/test_subnetpool.py @@ -12,20 +12,29 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import mock from neutron_lib import constants +from neutron_lib.db import model_query from oslo_utils import uuidutils +from neutron.extensions import rbac as ext_rbac +from neutron.objects.db import api as obj_db_api from neutron.objects import subnetpool 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 class SubnetPoolTestMixin(object): - def _create_test_subnetpool(self): + def _create_test_subnetpool(self, snp_id=None): + + if not snp_id: + snp_id = uuidutils.generate_uuid() + obj = subnetpool.SubnetPool( self.context, - id=uuidutils.generate_uuid(), + id=snp_id, ip_version=constants.IP_VERSION_4, default_prefixlen=24, min_prefixlen=0, @@ -70,6 +79,95 @@ class SubnetPoolDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, # values fo this. To be reworked in follow-up patch. pass + @mock.patch.object(model_query, 'query_with_hooks') + @mock.patch.object(obj_db_api, 'get_object') + def test_rbac_policy_create_no_address_scope(self, mock_get_object, + mock_query_with_hooks): + context = mock.Mock(is_admin=False, tenant_id='db_obj_owner_id') + payload = mock.Mock( + context=context, request_body=dict(object_id="fake_id") + ) + mock_get_object.return_value = dict(address_scope_id=None) + + subnetpool.SubnetPool.validate_rbac_policy_create( + None, None, None, payload=payload + ) + + mock_query_with_hooks.assert_not_called() + + def _validate_rbac_filter_mock(self, filter_mock, project_id, + address_scope_id): + filter_mock.assert_called_once() + self.assertEqual( + "addressscoperbacs.target_tenant IN ('*', '%(project_id)s') " + "AND addressscoperbacs.object_id = '%(address_scope_id)s'" % { + "project_id": project_id, + "address_scope_id": address_scope_id, + }, + filter_mock.call_args[0][0].compile( + compile_kwargs={"literal_binds": True} + ).string + ) + + @mock.patch.object(model_query, 'query_with_hooks') + @mock.patch.object(obj_db_api, 'get_object') + def test_rbac_policy_create_no_matching_policies(self, mock_get_object, + mock_query_with_hooks): + context = mock.Mock(is_admin=False, tenant_id='db_obj_owner_id') + fake_project_id = "fake_target_tenant_id" + payload = mock.Mock( + context=context, request_body=dict( + object_id="fake_id", + target_tenant=fake_project_id + ) + ) + fake_address_scope_id = "fake_as_id" + mock_get_object.return_value = dict( + address_scope_id=fake_address_scope_id + ) + filter_mock = mock.Mock( + return_value=mock.Mock(count=mock.Mock(return_value=0)) + ) + mock_query_with_hooks.return_value = mock.Mock(filter=filter_mock) + + self.assertRaises( + ext_rbac.RbacPolicyInitError, + subnetpool.SubnetPool.validate_rbac_policy_create, + resource=None, event=None, trigger=None, + payload=payload + ) + + self._validate_rbac_filter_mock( + filter_mock, fake_project_id, fake_address_scope_id + ) + + @mock.patch.object(model_query, 'query_with_hooks') + @mock.patch.object(obj_db_api, 'get_object') + def test_rbac_policy_create_valid(self, mock_get_object, + mock_query_with_hooks): + context = mock.Mock(is_admin=False, tenant_id='db_obj_owner_id') + fake_project_id = "fake_target_tenant_id" + payload = mock.Mock( + context=context, request_body=dict( + object_id="fake_id", + target_tenant=fake_project_id + ) + ) + fake_address_scope_id = "fake_as_id" + mock_get_object.return_value = dict( + address_scope_id=fake_address_scope_id + ) + filter_mock = mock.Mock(count=1) + mock_query_with_hooks.return_value = mock.Mock(filter=filter_mock) + + subnetpool.SubnetPool.validate_rbac_policy_create( + None, None, None, payload=payload + ) + + self._validate_rbac_filter_mock( + filter_mock, fake_project_id, fake_address_scope_id + ) + class SubnetPoolPrefixIfaceObjectTestCase( obj_test_base.BaseObjectIfaceTestCase): @@ -88,3 +186,25 @@ class SubnetPoolPrefixDbObjectTestCase( super(SubnetPoolPrefixDbObjectTestCase, self).setUp() self.update_obj_fields( {'subnetpool_id': lambda: self._create_test_subnetpool().id}) + + +class SubnetPoolRBACDbObjectTestCase(test_rbac.TestRBACObjectMixin, + obj_test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase, + SubnetPoolTestMixin): + + _test_class = subnetpool.SubnetPoolRBAC + + def setUp(self): + super(SubnetPoolRBACDbObjectTestCase, self).setUp() + for obj in self.db_objs: + self._create_test_subnetpool(obj['object_id']) + + def _create_test_subnetpool_rbac(self): + self.objs[0].create() + return self.objs[0] + + +class SubnetPoolRBACIfaceObjectTestCase(test_rbac.TestRBACObjectMixin, + obj_test_base.BaseObjectIfaceTestCase): + _test_class = subnetpool.SubnetPoolRBAC diff --git a/releasenotes/notes/add-subnetpool-rbac-2eb2008bd1b27b11.yaml b/releasenotes/notes/add-subnetpool-rbac-2eb2008bd1b27b11.yaml new file mode 100644 index 00000000000..751f5f32d80 --- /dev/null +++ b/releasenotes/notes/add-subnetpool-rbac-2eb2008bd1b27b11.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Subnetpool is now supported via the network RBAC mechanism. + Please refer to the admin guide for further details.