summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCedric Brandily <zzelle@gmail.com>2016-04-10 23:18:17 +0200
committerCedric Brandily <zzelle@gmail.com>2016-04-10 23:20:49 +0200
commit450f505c35f8762cca29d56b6e928490288ec166 (patch)
tree367b0d9a84f991053395d77ce965e9367e0136d5
parent015903e383a27318918ee7827574ecb120110ae2 (diff)
downloadpython-swiftclient-450f505c35f8762cca29d56b6e928490288ec166.tar.gz
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
-rw-r--r--swiftclient/client.py39
-rw-r--r--swiftclient/service.py4
-rwxr-xr-xswiftclient/shell.py12
-rw-r--r--tests/unit/test_shell.py4
-rw-r--r--tests/unit/test_swiftclient.py39
-rw-r--r--tests/unit/utils.py6
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 <service-type>]
[--os-endpoint-type <endpoint-type>]
[--os-cacert <ca-certificate>] [--insecure]
+ [--os-cert <client-certificate-file>]
+ [--os-key <client-certificate-key-file>]
[--no-ssl-compression]
<subcommand> [--help] [<subcommand options>]
@@ -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='<client-certificate-file>',
+ 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='<client-certificate-key-file>',
+ 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)