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
This commit is contained in:
parent
eb6104c0ac
commit
56b971bb42
doc/source/admin
neutron
db
db_base_plugin_common.pydb_base_plugin_v2.py
migration/alembic_migrations/versions
models_v2.pyrbac_db_models.pyextensions
objects
plugins/ml2
tests
contrib/hooks
unit
releasenotes/notes
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
]
|
||||
|
||||
|
@ -1 +1 @@
|
||||
e4e236b0e1ff
|
||||
e88badaa9591
|
||||
|
81
neutron/db/migration/alembic_migrations/versions/ussuri/expand/e88badaa9591_add_rbac_support_for_subnetpool.py
Normal file
81
neutron/db/migration/alembic_migrations/versions/ussuri/expand/e88badaa9591_add_rbac_support_for_subnetpool.py
Normal 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
|
@ -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}
|
||||
|
@ -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,)
|
||||
|
@ -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():
|
||||
|
22
neutron/extensions/rbac_subnetpool.py
Normal file
22
neutron/extensions/rbac_subnetpool.py
Normal 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
|
@ -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):
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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',
|
||||
|
@ -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())
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Subnetpool is now supported via the network RBAC mechanism.
|
||||
Please refer to the admin guide for further details.
|
Loading…
x
Reference in New Issue
Block a user