Merge "Allow sharing of subnet pools via RBAC mechanism"

This commit is contained in:
Zuul 2020-04-12 17:20:24 +00:00 committed by Gerrit Code Review
commit b5e96c49bf
20 changed files with 440 additions and 40 deletions

View File

@ -19,6 +19,7 @@ is supported by:
* Attaching router gateways to networks (since Mitaka). * Attaching router gateways to networks (since Mitaka).
* 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).
Sharing an object with specific projects 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 This process can be repeated any number of times to share an address scope
with an arbitrary number of projects. 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 How the 'shared' flag relates to these entries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As introduced in other guide entries, neutron provides a means of As introduced in other guide entries, neutron provides a means of
making an object (``address-scope``, ``network``, ``qos-policy``, 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: This is accomplished using the ``shared`` flag on the supported object:
.. code-block:: console .. code-block:: console

View File

@ -207,12 +207,13 @@ class DbBasePluginCommon(object):
'max_prefixlen': max_prefixlen, 'max_prefixlen': max_prefixlen,
'is_default': subnetpool['is_default'], 'is_default': subnetpool['is_default'],
'shared': subnetpool['shared'], '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'], 'ip_version': subnetpool['ip_version'],
'default_quota': subnetpool['default_quota'], 'default_quota': subnetpool['default_quota'],
'address_scope_id': subnetpool['address_scope_id']} 'address_scope_id': subnetpool['address_scope_id']}
resource_extend.apply_funcs( 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) return db_utils.resource_fields(res, fields)
def _make_port_dict(self, port, fields=None, def _make_port_dict(self, port, fields=None,

View File

@ -90,7 +90,7 @@ def _check_subnet_not_used(context, subnet_id):
def _update_subnetpool_dict(orig_pool, new_pool): def _update_subnetpool_dict(orig_pool, new_pool):
updated = dict((k, v) for k, v in orig_pool.to_dict().items() 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_pool = new_pool.copy()
new_prefixes = new_pool.pop('prefixes', constants.ATTR_NOT_SPECIFIED) new_prefixes = new_pool.pop('prefixes', constants.ATTR_NOT_SPECIFIED)
@ -1253,7 +1253,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
subnetpool = subnetpool_obj.SubnetPool(context, **pool_args) subnetpool = subnetpool_obj.SubnetPool(context, **pool_args)
subnetpool.create() subnetpool.create()
return self._make_subnetpool_dict(subnetpool.db_obj) return self._make_subnetpool_dict(subnetpool)
@db_api.retry_if_session_inactive() @db_api.retry_if_session_inactive()
def update_subnetpool(self, context, id, subnetpool): def update_subnetpool(self, context, id, subnetpool):
@ -1296,7 +1296,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
@db_api.retry_if_session_inactive() @db_api.retry_if_session_inactive()
def get_subnetpool(self, context, id, fields=None): def get_subnetpool(self, context, id, fields=None):
subnetpool = self._get_subnetpool(context, id) 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() @db_api.retry_if_session_inactive()
def get_subnetpools(self, context, filters=None, fields=None, def get_subnetpools(self, context, filters=None, fields=None,
@ -1307,7 +1307,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
subnetpools = subnetpool_obj.SubnetPool.get_objects( subnetpools = subnetpool_obj.SubnetPool.get_objects(
context, _pager=pager, validate_filters=False, **filters) context, _pager=pager, validate_filters=False, **filters)
return [ return [
self._make_subnetpool_dict(pool.db_obj, fields) self._make_subnetpool_dict(pool, fields)
for pool in subnetpools for pool in subnetpools
] ]

View File

@ -1 +1 @@
e4e236b0e1ff e88badaa9591

View File

@ -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

View File

@ -235,7 +235,15 @@ class SubnetPool(standard_attr.HasStandardAttributes, model_base.BASEV2,
default_prefixlen = sa.Column(sa.Integer, nullable=False) default_prefixlen = sa.Column(sa.Integer, nullable=False)
min_prefixlen = sa.Column(sa.Integer, nullable=False) min_prefixlen = sa.Column(sa.Integer, nullable=False)
max_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, is_default = sa.Column(sa.Boolean, nullable=False,
server_default=sql.false()) server_default=sql.false())
default_quota = sa.Column(sa.Integer, nullable=True) default_quota = sa.Column(sa.Integer, nullable=True)
@ -245,6 +253,10 @@ class SubnetPool(standard_attr.HasStandardAttributes, model_base.BASEV2,
backref='subnetpools', backref='subnetpools',
cascade='all, delete, delete-orphan', cascade='all, delete, delete-orphan',
lazy='subquery') 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] api_collections = [subnetpool_def.COLLECTION_NAME]
collection_resource_map = {subnetpool_def.COLLECTION_NAME: collection_resource_map = {subnetpool_def.COLLECTION_NAME:
subnetpool_def.RESOURCE_NAME} subnetpool_def.RESOURCE_NAME}

View File

@ -137,3 +137,14 @@ class AddressScopeRBAC(RBACColumns, model_base.BASEV2):
@staticmethod @staticmethod
def get_valid_actions(): def get_valid_actions():
return (ACCESS_SHARED,) 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,)

View File

@ -39,6 +39,11 @@ class DuplicateRbacPolicy(n_exc.Conflict):
message = _("An RBAC policy already exists with those values.") 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): def convert_valid_object_type(otype):
normalized = otype.strip().lower() normalized = otype.strip().lower()
if normalized in rbac_db_models.get_type_model_map(): if normalized in rbac_db_models.get_type_model_map():

View File

@ -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

View File

@ -14,7 +14,6 @@
from neutron_lib.objects import common_types from neutron_lib.objects import common_types
from oslo_versionedobjects import fields as obj_fields from oslo_versionedobjects import fields as obj_fields
import sqlalchemy as sa
from neutron.db.models import address_scope as models from neutron.db.models import address_scope as models
from neutron.db import models_v2 from neutron.db import models_v2
@ -32,22 +31,6 @@ class AddressScopeRBAC(rbac.RBACBaseObject):
db_model = rbac_db_models.AddressScopeRBAC 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 @base.NeutronObjectRegistry.register
class AddressScope(rbac_db.NeutronRbacObject): class AddressScope(rbac_db.NeutronRbacObject):

View File

@ -20,7 +20,6 @@ from oslo_versionedobjects import fields as obj_fields
from six import add_metaclass from six import add_metaclass
from sqlalchemy import and_ from sqlalchemy import and_
from neutron.db import rbac_db_models as models
from neutron.objects import base from neutron.objects import base
@ -45,13 +44,12 @@ class RBACBaseObject(base.NeutronDbObject):
target_tenant=None): target_tenant=None):
clauses = [] clauses = []
if object_id: if object_id:
clauses.append(models.NetworkRBAC.object_id == object_id) clauses.append(cls.db_model.object_id == object_id)
if action: if action:
clauses.append(models.NetworkRBAC.action == action) clauses.append(cls.db_model.action == action)
if target_tenant: if target_tenant:
clauses.append(models.NetworkRBAC.target_tenant == clauses.append(cls.db_model.target_tenant == target_tenant)
target_tenant) query = context.session.query(cls.db_model.target_tenant)
query = context.session.query(models.NetworkRBAC.target_tenant)
if clauses: if clauses:
query = query.filter(and_(*clauses)) query = query.filter(and_(*clauses))
return [data[0] for data in query] return [data[0] for data in query]

View File

@ -156,6 +156,13 @@ class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin,
obj_id=policy['object_id'], obj_id=policy['object_id'],
target_tenant=target_tenant) 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 @classmethod
def validate_rbac_policy_update(cls, resource, event, trigger, def validate_rbac_policy_update(cls, resource, event, trigger,
payload=None): payload=None):
@ -201,7 +208,8 @@ class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin,
msg = _("Only admins can manipulate policies on objects " msg = _("Only admins can manipulate policies on objects "
"they do not own") "they do not own")
raise exceptions.InvalidInput(error_message=msg) 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} events.BEFORE_DELETE: cls.validate_rbac_policy_delete}
if event in callback_map: if event in callback_map:
return callback_map[event](resource, event, trigger, return callback_map[event](resource, event, trigger,

View File

@ -14,18 +14,39 @@
# under the License. # under the License.
import netaddr import netaddr
from neutron_lib.db import model_query
from neutron_lib.objects import common_types from neutron_lib.objects import common_types
from oslo_versionedobjects import fields as obj_fields 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 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 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 @base.NeutronObjectRegistry.register
class SubnetPool(base.NeutronDbObject): class SubnetPoolRBAC(rbac.RBACBaseObject):
# Version 1.0: Initial version # Version 1.0: Initial version
VERSION = '1.0' 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 db_model = models.SubnetPool
fields = { fields = {
@ -83,6 +104,46 @@ class SubnetPool(base.NeutronDbObject):
if 'prefixes' in fields: if 'prefixes' in fields:
self._attach_prefixes(fields['prefixes']) 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 @base.NeutronObjectRegistry.register
class SubnetPoolPrefix(base.NeutronDbObject): class SubnetPoolPrefix(base.NeutronDbObject):

View File

@ -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 provider_net
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 security_groups_port_filtering 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 stateful_security_group
from neutron_lib.api.definitions import subnet as subnet_def from neutron_lib.api.definitions import subnet as subnet_def
@ -184,6 +185,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
"quotas", "security-group", "quotas", "security-group",
rbac_address_scope.ALIAS, rbac_address_scope.ALIAS,
rbac_sg_apidef.ALIAS, rbac_sg_apidef.ALIAS,
rbac_subnetpool.ALIAS,
agent_apidef.ALIAS, agent_apidef.ALIAS,
dhcpagentscheduler.ALIAS, dhcpagentscheduler.ALIAS,
multiprovidernet.ALIAS, multiprovidernet.ALIAS,

View File

@ -47,6 +47,7 @@ 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-scope""
NETWORK_API_EXTENSIONS+=",rbac-security-groups"" 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

@ -6767,7 +6767,7 @@ class DbModelTenantTestCase(DbModelMixin, testlib_api.SqlTestCase):
with db_api.CONTEXT_WRITER.using(ctx): with db_api.CONTEXT_WRITER.using(ctx):
subnetpool = models_v2.SubnetPool( subnetpool = models_v2.SubnetPool(
ip_version=constants.IP_VERSION_4, default_prefixlen=4, 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', default_quota=4, address_scope_id='f', tenant_id='dbcheck',
is_default=False is_default=False
) )
@ -6807,7 +6807,7 @@ class DbModelProjectTestCase(DbModelMixin, testlib_api.SqlTestCase):
with db_api.CONTEXT_WRITER.using(ctx): with db_api.CONTEXT_WRITER.using(ctx):
subnetpool = models_v2.SubnetPool( subnetpool = models_v2.SubnetPool(
ip_version=constants.IP_VERSION_4, default_prefixlen=4, 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', default_quota=4, address_scope_id='f', project_id='dbcheck',
is_default=False is_default=False
) )

View File

@ -108,8 +108,9 @@ object_data = {
'StandardAttribute': '1.0-617d4f46524c4ce734a6fc1cc0ac6a0b', 'StandardAttribute': '1.0-617d4f46524c4ce734a6fc1cc0ac6a0b',
'Subnet': '1.1-5b7e1789a1732259d1e28b4bd87eb1c2', 'Subnet': '1.1-5b7e1789a1732259d1e28b4bd87eb1c2',
'SubnetDNSPublishFixedIP': '1.0-db22af6fa20b143986f0cbe06cbfe0ea', 'SubnetDNSPublishFixedIP': '1.0-db22af6fa20b143986f0cbe06cbfe0ea',
'SubnetPool': '1.0-a0e03895d1a6e7b9d4ab7b0ca13c3867', 'SubnetPool': '1.1-a0e03895d1a6e7b9d4ab7b0ca13c3867',
'SubnetPoolPrefix': '1.0-13c15144135eb869faa4a76dc3ee3b6c', 'SubnetPoolPrefix': '1.0-13c15144135eb869faa4a76dc3ee3b6c',
'SubnetPoolRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d',
'SubnetServiceType': '1.0-05ae4cdb2a9026a697b143926a1add8c', 'SubnetServiceType': '1.0-05ae4cdb2a9026a697b143926a1add8c',
'SubPort': '1.0-72c8471068db1f0491b5480fe49b52bb', 'SubPort': '1.0-72c8471068db1f0491b5480fe49b52bb',
'Tag': '1.0-1a0d20379920ffa3cebfd3e016d2f7a0', 'Tag': '1.0-1a0d20379920ffa3cebfd3e016d2f7a0',

View File

@ -18,6 +18,7 @@ from neutron.objects import network
from neutron.objects.qos import policy from neutron.objects.qos import policy
from neutron.objects import rbac from neutron.objects import rbac
from neutron.objects import securitygroup from neutron.objects import securitygroup
from neutron.objects import subnetpool
from neutron.tests import base as neutron_test_base from neutron.tests import base as neutron_test_base
from neutron.tests.unit.objects import 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, class_map = {'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,
'subnetpool': subnetpool.SubnetPoolRBAC}
self.assertEqual(class_map, rbac.RBACBaseObject.get_type_class_map()) self.assertEqual(class_map, rbac.RBACBaseObject.get_type_class_map())

View File

@ -12,20 +12,29 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from neutron_lib import constants from neutron_lib import constants
from neutron_lib.db import model_query
from oslo_utils import uuidutils 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.objects import subnetpool
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
class SubnetPoolTestMixin(object): 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( obj = subnetpool.SubnetPool(
self.context, self.context,
id=uuidutils.generate_uuid(), id=snp_id,
ip_version=constants.IP_VERSION_4, ip_version=constants.IP_VERSION_4,
default_prefixlen=24, default_prefixlen=24,
min_prefixlen=0, min_prefixlen=0,
@ -70,6 +79,95 @@ class SubnetPoolDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
# values fo this. To be reworked in follow-up patch. # values fo this. To be reworked in follow-up patch.
pass 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( class SubnetPoolPrefixIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase): obj_test_base.BaseObjectIfaceTestCase):
@ -88,3 +186,25 @@ class SubnetPoolPrefixDbObjectTestCase(
super(SubnetPoolPrefixDbObjectTestCase, self).setUp() super(SubnetPoolPrefixDbObjectTestCase, self).setUp()
self.update_obj_fields( self.update_obj_fields(
{'subnetpool_id': lambda: self._create_test_subnetpool().id}) {'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

View File

@ -0,0 +1,5 @@
---
features:
- |
Subnetpool is now supported via the network RBAC mechanism.
Please refer to the admin guide for further details.