From 450f505c35f8762cca29d56b6e928490288ec166 Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Sun, 10 Apr 2016 23:18:17 +0200 Subject: Support client certificate/key This change enables to specify a client certificate/key with: * usual CLI options (--os-cert/--os-key) * usual environment variables ($OS_CERT/$OS_KEY) Closes-Bug: #1565112 Change-Id: I12e151adcb6084d801c6dfed21d82232a3259aea --- swiftclient/client.py | 39 ++++++++++++++++++++++++++++++++++++--- swiftclient/service.py | 4 ++++ swiftclient/shell.py | 12 ++++++++++++ tests/unit/test_shell.py | 4 +++- tests/unit/test_swiftclient.py | 39 +++++++++++++++++++++++++++++++++++++-- tests/unit/utils.py | 6 ++++++ 6 files changed, 98 insertions(+), 6 deletions(-) diff --git a/swiftclient/client.py b/swiftclient/client.py index 4dbbd49..0726f35 100644 --- a/swiftclient/client.py +++ b/swiftclient/client.py @@ -322,7 +322,8 @@ class _RetryBody(_ObjectBody): class HTTPConnection(object): def __init__(self, url, proxy=None, cacert=None, insecure=False, - ssl_compression=False, default_user_agent=None, timeout=None): + cert=None, cert_key=None, ssl_compression=False, + default_user_agent=None, timeout=None): """ Make an HTTPConnection or HTTPSConnection @@ -333,6 +334,9 @@ class HTTPConnection(object): certificate. :param insecure: Allow to access servers without checking SSL certs. The server's certificate will not be verified. + :param cert: Client certificate file to connect on SSL server + requiring SSL client certificate. + :param cert_key: Client certificate private key file. :param ssl_compression: SSL compression should be disabled by default and this setting is not usable as of now. The parameter is kept for backward compatibility. @@ -362,6 +366,14 @@ class HTTPConnection(object): # verify requests parameter is used to pass the CA_BUNDLE file # see: http://docs.python-requests.org/en/latest/user/advanced/ self.requests_args['verify'] = cacert + if cert: + # NOTE(cbrandily): cert requests parameter is used to pass client + # cert path or a tuple with client certificate/key paths. + if cert_key: + self.requests_args['cert'] = cert, cert_key + else: + self.requests_args['cert'] = cert + if proxy: proxy_parsed = urlparse(proxy) if not proxy_parsed.scheme: @@ -448,8 +460,11 @@ def http_connection(*arg, **kwarg): def get_auth_1_0(url, user, key, snet, **kwargs): cacert = kwargs.get('cacert', None) insecure = kwargs.get('insecure', False) + cert = kwargs.get('cert') + cert_key = kwargs.get('cert_key') timeout = kwargs.get('timeout', None) parsed, conn = http_connection(url, cacert=cacert, insecure=insecure, + cert=cert, cert_key=cert_key, timeout=timeout) method = 'GET' headers = {'X-Auth-User': user, 'X-Auth-Key': key} @@ -530,6 +545,8 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs): project_domain_id=os_options.get('project_domain_id'), debug=debug, cacert=kwargs.get('cacert'), + cert=kwargs.get('cert'), + key=kwargs.get('cert_key'), auth_url=auth_url, insecure=insecure, timeout=timeout) except exceptions.Unauthorized: msg = 'Unauthorized. Check username, password and tenant name/id.' @@ -580,6 +597,8 @@ def get_auth(auth_url, user, key, **kwargs): cacert = kwargs.get('cacert', None) insecure = kwargs.get('insecure', False) + cert = kwargs.get('cert') + cert_key = kwargs.get('cert_key') timeout = kwargs.get('timeout', None) if auth_version in AUTH_VERSIONS_V1: storage_url, token = get_auth_1_0(auth_url, @@ -588,6 +607,8 @@ def get_auth(auth_url, user, key, **kwargs): kwargs.get('snet'), cacert=cacert, insecure=insecure, + cert=cert, + cert_key=cert_key, timeout=timeout) elif auth_version in AUTH_VERSIONS_V2 + AUTH_VERSIONS_V3: # We are handling a special use case here where the user argument @@ -611,6 +632,8 @@ def get_auth(auth_url, user, key, **kwargs): key, os_options, cacert=cacert, insecure=insecure, + cert=cert, + cert_key=cert_key, timeout=timeout, auth_version=auth_version) else: @@ -1372,8 +1395,9 @@ class Connection(object): preauthurl=None, preauthtoken=None, snet=False, starting_backoff=1, max_backoff=64, tenant_name=None, os_options=None, auth_version="1", cacert=None, - insecure=False, ssl_compression=True, - retry_on_ratelimit=False, timeout=None): + insecure=False, cert=None, cert_key=None, + ssl_compression=True, retry_on_ratelimit=False, + timeout=None): """ :param authurl: authentication URL :param user: user name to authenticate as @@ -1395,6 +1419,9 @@ class Connection(object): service_username, service_project_name, service_key :param insecure: Allow to access servers without checking SSL certs. The server's certificate will not be verified. + :param cert: Client certificate file to connect on SSL server + requiring SSL client certificate. + :param cert_key: Client certificate private key file. :param ssl_compression: Whether to enable compression at the SSL layer. If set to 'False' and the pyOpenSSL library is present an attempt to disable SSL compression @@ -1430,6 +1457,8 @@ class Connection(object): self.service_token = None self.cacert = cacert self.insecure = insecure + self.cert = cert + self.cert_key = cert_key self.ssl_compression = ssl_compression self.auth_end_time = 0 self.retry_on_ratelimit = retry_on_ratelimit @@ -1452,6 +1481,8 @@ class Connection(object): os_options=self.os_options, cacert=self.cacert, insecure=self.insecure, + cert=self.cert, + cert_key=self.cert_key, timeout=self.timeout) return self.url, self.token @@ -1477,6 +1508,8 @@ class Connection(object): return http_connection(url if url else self.url, cacert=self.cacert, insecure=self.insecure, + cert=self.cert, + cert_key=self.cert_key, ssl_compression=self.ssl_compression, timeout=self.timeout) diff --git a/swiftclient/service.py b/swiftclient/service.py index 99c833e..d33d7bc 100644 --- a/swiftclient/service.py +++ b/swiftclient/service.py @@ -148,6 +148,8 @@ def _build_default_global_options(): "os_service_type": environ.get('OS_SERVICE_TYPE'), "os_endpoint_type": environ.get('OS_ENDPOINT_TYPE'), "os_cacert": environ.get('OS_CACERT'), + "os_cert": environ.get('OS_CERT'), + "os_key": environ.get('OS_KEY'), "insecure": config_true_value(environ.get('SWIFTCLIENT_INSECURE')), "ssl_compression": False, 'segment_threads': 10, @@ -236,6 +238,8 @@ def get_conn(options): snet=options['snet'], cacert=options['os_cacert'], insecure=options['insecure'], + cert=options['os_cert'], + cert_key=options['os_key'], ssl_compression=options['ssl_compression']) diff --git a/swiftclient/shell.py b/swiftclient/shell.py index 53d7d99..c9e1b75 100755 --- a/swiftclient/shell.py +++ b/swiftclient/shell.py @@ -1294,6 +1294,8 @@ def main(arguments=None): [--os-service-type ] [--os-endpoint-type ] [--os-cacert ] [--insecure] + [--os-cert ] + [--os-key ] [--no-ssl-compression] [--help] [] @@ -1535,6 +1537,16 @@ Examples: help='Specify a CA bundle file to use in verifying a ' 'TLS (https) server certificate. ' 'Defaults to env[OS_CACERT].') + os_grp.add_argument('--os-cert', + metavar='', + default=environ.get('OS_CERT'), + help='Specify a client certificate file (for client ' + 'auth). Defaults to env[OS_CERT].') + os_grp.add_argument('--os-key', + metavar='', + default=environ.get('OS_KEY'), + help='Specify a client certificate key file (for ' + 'client auth). Defaults to env[OS_KEY].') options, args = parse_args(parser, argv[1:], enforce_requires=False) if options.help or options.os_help: diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index 236f1ef..82a5590 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -1781,7 +1781,9 @@ class TestKeystoneOptions(MockHttpTest): 'project-id': 'projectid', 'project-domain-id': 'projectdomainid', 'project-domain-name': 'projectdomain', - 'cacert': 'foo'} + 'cacert': 'foo', + 'cert': 'minnie', + 'key': 'mickey'} catalog_opts = {'service-type': 'my-object-store', 'endpoint-type': 'public', 'region-name': 'my-region'} diff --git a/tests/unit/test_swiftclient.py b/tests/unit/test_swiftclient.py index f3bee3b..5df54b1 100644 --- a/tests/unit/test_swiftclient.py +++ b/tests/unit/test_swiftclient.py @@ -512,6 +512,32 @@ class TestGetAuth(MockHttpTest): os_options=os_options, auth_version='2.0', insecure=False) + def test_auth_v2_cert(self): + os_options = {'tenant_name': 'foo'} + c.get_auth_keystone = fake_get_auth_keystone(os_options, None) + + auth_url_no_sslauth = 'https://www.tests.com' + auth_url_sslauth = 'https://www.tests.com/client-certificate' + + url, token = c.get_auth(auth_url_no_sslauth, 'asdf', 'asdf', + os_options=os_options, auth_version='2.0') + self.assertTrue(url.startswith("http")) + self.assertTrue(token) + + url, token = c.get_auth(auth_url_sslauth, 'asdf', 'asdf', + os_options=os_options, auth_version='2.0', + cert='minnie', cert_key='mickey') + self.assertTrue(url.startswith("http")) + self.assertTrue(token) + + self.assertRaises(c.ClientException, c.get_auth, + auth_url_sslauth, 'asdf', 'asdf', + os_options=os_options, auth_version='2.0') + self.assertRaises(c.ClientException, c.get_auth, + auth_url_sslauth, 'asdf', 'asdf', + os_options=os_options, auth_version='2.0', + cert='minnie') + def test_auth_v3_with_tenant_name(self): # check the correct auth version is passed to get_auth_keystone os_options = {'tenant_name': 'asdf'} @@ -1511,6 +1537,15 @@ class TestHTTPConnection(MockHttpTest): conn = c.http_connection(u'http://www.test.com/', insecure=True) self.assertEqual(conn[1].requests_args['verify'], False) + def test_cert(self): + conn = c.http_connection(u'http://www.test.com/', cert='minnie') + self.assertEqual(conn[1].requests_args['cert'], 'minnie') + + def test_cert_key(self): + conn = c.http_connection( + u'http://www.test.com/', cert='minnie', cert_key='mickey') + self.assertEqual(conn[1].requests_args['cert'], ('minnie', 'mickey')) + def test_response_connection_released(self): _parsed_url, conn = c.http_connection(u'http://www.test.com/') conn.resp = MockHttpResponse() @@ -2018,8 +2053,8 @@ class TestConnection(MockHttpTest): return '' def local_http_connection(url, proxy=None, cacert=None, - insecure=False, ssl_compression=True, - timeout=None): + insecure=False, cert=None, cert_key=None, + ssl_compression=True, timeout=None): parsed = urlparse(url) return parsed, LocalConnection() diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 3b043bc..d04583f 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -57,6 +57,11 @@ def fake_get_auth_keystone(expected_os_options=None, exc=None, actual_kwargs['cacert'] is None: from swiftclient import client as c raise c.ClientException("unverified-certificate") + if auth_url.startswith("https") and \ + auth_url.endswith("client-certificate") and \ + not (actual_kwargs['cert'] and actual_kwargs['cert_key']): + from swiftclient import client as c + raise c.ClientException("noclient-certificate") return storage_url, token return fake_get_auth_keystone @@ -215,6 +220,7 @@ class MockHttpTest(unittest.TestCase): on_request = kwargs.get('on_request') def wrapper(url, proxy=None, cacert=None, insecure=False, + cert=None, cert_key=None, ssl_compression=True, timeout=None): if storage_url: self.assertEqual(storage_url, url) -- cgit v1.2.1