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:
Rick Aulino 2016-07-28 20:55:42 -06:00 committed by Rick Aulino
parent 01e60afd91
commit 7fb2e41527
11 changed files with 305 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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