Merge "Allow sharing of subnet pools via RBAC mechanism"
This commit is contained in:
commit
b5e96c49bf
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
e4e236b0e1ff
|
e88badaa9591
|
||||||
|
|
|
@ -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)
|
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}
|
||||||
|
|
|
@ -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,)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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 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):
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Subnetpool is now supported via the network RBAC mechanism.
|
||||||
|
Please refer to the admin guide for further details.
|
Loading…
Reference in New Issue