# Copyright (c) 2015 Mirantis, Inc. # 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 unittest import mock from neutron_lib.api import attributes from neutron_lib.callbacks import events from neutron_lib import context from neutron_lib.db import constants as db_const from neutron_lib.plugins import directory from oslo_config import cfg from oslo_policy import policy as oslo_policy from oslo_serialization import jsonutils from neutron.db.quota import driver as quota_driver from neutron import manager from neutron.pecan_wsgi.controllers import resource from neutron import policy from neutron.tests.functional.pecan_wsgi import test_functional class TestOwnershipHook(test_functional.PecanFunctionalTest): def test_network_ownership_check(self): net_response = self.app.post_json( '/v2.0/networks.json', params={'network': {'name': 'meh'}}, headers={'X-Project-Id': 'tenid'}) network_id = jsonutils.loads(net_response.body)['network']['id'] port_response = self.app.post_json( '/v2.0/ports.json', params={'port': {'network_id': network_id, 'admin_state_up': True}}, headers={'X-Project-Id': 'tenid'}) self.assertEqual(201, port_response.status_int) class TestQueryParametersHook(test_functional.PecanFunctionalTest): def test_if_match_on_update(self): net_response = jsonutils.loads(self.app.post_json( '/v2.0/networks.json', params={'network': {'name': 'meh'}}, headers={'X-Project-Id': 'tenid'}).body) network_id = net_response['network']['id'] response = self.app.put_json('/v2.0/networks/%s.json' % network_id, params={'network': {'name': 'cat'}}, headers={'X-Project-Id': 'tenid', 'If-Match': 'revision_number=0'}, expect_errors=True) # revision plugin not supported by default, so badrequest self.assertEqual(400, response.status_int) class TestQueryParametersHookWithRevision(test_functional.PecanFunctionalTest): def setUp(self): cfg.CONF.set_override('service_plugins', ['revisions']) super(TestQueryParametersHookWithRevision, self).setUp() def test_if_match_on_update(self): net_response = jsonutils.loads(self.app.post_json( '/v2.0/networks.json', params={'network': {'name': 'meh'}}, headers={'X-Project-Id': 'tenid'}).body) network_id = net_response['network']['id'] rev = net_response['network']['revision_number'] stale = rev - 1 response = self.app.put_json( '/v2.0/networks/%s.json' % network_id, params={'network': {'name': 'cat'}}, headers={'X-Project-Id': 'tenid', 'If-Match': 'revision_number=%s' % stale}, expect_errors=True) self.assertEqual(412, response.status_int) self.app.put_json('/v2.0/networks/%s.json' % network_id, params={'network': {'name': 'cat'}}, headers={'X-Project-Id': 'tenid', 'If-Match': 'revision_number=%s' % rev}) class TestQuotaEnforcementHook(test_functional.PecanFunctionalTest): def test_quota_enforcement_single(self): ctx = context.get_admin_context() quota_driver.DbQuotaDriver.update_quota_limit( ctx, 'tenid', 'network', 1) # There is enough headroom for creating a network response = self.app.post_json( '/v2.0/networks.json', params={'network': {'name': 'meh'}}, headers={'X-Project-Id': 'tenid'}) self.assertEqual(response.status_int, 201) # But a second request will fail response = self.app.post_json( '/v2.0/networks.json', params={'network': {'name': 'meh-2'}}, headers={'X-Project-Id': 'tenid'}, expect_errors=True) self.assertEqual(response.status_int, 409) def test_quota_enforcement_bulk_request(self): ctx = context.get_admin_context() quota_driver.DbQuotaDriver.update_quota_limit( ctx, 'tenid', 'network', 3) # There is enough headroom for a bulk request creating 2 networks response = self.app.post_json( '/v2.0/networks.json', params={'networks': [ {'name': 'meh1'}, {'name': 'meh2'}]}, headers={'X-Project-Id': 'tenid'}) self.assertEqual(response.status_int, 201) # But it won't be possible to create 2 more networks... response = self.app.post_json( '/v2.0/networks.json', params={'networks': [ {'name': 'meh3'}, {'name': 'meh4'}]}, headers={'X-Project-Id': 'tenid'}, expect_errors=True) self.assertEqual(response.status_int, 409) class TestPolicyEnforcementHook(test_functional.PecanFunctionalTest): FAKE_RESOURCE = { 'mehs': { 'id': {'allow_post': False, 'allow_put': False, 'is_visible': True, 'primary_key': True}, 'attr': {'allow_post': True, 'allow_put': True, 'is_visible': True, 'default': ''}, 'restricted_attr': {'allow_post': True, 'allow_put': True, 'is_visible': True, 'default': ''}, 'tenant_id': {'allow_post': True, 'allow_put': False, 'required_by_policy': True, 'validate': {'type:string': db_const.PROJECT_ID_FIELD_SIZE}, 'is_visible': True} } } def setUp(self): # Create a controller for a fake resource. This will make the tests # independent from the evolution of the API (so if one changes the API # or the default policies there won't be any risk of breaking these # tests, or at least I hope so) super(TestPolicyEnforcementHook, self).setUp() self.mock_plugin = mock.Mock() attributes.RESOURCES.update(self.FAKE_RESOURCE) manager.NeutronManager.set_plugin_for_resource('mehs', self.mock_plugin) fake_controller = resource.CollectionsController('mehs', 'meh') manager.NeutronManager.set_controller_for_resource( 'mehs', fake_controller) # Inject policies for the fake resource policy.init() policy._ENFORCER.set_rules( oslo_policy.Rules.from_dict( {'create_meh': '', 'update_meh': 'rule:admin_only', 'delete_meh': 'rule:admin_only', 'get_meh': 'rule:admin_only or field:mehs:id=xxx', 'get_meh:restricted_attr': 'rule:admin_only'}), overwrite=False) def test_before_on_create_authorized(self): # Mock a return value for an hypothetical create operation self.mock_plugin.create_meh.return_value = { 'id': 'xxx', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'} response = self.app.post_json('/v2.0/mehs.json', params={'meh': {'attr': 'meh'}}, headers={'X-Project-Id': 'tenid'}) # We expect this operation to succeed self.assertEqual(201, response.status_int) self.assertEqual(0, self.mock_plugin.get_meh.call_count) self.assertEqual(1, self.mock_plugin.create_meh.call_count) def test_before_on_put_not_authorized(self): # The policy hook here should load the resource, and therefore we must # mock a get response self.mock_plugin.get_meh.return_value = { 'id': 'xxx', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'} # The policy engine should trigger an exception in 'before', and the # plugin method should not be called at all response = self.app.put_json('/v2.0/mehs/xxx.json', params={'meh': {'attr': 'meh'}}, headers={'X-Project-Id': 'tenid'}, expect_errors=True) self.assertEqual(403, response.status_int) self.assertEqual(1, self.mock_plugin.get_meh.call_count) self.assertEqual(0, self.mock_plugin.update_meh.call_count) def test_before_on_put_not_found_when_not_authorized_to_get(self): # the user won't even have permission to view this resource # so the error on unauthorized updates should be translated into # a 404 self.mock_plugin.get_meh.return_value = { 'id': 'yyy', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'} response = self.app.put_json('/v2.0/mehs/yyy.json', params={'meh': {'attr': 'meh'}}, headers={'X-Project-Id': 'tenid'}, expect_errors=True) self.assertEqual(404, response.status_int) self.assertEqual(1, self.mock_plugin.get_meh.call_count) self.assertEqual(0, self.mock_plugin.update_meh.call_count) def test_before_on_delete_not_authorized(self): # The policy hook here should load the resource, and therefore we must # mock a get response self.mock_plugin.delete_meh.return_value = None self.mock_plugin.get_meh.return_value = { 'id': 'xxx', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'} # The policy engine should trigger an exception in 'before', and the # plugin method should not be called response = self.app.delete_json('/v2.0/mehs/xxx.json', headers={'X-Project-Id': 'tenid'}, expect_errors=True) self.assertEqual(403, response.status_int) self.assertEqual(1, self.mock_plugin.get_meh.call_count) self.assertEqual(0, self.mock_plugin.delete_meh.call_count) def test_after_on_get_not_found(self): # The GET test policy will deny access to anything whose id is not # 'xxx', so the following request should be forbidden and presented # to the user as an HTTPNotFound self.mock_plugin.get_meh.return_value = { 'id': 'yyy', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'} # The policy engine should trigger an exception in 'after', and the # plugin method should be called response = self.app.get('/v2.0/mehs/yyy.json', headers={'X-Project-Id': 'tenid'}, expect_errors=True) self.assertEqual(404, response.status_int) self.assertEqual(1, self.mock_plugin.get_meh.call_count) def test_after_on_get_excludes_admin_attribute(self): self.mock_plugin.get_meh.return_value = { 'id': 'xxx', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'} response = self.app.get('/v2.0/mehs/xxx.json', headers={'X-Project-Id': 'tenid'}) self.assertEqual(200, response.status_int) json_response = jsonutils.loads(response.body) self.assertNotIn('restricted_attr', json_response['meh']) def test_after_on_list_excludes_admin_attribute(self): self.mock_plugin.get_mehs.return_value = [{ 'id': 'xxx', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'}] response = self.app.get('/v2.0/mehs', headers={'X-Project-Id': 'tenid'}) self.assertEqual(200, response.status_int) json_response = jsonutils.loads(response.body) self.assertNotIn('restricted_attr', json_response['mehs'][0]) def test_after_inits_policy(self): self.mock_plugin.get_mehs.return_value = [{ 'id': 'xxx', 'attr': 'meh', 'restricted_attr': '', 'tenant_id': 'tenid'}] policy.reset() response = self.app.get('/v2.0/mehs', headers={'X-Project-Id': 'tenid'}) self.assertEqual(200, response.status_int) class TestMetricsNotifierHook(test_functional.PecanFunctionalTest): def setUp(self): patcher = mock.patch('neutron.pecan_wsgi.hooks.notifier.NotifierHook.' '_notifier') self.mock_notifier = patcher.start().info super(TestMetricsNotifierHook, self).setUp() def test_post_put_delete_triggers_notification(self): req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'} payload = {'network': {'name': 'meh'}} response = self.app.post_json( '/v2.0/networks.json', params=payload, headers=req_headers) self.assertEqual(201, response.status_int) json_body = jsonutils.loads(response.body) self.assertEqual( [mock.call(mock.ANY, 'network.create.start', payload), mock.call(mock.ANY, 'network.create.end', json_body)], self.mock_notifier.mock_calls) self.mock_notifier.reset_mock() network_id = json_body['network']['id'] payload = {'network': {'name': 'meh-2'}} response = self.app.put_json( '/v2.0/networks/%s.json' % network_id, params=payload, headers=req_headers) self.assertEqual(200, response.status_int) json_body = jsonutils.loads(response.body) # id should be in payload sent to notifier payload['id'] = network_id self.assertEqual( [mock.call(mock.ANY, 'network.update.start', payload), mock.call(mock.ANY, 'network.update.end', json_body)], self.mock_notifier.mock_calls) self.mock_notifier.reset_mock() before_payload = {'network_id': network_id} after_payload = before_payload.copy() after_payload['network'] = directory.get_plugin().get_network( context.get_admin_context(), network_id) response = self.app.delete( '/v2.0/networks/%s.json' % network_id, headers=req_headers) self.assertEqual(204, response.status_int) self.assertEqual( [mock.call(mock.ANY, 'network.delete.start', before_payload), mock.call(mock.ANY, 'network.delete.end', after_payload)], self.mock_notifier.mock_calls) def test_bulk_create_triggers_notification(self): req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'} payload = {'networks': [{'name': 'meh_1'}, {'name': 'meh_2'}]} response = self.app.post_json( '/v2.0/networks.json', params=payload, headers=req_headers) self.assertEqual(201, response.status_int) json_body = jsonutils.loads(response.body) self.assertEqual(2, self.mock_notifier.call_count) self.mock_notifier.assert_has_calls( [mock.call(mock.ANY, 'network.create.start', payload), mock.call(mock.ANY, 'network.create.end', json_body)]) def test_bad_create_doesnt_emit_end(self): req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'} payload = {'network': {'name': 'meh'}} plugin = directory.get_plugin() with mock.patch.object(plugin, 'create_network', side_effect=ValueError): response = self.app.post_json( '/v2.0/networks.json', params=payload, headers=req_headers, expect_errors=True) self.assertEqual(500, response.status_int) self.assertEqual( [mock.call(mock.ANY, 'network.create.start', mock.ANY)], self.mock_notifier.mock_calls) def test_bad_update_doesnt_emit_end(self): req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'} payload = {'network': {'name': 'meh'}} response = self.app.post_json( '/v2.0/networks.json', params=payload, headers=req_headers, expect_errors=True) self.assertEqual(201, response.status_int) json_body = jsonutils.loads(response.body) self.mock_notifier.reset_mock() plugin = directory.get_plugin() with mock.patch.object(plugin, 'update_network', side_effect=ValueError): response = self.app.put_json( '/v2.0/networks/%s.json' % json_body['network']['id'], params=payload, headers=req_headers, expect_errors=True) self.assertEqual(500, response.status_int) self.assertEqual( [mock.call(mock.ANY, 'network.update.start', mock.ANY)], self.mock_notifier.mock_calls) def test_bad_delete_doesnt_emit_end(self): req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'} payload = {'network': {'name': 'meh'}} response = self.app.post_json( '/v2.0/networks.json', params=payload, headers=req_headers, expect_errors=True) self.assertEqual(201, response.status_int) json_body = jsonutils.loads(response.body) self.mock_notifier.reset_mock() plugin = directory.get_plugin() with mock.patch.object(plugin, 'delete_network', side_effect=ValueError): response = self.app.delete( '/v2.0/networks/%s.json' % json_body['network']['id'], headers=req_headers, expect_errors=True) self.assertEqual(500, response.status_int) self.assertEqual( [mock.call(mock.ANY, 'network.delete.start', mock.ANY)], self.mock_notifier.mock_calls) class TestCallbackRegistryNotifier(test_functional.PecanFunctionalTest): def setUp(self): super(TestCallbackRegistryNotifier, self).setUp() patcher = mock.patch('neutron.pecan_wsgi.hooks.notifier.registry') self.mock_notifier = patcher.start().publish def _create(self, bulk=False): if bulk: body = {'networks': [{'name': 'meh-1'}, {'name': 'meh-2'}]} else: body = {'network': {'name': 'meh-1'}} response = self.app.post_json( '/v2.0/networks.json', params=body, headers={'X-Project-Id': 'tenid'}) return response.json def test_create(self): self._create() self.mock_notifier.assert_called_once_with( 'network', events.BEFORE_RESPONSE, mock.ANY, payload=mock.ANY) payload = self.mock_notifier.call_args[1]['payload'] self.assertEqual('network.create.end', payload.method_name) self.assertEqual('create_network', payload.action) self.assertEqual('networks', payload.collection_name) actual = payload.latest_state self.assertEqual('meh-1', actual['network']['name']) def test_create_bulk(self): self._create(bulk=True) self.mock_notifier.assert_called_once_with( 'network', events.BEFORE_RESPONSE, mock.ANY, payload=mock.ANY) payload = self.mock_notifier.call_args[1]['payload'] self.assertEqual('network.create.end', payload.method_name) self.assertEqual('create_network', payload.action) self.assertEqual('networks', payload.collection_name) actual = payload.latest_state self.assertEqual(2, len(actual['networks'])) self.assertEqual('meh-1', actual['networks'][0]['name']) self.assertEqual('meh-2', actual['networks'][1]['name']) def test_update(self): network_id = self._create()['network']['id'] self.mock_notifier.reset_mock() self.app.put_json('/v2.0/networks/%s.json' % network_id, params={'network': {'name': 'new-meh'}}, headers={'X-Project-Id': 'tenid'}) self.mock_notifier.assert_called_once_with( 'network', events.BEFORE_RESPONSE, mock.ANY, payload=mock.ANY) payload = self.mock_notifier.call_args[1]['payload'] self.assertEqual('network.update.end', payload.method_name) self.assertEqual('update_network', payload.action) self.assertEqual('networks', payload.collection_name) actual_new = payload.latest_state self.assertEqual('new-meh', actual_new['network']['name']) actual_original = payload.states[0] self.assertEqual(network_id, actual_original['id']) def test_delete(self): network_id = self._create()['network']['id'] self.mock_notifier.reset_mock() self.app.delete( '/v2.0/networks/%s.json' % network_id, headers={'X-Project-Id': 'tenid'}) self.mock_notifier.assert_called_once_with( 'network', events.BEFORE_RESPONSE, mock.ANY, payload=mock.ANY) payload = self.mock_notifier.call_args[1]['payload'] self.assertEqual('network.delete.end', payload.method_name) self.assertEqual('delete_network', payload.action) self.assertEqual('networks', payload.collection_name) actual = payload.latest_state self.assertEqual(network_id, actual['network']['id'])