RBAC for network sharing
Adds support for the new Newton Neutron RBAC
policy functionality. This allows networks to be
shared across tenants.
For more information, please take a look at:
https://developer.rackspace.com/blog/A-First-Look-at-RBAC-in-the-Liberty-Release-of-Neutron/
Change-Id: Ie433dbd2554648bad7ca9f6940069c85258b03c7
Implements: blueprint neutron-tenant-rbac
(cherry picked from commit aef7fed140
)
This commit is contained in:
parent
01e60afd91
commit
7fb2e41527
|
@ -154,6 +154,12 @@ above will be ignored when indexing.
|
|||
Release Notes
|
||||
=============
|
||||
|
||||
1.0.0.0 (Newton)
|
||||
----------------
|
||||
|
||||
The Neutron tenant RBAC policy functionality is supported as
|
||||
part of the OS::Neutron::Net resource type.
|
||||
|
||||
0.2.0.0 (Mitaka)
|
||||
----------------
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
prelude: >
|
||||
Add support for the new Neutron tenant RBAC
|
||||
policy functionality. This allows networks
|
||||
to be shared across tenants.
|
||||
features:
|
||||
- Add support for the new Neutron tenant RBAC
|
||||
policy functionality. This allows networks
|
||||
to be shared across tenants.
|
|
@ -417,6 +417,28 @@ class IndexingHelper(object):
|
|||
return self._index_alias_multiple_indexes_get(
|
||||
doc_id=doc_id, routing=routing)
|
||||
|
||||
def get_docs_by_nested_field(self, path, field, value, version=False):
|
||||
"""Query ElasticSearch based on a nested field. The caller will
|
||||
need to specify the path of the nested field as well as the
|
||||
field itself. We will include the 'version' field if commanded
|
||||
as such by the caller.
|
||||
"""
|
||||
# Set up query for accessing a nested field.
|
||||
nested_field = path + "." + field
|
||||
body = {"query": {"nested": {
|
||||
"path": path, "query": {"term": {nested_field: value}}}}}
|
||||
if version:
|
||||
body['version'] = True
|
||||
try:
|
||||
return self.engine.search(index=self.alias_name,
|
||||
doc_type=self.document_type,
|
||||
body=body, ignore_unavailable=True)
|
||||
except Exception as exc:
|
||||
LOG.warning(_LW(
|
||||
'Error querying %(p)s %(f)s. Error %(exc)s') %
|
||||
{'p': path, 'f': field, 'exc': exc})
|
||||
raise
|
||||
|
||||
def update_document(self, document, doc_id, update_as_script,
|
||||
expected_version=None):
|
||||
"""Updates are a little simpler than inserts because the documents
|
||||
|
|
|
@ -18,11 +18,30 @@ import copy
|
|||
from searchlight.elasticsearch.plugins import utils
|
||||
|
||||
|
||||
def add_rbac(network, target_tenant, policy_id):
|
||||
"""Update a network based on an RBAC policy.
|
||||
"""
|
||||
# Add target_tenant to members list.
|
||||
members_list = network['members']
|
||||
if target_tenant not in members_list:
|
||||
members_list.append(target_tenant)
|
||||
|
||||
# Add RBAC policy.
|
||||
rbac_policy = network['rbac_policy']
|
||||
policy = {'rbac_id': policy_id, 'target_tenant': target_tenant}
|
||||
rbac_policy.append(policy)
|
||||
network['rbac_policy'] = rbac_policy
|
||||
|
||||
|
||||
def serialize_network(network):
|
||||
serialized = copy.deepcopy(network)
|
||||
# Remove subnets because we index them separately
|
||||
serialized.pop('subnets')
|
||||
serialized['project_id'] = serialized['tenant_id']
|
||||
if 'members' not in serialized:
|
||||
serialized['members'] = []
|
||||
if 'rbac_policy' not in serialized:
|
||||
serialized['rbac_policy'] = []
|
||||
return serialized
|
||||
|
||||
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from collections import defaultdict
|
||||
from searchlight.common import resource_types
|
||||
from searchlight.elasticsearch.plugins import base
|
||||
from searchlight.elasticsearch.plugins.neutron import add_rbac
|
||||
from searchlight.elasticsearch.plugins.neutron import notification_handlers
|
||||
from searchlight.elasticsearch.plugins.neutron import serialize_network
|
||||
from searchlight.elasticsearch.plugins import openstack_clients
|
||||
|
@ -25,6 +27,9 @@ class NetworkIndex(base.IndexBase):
|
|||
|
||||
ADMIN_ONLY_FIELDS = ['provider:*']
|
||||
|
||||
def __init__(self):
|
||||
super(NetworkIndex, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def get_document_type(self):
|
||||
return resource_types.NEUTRON_NETWORK
|
||||
|
@ -63,6 +68,14 @@ class NetworkIndex(base.IndexBase):
|
|||
'shared': {'type': 'boolean'},
|
||||
'status': {'type': 'string', 'index': 'not_analyzed'},
|
||||
'tenant_id': {'type': 'string', 'index': 'not_analyzed'},
|
||||
'members': {'type': 'string', 'index': 'not_analyzed'},
|
||||
'rbac_policy': {
|
||||
'type': 'nested',
|
||||
'properties': {
|
||||
'rbac_id': {'type': 'string', 'index': 'not_analyzed'},
|
||||
'tenant': {'type': 'string', 'index': 'not_analyzed'}
|
||||
}
|
||||
},
|
||||
'updated_at': {'type': 'date'}
|
||||
},
|
||||
"_meta": {
|
||||
|
@ -110,18 +123,48 @@ class NetworkIndex(base.IndexBase):
|
|||
'bool': {
|
||||
'should': [
|
||||
{'term': {'tenant_id': request_context.owner}},
|
||||
{'terms': {'members': [request_context.owner, '*']}},
|
||||
{'term': {'router:external': True}},
|
||||
{'term': {'shared': True}}
|
||||
]
|
||||
}
|
||||
}]
|
||||
|
||||
def get_rbac_policies(self):
|
||||
policies = defaultdict(list)
|
||||
policy_list = self.get_rbac_objects()
|
||||
for policy in policy_list:
|
||||
policies[policy['object_id']].append(policy)
|
||||
return policies
|
||||
|
||||
def get_objects(self):
|
||||
"""Generator that lists all networks owned by all tenants."""
|
||||
# Neutronclient handles pagination itself; list_networks is a generator
|
||||
policies = self.get_rbac_policies()
|
||||
neutron_client = openstack_clients.get_neutronclient()
|
||||
for network in neutron_client.list_networks()['networks']:
|
||||
network['members'] = []
|
||||
network['rbac_policy'] = []
|
||||
for policy in policies[network['id']]:
|
||||
add_rbac(network, policy['target_tenant'], policy['id'])
|
||||
yield network
|
||||
|
||||
def serialize(self, network):
|
||||
return serialize_network(network)
|
||||
|
||||
def filter_result(self, hit, request_context):
|
||||
# The mapping contains internal fields related to RBAC policy.
|
||||
# Remove them.
|
||||
source = hit['_source']
|
||||
source.pop('rbac_policy', None)
|
||||
source.pop('members', None)
|
||||
|
||||
def get_rbac_objects(self):
|
||||
"""Generator that lists all RBAC policies for all tenants."""
|
||||
valid_actions = notification_handlers.RBAC_VALID_ACTIONS
|
||||
neutron_client = openstack_clients.get_neutronclient()
|
||||
policies = neutron_client.list_rbac_policies()['rbac_policies']
|
||||
for policy in [p for p in policies if
|
||||
p['object_type'] == 'network' and
|
||||
p['action'] in valid_actions]:
|
||||
yield policy
|
||||
|
|
|
@ -17,6 +17,8 @@ from oslo_log import log as logging
|
|||
|
||||
from elasticsearch import helpers
|
||||
from searchlight.elasticsearch.plugins import base
|
||||
from searchlight.elasticsearch.plugins.helper import USER_ID_SUFFIX
|
||||
from searchlight.elasticsearch.plugins.neutron import add_rbac
|
||||
from searchlight.elasticsearch.plugins.neutron import serialize_floatingip
|
||||
from searchlight.elasticsearch.plugins.neutron import serialize_network
|
||||
from searchlight.elasticsearch.plugins.neutron import serialize_port
|
||||
|
@ -26,12 +28,12 @@ from searchlight.elasticsearch.plugins.neutron import serialize_subnet
|
|||
from searchlight.elasticsearch.plugins import openstack_clients
|
||||
from searchlight.elasticsearch.plugins import utils
|
||||
|
||||
import searchlight.elasticsearch
|
||||
from searchlight.i18n import _LE, _LW, _LI
|
||||
from searchlight.i18n import _LE, _LI
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
SECGROUP_RETRIES = 20
|
||||
RBAC_VALID_ACTIONS = ["access_as_shared", "access_as_external"]
|
||||
|
||||
|
||||
class NetworkHandler(base.NotificationBase):
|
||||
|
@ -43,7 +45,9 @@ class NetworkHandler(base.NotificationBase):
|
|||
return {
|
||||
'network.create.end': self.create_or_update,
|
||||
'network.update.end': self.create_or_update,
|
||||
'network.delete.end': self.delete
|
||||
'network.delete.end': self.delete,
|
||||
'rbac_policy.create.end': self.rbac_create,
|
||||
'rbac_policy.delete.end': self.rbac_delete
|
||||
}
|
||||
|
||||
def get_log_fields(self, event_type, payload):
|
||||
|
@ -52,6 +56,15 @@ class NetworkHandler(base.NotificationBase):
|
|||
return ('id', payload['network'].get('id')),
|
||||
elif 'network_id' in payload:
|
||||
return ('id', payload['network_id']),
|
||||
elif 'rbac_policy' in payload:
|
||||
return (
|
||||
('network_id', payload['rbac_policy'].get('object_id')),
|
||||
('target_tenant',
|
||||
payload['rbac_policy'].get('target_tenant')),
|
||||
('object_type',
|
||||
payload['rbac_policy'].get('object_type')))
|
||||
elif 'rbac_policy_id' in payload:
|
||||
return ('rbac_policy_id', payload['rbac_policy_id']),
|
||||
return ()
|
||||
|
||||
def create_or_update(self, payload, timestamp):
|
||||
|
@ -77,6 +90,91 @@ class NetworkHandler(base.NotificationBase):
|
|||
'from index. Error: %(exc)s') %
|
||||
{'network_id': network_id, 'exc': exc})
|
||||
|
||||
def rbac_create(self, payload, timestamp):
|
||||
"""RBAC policy is making a network visible to users in a specfic
|
||||
tenant. Previously this network was not visible to users in that
|
||||
tenant. We will want to add this tenant to the members list.
|
||||
Also add the RBAC policy.
|
||||
"""
|
||||
valid_types = ["network"]
|
||||
|
||||
event_type = payload['rbac_policy']['object_type']
|
||||
action = payload['rbac_policy']['action']
|
||||
if action not in RBAC_VALID_ACTIONS or event_type not in valid_types:
|
||||
# I'm bored. Nothing that concerns nor interests us.
|
||||
return
|
||||
|
||||
network_id = payload['rbac_policy']['object_id']
|
||||
target_tenant = payload['rbac_policy']['target_tenant']
|
||||
policy_id = payload['rbac_policy']['id']
|
||||
LOG.debug("Adding RBAC policy for network %s with tenant %s",
|
||||
network_id, target_tenant)
|
||||
|
||||
# Read, modify, write an existing network document. Grab and modify
|
||||
# the admin version of the document. When saving the document it will
|
||||
# be indexed for both admin and user.
|
||||
doc = self.index_helper.get_document(network_id, for_admin=True)
|
||||
|
||||
if not doc or not doc['_source']:
|
||||
LOG.error(_LE('Error adding rule to network. Network %(id)s '
|
||||
'does not exist.') % {'id': network_id})
|
||||
return
|
||||
|
||||
body = doc['_source']
|
||||
|
||||
# Update network with RBAC policy.
|
||||
add_rbac(body, target_tenant, policy_id)
|
||||
|
||||
# Bump version for race condition prevention. Use doc and not
|
||||
# body, since '_version' is outside of '_source'.
|
||||
version = doc['_version'] + 1
|
||||
self.index_helper.save_document(body, version=version)
|
||||
|
||||
def rbac_delete(self, payload, timestamp):
|
||||
"""RBAC policy is making a network invisible to users in specific
|
||||
tenant. Previously this network was visible to users in that
|
||||
tenant. We will remove this tenant from the members list.
|
||||
Also remove the RBAC policy.
|
||||
"""
|
||||
policy_id = payload['rbac_policy_id']
|
||||
|
||||
# Read, modify, write an existing network document. For both the
|
||||
# admin and user version of the document.
|
||||
|
||||
# Find all documents (admin and user) with the policy ID.
|
||||
docs = self.index_helper.get_docs_by_nested_field(
|
||||
"rbac_policy", "rbac_id", policy_id, version=True)
|
||||
|
||||
if not docs or not docs['hits']['hits']:
|
||||
return
|
||||
|
||||
for doc in docs['hits']['hits']:
|
||||
if doc['_id'].endswith(USER_ID_SUFFIX):
|
||||
# We only want to use the admin document.
|
||||
continue
|
||||
body = doc['_source']
|
||||
|
||||
target_tenant = None
|
||||
policies = body['rbac_policy']
|
||||
for p in policies:
|
||||
if p.get('rbac_id') == policy_id:
|
||||
target_tenant = p['target_tenant']
|
||||
|
||||
# Remove target_tenant from members list.
|
||||
members_list = (body['members'])
|
||||
if target_tenant in members_list:
|
||||
members_list.remove(target_tenant)
|
||||
body['members'] = members_list
|
||||
|
||||
# Remove RBAC policy.
|
||||
new_list = [p for p in policies if p.get('rbac_id') != policy_id]
|
||||
body['rbac_policy'] = new_list
|
||||
|
||||
# Bump version for race condition prevention. Use doc and not
|
||||
# body, since '_version' is outside of '_source'.
|
||||
version = doc['_version'] + 1
|
||||
self.index_helper.save_document(body, version=version)
|
||||
|
||||
|
||||
class PortHandler(base.NotificationBase):
|
||||
@classmethod
|
||||
|
@ -396,7 +494,7 @@ class SecurityGroupHandler(base.NotificationBase):
|
|||
# search for the document and save the document ID. This way we
|
||||
# do not need to search inside the loop. We will access the document
|
||||
# directly by the ID which will always return the latest version.
|
||||
orig_doc = self.get_doc_by_nested_field(
|
||||
orig_doc = self.index_helper.get_docs_by_nested_field(
|
||||
"security_group_rules", "id", rule_id, version=True)
|
||||
if not orig_doc:
|
||||
return
|
||||
|
@ -427,27 +525,3 @@ class SecurityGroupHandler(base.NotificationBase):
|
|||
if attempts == (SECGROUP_RETRIES - 1):
|
||||
LOG.error(_LE('Error deleting security group rule %(id)s:'
|
||||
' Too many retries') % {'id': rule_id})
|
||||
|
||||
def get_doc_by_nested_field(self, path, field, value, version=False):
|
||||
"""Query ElasticSearch based on a nested field. The caller will
|
||||
need to specify the path of the nested field as well as the
|
||||
field itself. We will include the 'version' field if commanded
|
||||
as such by the caller.
|
||||
"""
|
||||
es_engine = searchlight.elasticsearch.get_api()
|
||||
|
||||
# Set up query for accessing a nested field.
|
||||
nested_field = path + "." + field
|
||||
body = {"query": {"nested": {
|
||||
"path": path, "query": {"term": {nested_field: value}}}}}
|
||||
if version:
|
||||
body['version'] = True
|
||||
try:
|
||||
return es_engine.search(index=self.index_helper.alias_name,
|
||||
doc_type=self.index_helper.document_type,
|
||||
body=body, ignore_unavailable=True)
|
||||
except Exception as exc:
|
||||
LOG.warning(_LW(
|
||||
'Error querying %(p)s %(f)s. Error %(exc)s') %
|
||||
{'p': path, 'f': field, 'exc': exc})
|
||||
return {}
|
||||
|
|
|
@ -466,6 +466,10 @@ class FunctionalTest(test_utils.BaseTestCase):
|
|||
"""
|
||||
with mock.patch.object(plugin, 'get_objects', return_value=docs):
|
||||
with mock.patch.object(plugin, 'child_plugins', return_value=[]):
|
||||
if hasattr(plugin, 'get_rbac_objects'):
|
||||
rbac_mock = mock.patch.object(plugin, 'get_rbac_objects',
|
||||
return_value={})
|
||||
rbac_mock.start()
|
||||
plugin.index_initial_data()
|
||||
|
||||
if refresh_index:
|
||||
|
|
|
@ -147,5 +147,82 @@
|
|||
"timestamp": "2016-03-21 17:09:23.286750",
|
||||
"message_id": "159da540-d312-4c80-ba14-4a982bc82905"
|
||||
}
|
||||
},
|
||||
"rbac_policy.create.end": {
|
||||
"event_type": "rbac_policy.create.end",
|
||||
"payload": {
|
||||
"rbac_policy": {
|
||||
"target_tenant": "c4b424b17cc04cefa7211b40c5c893c2",
|
||||
"tenant_id": "c4b424b17cc04cefa7211b40c5c893c2",
|
||||
"object_type": "network",
|
||||
"object_id": "7a95c19d-9abc-4651-b491-f2345994e956",
|
||||
"action": "access_as_shared",
|
||||
"id": "d7491be9-ee3d-40d7-9880-0ce82c7c12f6"
|
||||
}
|
||||
},
|
||||
"publisher_id": "network.devstack",
|
||||
"ctxt": {
|
||||
"read_only": false,
|
||||
"domain": null,
|
||||
"project_name": "admin",
|
||||
"user_id": "b52c5f3792714829a69f7d8b976076be",
|
||||
"show_deleted": false,
|
||||
"roles": [
|
||||
"admin"
|
||||
],
|
||||
"user_identity": "b52c5f3792714829a69f7d8b976076be 5fe7c4e4e492490393c674089a178e19 - - -",
|
||||
"project_domain": null,
|
||||
"tenant_name": "admin",
|
||||
"auth_token": "b9f19e8a373240dc81d20b66c986174c",
|
||||
"resource_uuid": null,
|
||||
"project_id": "c4b424b17cc04cefa7211b40c5c893c2",
|
||||
"tenant_id": "c4b424b17cc04cefa7211b40c5c893c2",
|
||||
"is_admin": true,
|
||||
"user": "b52c5f3792714829a69f7d8b976076be",
|
||||
"request_id": "req-b9619bd2-f62f-4d88-88a7-41a1ad1f6df7",
|
||||
"user_domain": null,
|
||||
"timestamp": "2016-03-21 17:09:22.952733",
|
||||
"tenant": "c4b424b17cc04cefa7211b40c5c893c2",
|
||||
"user_name": "admin"
|
||||
},
|
||||
"metadata": {
|
||||
"timestamp": "2016-03-21 17:09:23.286750",
|
||||
"message_id": "159da540-d312-4c80-ba14-4a982bc82905"
|
||||
}
|
||||
},
|
||||
"rbac_policy.delete.end": {
|
||||
"event_type": "rbac_policy.delete.end",
|
||||
"payload": {
|
||||
"rbac_policy_id": "d7491be9-ee3d-40d7-9880-0ce82c7c12f6"
|
||||
},
|
||||
"publisher_id": "network.devstack",
|
||||
"ctxt": {
|
||||
"read_only": false,
|
||||
"domain": null,
|
||||
"project_name": "admin",
|
||||
"user_id": "b52c5f3792714829a69f7d8b976076be",
|
||||
"show_deleted": false,
|
||||
"roles": [
|
||||
"admin"
|
||||
],
|
||||
"user_identity": "b52c5f3792714829a69f7d8b976076be 5fe7c4e4e492490393c674089a178e19 - - -",
|
||||
"project_domain": null,
|
||||
"tenant_name": "admin",
|
||||
"auth_token": "b9f19e8a373240dc81d20b66c986174c",
|
||||
"resource_uuid": null,
|
||||
"project_id": "5fe7c4e4e492490393c674089a178e19",
|
||||
"tenant_id": "5fe7c4e4e492490393c674089a178e19",
|
||||
"is_admin": true,
|
||||
"user": "b52c5f3792714829a69f7d8b976076be",
|
||||
"request_id": "req-b9619bd2-f62f-4d88-88a7-41a1ad1f6df7",
|
||||
"user_domain": null,
|
||||
"timestamp": "2016-03-21 17:09:22.952733",
|
||||
"tenant": "5fe7c4e4e492490393c674089a178e19",
|
||||
"user_name": "admin"
|
||||
},
|
||||
"metadata": {
|
||||
"timestamp": "2016-03-21 17:09:23.286750",
|
||||
"message_id": "159da540-d312-4c80-ba14-4a982bc82905"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -327,6 +327,24 @@ class TestNeutronListeners(test_listener.TestSearchListenerBase):
|
|||
self._verify_result(update_event, verification_keys, result,
|
||||
inner_key='network')
|
||||
|
||||
rbac_create_event = self.network_events['rbac_policy.create.end']
|
||||
self._send_event_to_listener(rbac_create_event, self.listener_alias)
|
||||
result = self._verify_event_processing(rbac_create_event,
|
||||
owner=EV_TENANT)
|
||||
cbody = result['hits']['hits'][0]['_source']
|
||||
# Verify internal states are not visible.
|
||||
self.assertNotIn('members', cbody)
|
||||
self.assertNotIn('rbac_policy', cbody)
|
||||
|
||||
rbac_delete_event = self.network_events['rbac_policy.delete.end']
|
||||
self._send_event_to_listener(rbac_delete_event, self.listener_alias)
|
||||
result = self._verify_event_processing(rbac_delete_event,
|
||||
owner=EV_TENANT)
|
||||
dbody = result['hits']['hits'][0]['_source']
|
||||
# Verify internal states are not visible.
|
||||
self.assertNotIn('members', dbody)
|
||||
self.assertNotIn('rbac_policy', dbody)
|
||||
|
||||
delete_event = self.network_events['network.delete.end']
|
||||
self._send_event_to_listener(delete_event, self.listener_alias)
|
||||
self._verify_event_processing(delete_event, count=0,
|
||||
|
|
|
@ -78,6 +78,8 @@ class TestNetworkLoaderPlugin(test_utils.BaseTestCase):
|
|||
self.assertEqual(_now_str, serialized['updated_at'])
|
||||
# project id should get copied from tenant_id
|
||||
self.assertEqual(TENANT1, serialized['project_id'])
|
||||
self.assertEqual([], serialized['members'])
|
||||
self.assertEqual([], serialized['rbac_policy'])
|
||||
|
||||
def test_rbac_filter(self):
|
||||
fake_request = unit_test_utils.get_fake_request(
|
||||
|
@ -88,6 +90,7 @@ class TestNetworkLoaderPlugin(test_utils.BaseTestCase):
|
|||
'bool': {
|
||||
'should': [
|
||||
{'term': {'tenant_id': TENANT1}},
|
||||
{'terms': {'members': [TENANT1, '*']}},
|
||||
{'term': {'router:external': True}},
|
||||
{'term': {'shared': True}}
|
||||
]
|
||||
|
|
|
@ -159,7 +159,8 @@ class TestSecurityGroupLoaderPlugin(test_utils.BaseTestCase):
|
|||
'_version': 1}]}}
|
||||
|
||||
handler = self.plugin.get_notification_handler()
|
||||
with mock.patch.object(handler, 'get_doc_by_nested_field') as mo_nest:
|
||||
with mock.patch.object(self.plugin.index_helper,
|
||||
'get_docs_by_nested_field') as mo_nest:
|
||||
with mock.patch.object(self.plugin.index_helper,
|
||||
'get_document') as mock_get:
|
||||
with mock.patch.object(self.plugin.index_helper,
|
||||
|
|
Loading…
Reference in New Issue