Merge "Assert URL consistency for agent_url"
This commit is contained in:
commit
8086167054
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
from http import client as http_client
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
@ -225,6 +226,20 @@ class HeartbeatController(rest.RestController):
|
||||
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
|
||||
dii = rpc_node['driver_internal_info']
|
||||
agent_url = dii.get('agent_url')
|
||||
try:
|
||||
# NOTE(TheJulia): Use of urllib.urlparse is not a security
|
||||
# guard, but detecting oddities and incorrect formatting
|
||||
# https://docs.python.org/3/library/urllib.parse.html#url-parsing-security # noqa
|
||||
parsed_url = urlparse.urlparse(callback_url)
|
||||
# Check if http (compatibility), or https (much newer agents)
|
||||
if 'http' not in parsed_url.scheme:
|
||||
raise ValueError
|
||||
callback_url = parsed_url.geturl()
|
||||
except ValueError:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('An issue with the supplied "callback_url" has been '
|
||||
'detected.'))
|
||||
|
||||
# If we have an agent_url on file, and we get something different
|
||||
# we should fail because this is unexpected behavior of the agent.
|
||||
if agent_url is not None and agent_url != callback_url:
|
||||
|
@ -204,7 +204,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
def test_old_api_version(self):
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % uuidutils.generate_uuid(),
|
||||
{'callback_url': 'url'},
|
||||
{'callback_url': 'https://url'},
|
||||
headers={api_base.Version.string: str(api_v1.min_version())},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||
@ -212,7 +212,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
def test_node_not_found(self):
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % uuidutils.generate_uuid(),
|
||||
{'callback_url': 'url'},
|
||||
{'callback_url': 'https://url'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||
@ -222,14 +222,14 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'x'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None, 'x',
|
||||
None, None, None,
|
||||
node.uuid, 'https://url', None,
|
||||
'x', None, None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
@ -238,14 +238,14 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s.json' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'maybe some magic'},
|
||||
headers=headers)
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
'maybe some magic',
|
||||
node.uuid, 'https://url',
|
||||
None, 'maybe some magic',
|
||||
None, None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@ -254,13 +254,13 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context, name='test.1')
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.name,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'token'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
node.uuid, 'https://url', None,
|
||||
'token', None, None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@ -269,14 +269,15 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_version': '1.4.1',
|
||||
'agent_token': 'meow'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', '1.4.1',
|
||||
node.uuid, 'https://url',
|
||||
'1.4.1',
|
||||
'meow',
|
||||
None, None, None,
|
||||
topic='test-topic')
|
||||
@ -286,20 +287,31 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_version': '1.4.1'},
|
||||
headers={api_base.Version.string: '1.35'},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
def test_heartbeat_rejects_file_url(self, mock_heartbeat):
|
||||
node = obj_utils.create_test_node(
|
||||
self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'file:///path/to/the/wizzard'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
def test_heartbeat_rejects_different_callback_url(self, mock_heartbeat):
|
||||
node = obj_utils.create_test_node(
|
||||
self.context,
|
||||
driver_internal_info={'agent_url': 'url'})
|
||||
driver_internal_info={'agent_url': 'https://url'})
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url2'},
|
||||
{'callback_url': 'https://url2'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||
@ -309,13 +321,13 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'http://url',
|
||||
'agent_token': 'abcdef1'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
node.uuid, 'http://url', None,
|
||||
'abcdef1', None, None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@ -324,14 +336,14 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'meow',
|
||||
'agent_verify_ca': 'abcdef1'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
node.uuid, 'https://url', None,
|
||||
'meow', 'abcdef1', None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@ -340,7 +352,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'meow',
|
||||
'agent_status': 'start',
|
||||
'agent_status_message': 'woof',
|
||||
@ -349,7 +361,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
node.uuid, 'https://url', None,
|
||||
'meow', 'abcdef1', 'start',
|
||||
'woof', topic='test-topic')
|
||||
|
||||
@ -358,7 +370,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'meow',
|
||||
'agent_status': 'invalid_state',
|
||||
'agent_status_message': 'woof',
|
||||
@ -372,7 +384,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'meow',
|
||||
'agent_verify_ca': 'abcd'},
|
||||
headers={api_base.Version.string: '1.67'},
|
||||
@ -384,7 +396,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
{'callback_url': 'https://url',
|
||||
'agent_token': 'meow',
|
||||
'agent_verify_ca': 'abcd',
|
||||
'agent_status': 'wow',
|
||||
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds additional validation to the agent ``callback_url``.
|
||||
security:
|
||||
- |
|
||||
Additional validation of the ``callback_url`` which is supplied to Ironic
|
||||
by the agent has been added. In addition to any standardized formatting
|
||||
checks included in Python urllib, we will also reject requests which
|
||||
have an invalid URL schema formatting.
|
Loading…
Reference in New Issue
Block a user