consoleauth: Store access_url on token authorization

Related-bug: 1409142

As part of the fix for the related bug - we've added protocol checking
to mitigate MITM attacks, however we base protocol checking on a config
option that is normally only intended for compute hosts.

This is quite user hostile, as it is now important that all nodes
running compute and proxy services have this option in sync.

We can do better than that - we can persist the URL the client is
expected to use, and once we get it back on token validation, we can
make sure that the request is using the intended protocol, mitigating
the MITM injected script attacks.

This patch makes sure that the access_url is persisted with the token -
the follow-up patch makes consoles use that info.

Change-Id: I02a377f54de46536ca35413b615d3298967afc33
This commit is contained in:
Nikola Dipanov 2015-04-01 14:35:13 +01:00
parent bf70df295b
commit 2ffcf18d00
6 changed files with 69 additions and 28 deletions

View File

@ -2858,7 +2858,8 @@ class API(base.Base):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type,
connect_info['host'], connect_info['port'],
connect_info['internal_access_path'], instance.uuid)
connect_info['internal_access_path'], instance.uuid,
access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@ -2878,7 +2879,8 @@ class API(base.Base):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type,
connect_info['host'], connect_info['port'],
connect_info['internal_access_path'], instance.uuid)
connect_info['internal_access_path'], instance.uuid,
access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@ -2898,7 +2900,8 @@ class API(base.Base):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type,
connect_info['host'], connect_info['port'],
connect_info['internal_access_path'], instance.uuid)
connect_info['internal_access_path'], instance.uuid,
access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@ -2919,7 +2922,8 @@ class API(base.Base):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type,
connect_info['host'], connect_info['port'],
connect_info['internal_access_path'], instance.uuid)
connect_info['internal_access_path'], instance.uuid,
access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@check_instance_host

View File

@ -337,7 +337,7 @@ class ComputeCellsAPI(compute_api.API):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type, connect_info['host'],
connect_info['port'], connect_info['internal_access_path'],
instance.uuid)
instance.uuid, access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@wrap_check_policy
@ -353,7 +353,7 @@ class ComputeCellsAPI(compute_api.API):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type, connect_info['host'],
connect_info['port'], connect_info['internal_access_path'],
instance.uuid)
instance.uuid, access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@wrap_check_policy
@ -369,7 +369,7 @@ class ComputeCellsAPI(compute_api.API):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type, connect_info['host'],
connect_info['port'], connect_info['internal_access_path'],
instance.uuid)
instance.uuid, access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@wrap_check_policy
@ -385,7 +385,7 @@ class ComputeCellsAPI(compute_api.API):
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type, connect_info['host'],
connect_info['port'], connect_info['internal_access_path'],
instance.uuid)
instance.uuid, access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
@check_instance_cell

View File

@ -47,7 +47,7 @@ CONF.import_opt('enable', 'nova.cells.opts', group='cells')
class ConsoleAuthManager(manager.Manager):
"""Manages token based authentication."""
target = messaging.Target(version='2.0')
target = messaging.Target(version='2.1')
def __init__(self, scheduler_driver=None, *args, **kwargs):
super(ConsoleAuthManager, self).__init__(service_name='consoleauth',
@ -65,7 +65,8 @@ class ConsoleAuthManager(manager.Manager):
return tokens
def authorize_console(self, context, token, console_type, host, port,
internal_access_path, instance_uuid):
internal_access_path, instance_uuid,
access_url=None):
token_dict = {'token': token,
'instance_uuid': instance_uuid,
@ -73,6 +74,7 @@ class ConsoleAuthManager(manager.Manager):
'host': host,
'port': port,
'internal_access_path': internal_access_path,
'access_url': access_url,
'last_activity_at': time.time()}
data = jsonutils.dumps(token_dict)

View File

@ -47,6 +47,8 @@ class ConsoleAuthAPI(object):
... Icehouse and Juno support message version 2.0. So, any changes to
existing methods in 2.x after that point should be done such that they
can handle the version_cap being set to 2.0.
* 2.1 - Added access_url to authorize_console
'''
VERSION_ALIASES = {
@ -58,22 +60,28 @@ class ConsoleAuthAPI(object):
def __init__(self):
super(ConsoleAuthAPI, self).__init__()
target = messaging.Target(topic=CONF.consoleauth_topic, version='2.0')
target = messaging.Target(topic=CONF.consoleauth_topic, version='2.1')
version_cap = self.VERSION_ALIASES.get(CONF.upgrade_levels.consoleauth,
CONF.upgrade_levels.consoleauth)
self.client = rpc.get_client(target, version_cap=version_cap)
def authorize_console(self, ctxt, token, console_type, host, port,
internal_access_path, instance_uuid):
internal_access_path, instance_uuid,
access_url):
# The remote side doesn't return anything, but we want to block
# until it completes.'
cctxt = self.client.prepare()
return cctxt.call(ctxt,
'authorize_console',
token=token, console_type=console_type,
host=host, port=port,
internal_access_path=internal_access_path,
instance_uuid=instance_uuid)
msg_args = dict(token=token, console_type=console_type,
host=host, port=port,
internal_access_path=internal_access_path,
instance_uuid=instance_uuid,
access_url=access_url)
version = '2.1'
if not self.client.can_send_version('2.1'):
version = '2.0'
del msg_args['access_url']
cctxt = self.client.prepare(version=version)
return cctxt.call(ctxt, 'authorize_console', **msg_args)
def check_token(self, ctxt, token):
cctxt = self.client.prepare()

View File

@ -8979,7 +8979,8 @@ class ComputeAPITestCase(BaseTestCase):
'authorize_console')
self.compute_api.consoleauth_rpcapi.authorize_console(
self.context, 'fake_token', fake_console_type, 'fake_console_host',
'fake_console_port', 'fake_access_path', 'fake_uuid')
'fake_console_port', 'fake_access_path', 'fake_uuid',
access_url='fake_console_url')
self.mox.ReplayAll()
@ -9018,7 +9019,8 @@ class ComputeAPITestCase(BaseTestCase):
'authorize_console')
self.compute_api.consoleauth_rpcapi.authorize_console(
self.context, 'fake_token', fake_console_type, 'fake_console_host',
'fake_console_port', 'fake_access_path', 'fake_uuid')
'fake_console_port', 'fake_access_path', 'fake_uuid',
access_url='fake_console_url')
self.mox.ReplayAll()
@ -9057,7 +9059,8 @@ class ComputeAPITestCase(BaseTestCase):
'authorize_console')
self.compute_api.consoleauth_rpcapi.authorize_console(
self.context, 'fake_token', fake_console_type, 'fake_console_host',
'fake_console_port', 'fake_access_path', 'fake_uuid')
'fake_console_port', 'fake_access_path', 'fake_uuid',
access_url='fake_console_url')
self.mox.ReplayAll()
@ -9097,7 +9100,8 @@ class ComputeAPITestCase(BaseTestCase):
self.compute_api.consoleauth_rpcapi.authorize_console(
self.context, 'fake_token', fake_console_type,
'fake_serial_host', 'fake_tcp_port',
'fake_access_path', 'fake_uuid')
'fake_access_path', 'fake_uuid',
access_url='fake_access_url')
console = self.compute_api.get_serial_console(self.context,
fake_instance,

View File

@ -20,6 +20,7 @@ import contextlib
import mock
from oslo_config import cfg
import six
from nova.consoleauth import rpcapi as consoleauth_rpcapi
from nova import context
@ -29,6 +30,8 @@ CONF = cfg.CONF
class ConsoleAuthRpcAPITestCase(test.NoDBTestCase):
DROPPED_ARG = object()
def _test_consoleauth_api(self, method, **kwargs):
do_cast = kwargs.pop('_do_cast', False)
@ -40,6 +43,10 @@ class ConsoleAuthRpcAPITestCase(test.NoDBTestCase):
orig_prepare = rpcapi.client.prepare
version = kwargs.pop('version', None)
rpc_kwargs = {k: v for k, v in six.iteritems(kwargs)
if v is not self.DROPPED_ARG}
with contextlib.nested(
mock.patch.object(rpcapi.client, 'cast' if do_cast else 'call'),
mock.patch.object(rpcapi.client, 'prepare'),
@ -49,19 +56,35 @@ class ConsoleAuthRpcAPITestCase(test.NoDBTestCase):
):
prepare_mock.return_value = rpcapi.client
rpc_mock.return_value = None if do_cast else 'foo'
csv_mock.side_effect = (
lambda v: orig_prepare().can_send_version())
def fake_csv(v):
if version:
return orig_prepare(
version_cap=version).can_send_version(version=v)
else:
return orig_prepare().can_send_version()
csv_mock.side_effect = fake_csv
retval = getattr(rpcapi, method)(ctxt, **kwargs)
self.assertEqual(retval, rpc_mock.return_value)
prepare_mock.assert_called_once_with()
rpc_mock.assert_called_once_with(ctxt, method, **kwargs)
if version:
prepare_mock.assert_called_once_with(version=version)
else:
prepare_mock.assert_called_once_with()
rpc_mock.assert_called_once_with(ctxt, method, **rpc_kwargs)
def test_authorize_console(self):
self._test_consoleauth_api('authorize_console', token='token',
console_type='ctype', host='h', port='p',
internal_access_path='iap', instance_uuid="instance")
internal_access_path='iap', instance_uuid="instance",
access_url=self.DROPPED_ARG, version='2.0')
def test_authorize_console_access_url(self):
self._test_consoleauth_api('authorize_console', token='token',
console_type='ctype', host='h', port='p',
internal_access_path='iap', instance_uuid="instance",
access_url="fake_access_url", version='2.1')
def test_check_token(self):
self._test_consoleauth_api('check_token', token='t')