From 499ed84bd70ce12658b546be360c82cd758a3caa Mon Sep 17 00:00:00 2001 From: Roman Podolyaka <rpodolyaka@mirantis.com> Date: Tue, 19 Mar 2013 10:45:54 +0200 Subject: [PATCH] Handle host side SSL certificates validation - add --os-cacert option which allows to set a file containing certificates of root CAs (certificate authorities) that are required for validation of HTTPS servers SSL certificates - wrap httplib2 SSL certificates validation errors with a custom quantumclient exception Blueprint: quantum-client-ssl Change-Id: I4e6a7d177ba14314ba9bed613ec2684bffc35222 --- neutronclient/client.py | 7 +- neutronclient/common/clientmanager.py | 7 +- neutronclient/common/exceptions.py | 4 + neutronclient/neutron/client.py | 3 +- neutronclient/shell.py | 11 ++- neutronclient/v2_0/client.py | 3 +- tests/unit/test_ssl.py | 137 ++++++++++++++++++++++++++ 7 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_ssl.py diff --git a/neutronclient/client.py b/neutronclient/client.py index fefa07b63..daea317fe 100644 --- a/neutronclient/client.py +++ b/neutronclient/client.py @@ -99,8 +99,9 @@ class HTTPClient(httplib2.Http): token=None, region_name=None, timeout=None, endpoint_url=None, insecure=False, endpoint_type='publicURL', - auth_strategy='keystone', **kwargs): - super(HTTPClient, self).__init__(timeout=timeout) + auth_strategy='keystone', ca_cert=None, **kwargs): + super(HTTPClient, self).__init__(timeout=timeout, ca_certs=ca_cert) + self.username = username self.tenant_name = tenant_name self.tenant_id = tenant_id @@ -134,6 +135,8 @@ class HTTPClient(httplib2.Http): utils.http_log_req(_logger, args, kargs) try: resp, body = self.request(*args, **kargs) + except httplib2.SSLHandshakeError as e: + raise exceptions.SslCertificateValidationError(reason=e) except Exception as e: # Wrap the low-level connection error (socket timeout, redirect # limit, decompression error, etc) into our custom high-level diff --git a/neutronclient/common/clientmanager.py b/neutronclient/common/clientmanager.py index f9e886e01..ca8962737 100644 --- a/neutronclient/common/clientmanager.py +++ b/neutronclient/common/clientmanager.py @@ -55,7 +55,8 @@ class ClientManager(object): region_name=None, api_version=None, auth_strategy=None, - insecure=False + insecure=False, + ca_cert=None, ): self._token = token self._url = url @@ -70,6 +71,7 @@ class ClientManager(object): self._service_catalog = None self._auth_strategy = auth_strategy self._insecure = insecure + self._ca_cert = ca_cert return def initialize(self): @@ -81,7 +83,8 @@ class ClientManager(object): region_name=self._region_name, auth_url=self._auth_url, endpoint_type=self._endpoint_type, - insecure=self._insecure) + insecure=self._insecure, + ca_cert=self._ca_cert) httpclient.authenticate() # Populate other password flow attributes self._token = httpclient.auth_token diff --git a/neutronclient/common/exceptions.py b/neutronclient/common/exceptions.py index c62d25335..c2b8b3cd3 100644 --- a/neutronclient/common/exceptions.py +++ b/neutronclient/common/exceptions.py @@ -172,3 +172,7 @@ class CommandError(Exception): class NeutronClientNoUniqueMatch(NeutronClientException): message = _("Multiple %(resource)s matches found for name '%(name)s'," " use an ID to be more specific.") + + +class SslCertificateValidationError(NeutronClientException): + message = _("SSL certificate validation has failed: %(reason)s") diff --git a/neutronclient/neutron/client.py b/neutronclient/neutron/client.py index daf04815a..2c87517b6 100644 --- a/neutronclient/neutron/client.py +++ b/neutronclient/neutron/client.py @@ -45,7 +45,8 @@ def make_client(instance): endpoint_url=url, token=instance._token, auth_strategy=instance._auth_strategy, - insecure=instance._insecure) + insecure=instance._insecure, + ca_cert=instance._ca_cert) return client else: raise exceptions.UnsupportedVersion("API version %s is not supported" % diff --git a/neutronclient/shell.py b/neutronclient/shell.py index 89c7adf0c..22cefbfd7 100644 --- a/neutronclient/shell.py +++ b/neutronclient/shell.py @@ -345,6 +345,14 @@ class NeutronShell(app.App): '--os_url', help=argparse.SUPPRESS) + parser.add_argument( + '--os-cacert', + metavar='<ca-certificate>', + default=env('OS_CACERT', default=None), + help="Specify a CA bundle file to use in " + "verifying a TLS (https) server certificate. " + "Defaults to env[OS_CACERT]") + parser.add_argument( '--insecure', action='store_true', @@ -516,7 +524,8 @@ class NeutronShell(app.App): api_version=self.api_version, auth_strategy=self.options.os_auth_strategy, endpoint_type=self.options.endpoint_type, - insecure=self.options.insecure, ) + insecure=self.options.insecure, + ca_cert=self.options.os_cacert, ) return def initialize_app(self, argv): diff --git a/neutronclient/v2_0/client.py b/neutronclient/v2_0/client.py index e2e1c792c..0f2d399ce 100644 --- a/neutronclient/v2_0/client.py +++ b/neutronclient/v2_0/client.py @@ -131,7 +131,8 @@ class Client(object): instantiation.(optional) :param integer timeout: Allows customization of the timeout for client http requests. (optional) - :param insecure: ssl certificate validation. (optional) + :param bool insecure: SSL certificate validation. (optional) + :param string ca_cert: SSL CA bundle file to use. (optional) Example:: diff --git a/tests/unit/test_ssl.py b/tests/unit/test_ssl.py new file mode 100644 index 000000000..0370ae657 --- /dev/null +++ b/tests/unit/test_ssl.py @@ -0,0 +1,137 @@ +# Copyright (C) 2013 OpenStack Foundation. +# 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. +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import fixtures +import httplib2 +import mox +import testtools + +from neutronclient.client import HTTPClient +from neutronclient.common.clientmanager import ClientManager +from neutronclient.common import exceptions +from neutronclient import shell as openstack_shell + + +AUTH_TOKEN = 'test_token' +END_URL = 'test_url' +METHOD = 'GET' +URL = 'http://test.test:1234/v2.0/' +CA_CERT = '/tmp/test/path' + + +class TestSSL(testtools.TestCase): + def setUp(self): + super(TestSSL, self).setUp() + + self.useFixture(fixtures.EnvironmentVariable('OS_TOKEN', AUTH_TOKEN)) + self.useFixture(fixtures.EnvironmentVariable('OS_URL', END_URL)) + + self.mox = mox.Mox() + self.addCleanup(self.mox.UnsetStubs) + + def test_ca_cert_passed(self): + self.mox.StubOutWithMock(ClientManager, '__init__') + self.mox.StubOutWithMock(openstack_shell.NeutronShell, 'interact') + + ClientManager.__init__( + ca_cert=CA_CERT, + # we are not really interested in other args + api_version=mox.IgnoreArg(), + auth_strategy=mox.IgnoreArg(), + auth_url=mox.IgnoreArg(), + endpoint_type=mox.IgnoreArg(), + insecure=mox.IgnoreArg(), + password=mox.IgnoreArg(), + region_name=mox.IgnoreArg(), + tenant_id=mox.IgnoreArg(), + tenant_name=mox.IgnoreArg(), + token=mox.IgnoreArg(), + url=mox.IgnoreArg(), + username=mox.IgnoreArg() + ) + openstack_shell.NeutronShell.interact().AndReturn(0) + self.mox.ReplayAll() + + openstack_shell.NeutronShell('2.0').run(['--os-cacert', CA_CERT]) + self.mox.VerifyAll() + + def test_ca_cert_passed_as_env_var(self): + self.useFixture(fixtures.EnvironmentVariable('OS_CACERT', CA_CERT)) + + self.mox.StubOutWithMock(ClientManager, '__init__') + self.mox.StubOutWithMock(openstack_shell.NeutronShell, 'interact') + + ClientManager.__init__( + ca_cert=CA_CERT, + # we are not really interested in other args + api_version=mox.IgnoreArg(), + auth_strategy=mox.IgnoreArg(), + auth_url=mox.IgnoreArg(), + endpoint_type=mox.IgnoreArg(), + insecure=mox.IgnoreArg(), + password=mox.IgnoreArg(), + region_name=mox.IgnoreArg(), + tenant_id=mox.IgnoreArg(), + tenant_name=mox.IgnoreArg(), + token=mox.IgnoreArg(), + url=mox.IgnoreArg(), + username=mox.IgnoreArg() + ) + openstack_shell.NeutronShell.interact().AndReturn(0) + self.mox.ReplayAll() + + openstack_shell.NeutronShell('2.0').run([]) + self.mox.VerifyAll() + + def test_client_manager_properly_creates_httpclient_instance(self): + self.mox.StubOutWithMock(HTTPClient, '__init__') + HTTPClient.__init__( + ca_cert=CA_CERT, + # we are not really interested in other args + auth_strategy=mox.IgnoreArg(), + auth_url=mox.IgnoreArg(), + endpoint_url=mox.IgnoreArg(), + insecure=mox.IgnoreArg(), + password=mox.IgnoreArg(), + region_name=mox.IgnoreArg(), + tenant_name=mox.IgnoreArg(), + token=mox.IgnoreArg(), + username=mox.IgnoreArg(), + ) + self.mox.ReplayAll() + + version = {'network': '2.0'} + ClientManager(ca_cert=CA_CERT, + api_version=version, + url=END_URL, + token=AUTH_TOKEN).neutron + self.mox.VerifyAll() + + def test_proper_exception_is_raised_when_cert_validation_fails(self): + http = HTTPClient(token=AUTH_TOKEN, endpoint_url=END_URL) + + self.mox.StubOutWithMock(httplib2.Http, 'request') + httplib2.Http.request( + URL, METHOD, headers=mox.IgnoreArg() + ).AndRaise(httplib2.SSLHandshakeError) + self.mox.ReplayAll() + + self.assertRaises( + exceptions.SslCertificateValidationError, + http._cs_request, + URL, METHOD + ) + self.mox.VerifyAll()