From abc7c47c18f54c33668e9862fac614b7ce1d6d0a Mon Sep 17 00:00:00 2001 From: Liem Nguyen Date: Wed, 23 May 2012 18:16:50 +0000 Subject: [PATCH] Support 2-way SSL with Keystone server if it is configured to enforce 2-way SSL. See also https://review.openstack.org/#/c/7706/ for the corresponding review for the 2-way SSL addition to Keystone. Change-Id: If0cb46a43d663687396d93604a7139d85a4e7114 --- doc/source/shell.rst | 21 ++++++++++- keystoneclient/client.py | 10 ++++-- keystoneclient/service_catalog.py | 2 +- keystoneclient/shell.py | 45 +++++++++++++++++++++-- tests/test_https.py | 60 +++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 tests/test_https.py diff --git a/doc/source/shell.rst b/doc/source/shell.rst index f86af725d..209d681aa 100644 --- a/doc/source/shell.rst +++ b/doc/source/shell.rst @@ -42,13 +42,32 @@ options, it is easier to just set them as environment variables: The OpenStack Identity API version. +.. envvar:: OS_CA_CERT + + The location for the CA truststore (PEM formatted) for this client. + +.. envvar:: OS_CERT + + The location for the keystore (PEM formatted) containing the public + key of this client. This keystore can also optionally contain the + private key of this client. + +.. envvar:: OS_KEY + + The location for the keystore (PEM formatted) containing the private + key of this client. This value can be empty if the private key is + included in the OS_CERT file. + For example, in Bash you'd use:: export OS_USERNAME=yourname export OS_PASSWORD=yadayadayada export OS_TENANT_NAME=myproject - export OS_AUTH_URL=http://example.com:5000/v2.0/ + export OS_AUTH_URL=http(s)://example.com:5000/v2.0/ export OS_IDENTITY_API_VERSION=2.0 + export OS_CA_CERT=/etc/keystone/yourca.pem + export OS_CERT=/etc/keystone/yourpublickey.pem + export OS_KEY=/etc/keystone/yourprivatekey.pem From there, all shell commands take the form:: diff --git a/keystoneclient/client.py b/keystoneclient/client.py index 337f04891..b53d7cfad 100644 --- a/keystoneclient/client.py +++ b/keystoneclient/client.py @@ -38,8 +38,14 @@ class HTTPClient(httplib2.Http): def __init__(self, username=None, tenant_id=None, tenant_name=None, password=None, auth_url=None, region_name=None, timeout=None, - endpoint=None, token=None): - super(HTTPClient, self).__init__(timeout=timeout) + endpoint=None, token=None, cacert=None, key=None, + cert=None): + super(HTTPClient, self).__init__(timeout=timeout, ca_certs=cacert) + if cert: + if key: + self.add_certificate(key=key, cert=cert, domain='') + else: + self.add_certificate(key=cert, cert=cert, domain='') self.username = username self.tenant_id = tenant_id self.tenant_name = tenant_name diff --git a/keystoneclient/service_catalog.py b/keystoneclient/service_catalog.py index b78d019e4..faff74bfe 100644 --- a/keystoneclient/service_catalog.py +++ b/keystoneclient/service_catalog.py @@ -39,7 +39,7 @@ class ServiceCatalog(object): return token def url_for(self, attr=None, filter_value=None, - service_type='identity', endpoint_type='publicURL'): + service_type='identity', endpoint_type='publicURL'): """Fetch an endpoint from the service catalog. Fetch the specified endpoint from the service catalog for diff --git a/keystoneclient/shell.py b/keystoneclient/shell.py index 4c4c5eb71..b1752e233 100644 --- a/keystoneclient/shell.py +++ b/keystoneclient/shell.py @@ -127,6 +127,41 @@ class OpenStackIdentityShell(object): default=env('SERVICE_ENDPOINT'), help='Defaults to env[SERVICE_ENDPOINT]') + parser.add_argument('--os_cacert', metavar='', + default=env('OS_CA_CERT'), + help='Defaults to env[OS_CA_CERT]') + + parser.add_argument('--os_cert', metavar='', + default=env('OS_CERT'), + help='Defaults to env[OS_CERT]') + + parser.add_argument('--os_key', metavar='', + default=env('OS_KEY'), + help='Defaults to env[OS_KEY]') + + # FIXME(dtroyer): The args below are here for diablo compatibility, + # remove them in folsum cycle + + parser.add_argument('--username', + metavar='', + help='Deprecated') + + parser.add_argument('--password', + metavar='', + help='Deprecated') + + parser.add_argument('--tenant_name', + metavar='', + help='Deprecated') + + parser.add_argument('--auth_url', + metavar='', + help='Deprecated') + + parser.add_argument('--region_name', + metavar='', + help='Deprecated') + return parser def get_subcommand_parser(self, version): @@ -246,7 +281,10 @@ class OpenStackIdentityShell(object): 'env[OS_AUTH_URL]') if utils.isunauthenticated(args.func): - self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url) + self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url, + cacert=args.os_cacert, + key=args.os_key, + cert=args.os_cert) else: token = None endpoint = None @@ -262,7 +300,10 @@ class OpenStackIdentityShell(object): endpoint=endpoint, password=args.os_password, auth_url=args.os_auth_url, - region_name=args.os_region_name) + region_name=args.os_region_name, + cacert=args.os_cacert, + key=args.os_key, + cert=args.os_cert) try: args.func(self.cs, args) diff --git a/tests/test_https.py b/tests/test_https.py new file mode 100644 index 000000000..4d433c695 --- /dev/null +++ b/tests/test_https.py @@ -0,0 +1,60 @@ +import httplib2 +import mock + +from keystoneclient import client +from tests import utils + + +FAKE_RESPONSE = httplib2.Response({"status": 200}) +FAKE_BODY = '{"hi": "there"}' +MOCK_REQUEST = mock.Mock(return_value=(FAKE_RESPONSE, FAKE_BODY)) + + +def get_client(): + cl = client.HTTPClient(username="username", password="password", + tenant_id="tenant", auth_url="auth_test", + cacert="ca.pem", key="key.pem", cert="cert.pem") + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + return cl + + +class ClientTest(utils.TestCase): + + def test_get(self): + cl = get_authed_client() + + @mock.patch.object(httplib2.Http, "request", MOCK_REQUEST) + @mock.patch('time.time', mock.Mock(return_value=1234)) + def test_get_call(): + resp, body = cl.get("/hi") + headers = {"X-Auth-Token": "token", + "User-Agent": cl.USER_AGENT} + MOCK_REQUEST.assert_called_with("https://127.0.0.1:5000/hi", + "GET", headers=headers) + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + test_get_call() + + def test_post(self): + cl = get_authed_client() + + @mock.patch.object(httplib2.Http, "request", MOCK_REQUEST) + def test_post_call(): + cl.post("/hi", body=[1, 2, 3]) + headers = { + "X-Auth-Token": "token", + "Content-Type": "application/json", + "User-Agent": cl.USER_AGENT + } + MOCK_REQUEST.assert_called_with("https://127.0.0.1:5000/hi", + "POST", headers=headers, + body='[1, 2, 3]') + + test_post_call()