From 3a8320a1d73444b3bb823300e94c3e2ee85fd6ef Mon Sep 17 00:00:00 2001
From: Cedric Brandily <zzelle@gmail.com>
Date: Fri, 1 Apr 2016 23:42:27 +0200
Subject: [PATCH] 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)
 * os-client-config

Change-Id: Ibeaaa5897ae37b37c1e91f3e47076e4e8e4a8ded
Closes-Bug: #1565112
---
 doc/source/man/openstack.rst                  | 12 ++++++++++
 openstackclient/common/clientmanager.py       | 10 ++++++++
 openstackclient/shell.py                      | 12 ++++++++++
 .../tests/common/test_clientmanager.py        | 17 ++++++++++++++
 openstackclient/tests/test_shell.py           | 23 +++++++++++++++++++
 .../notes/bug_1565112-e0cea9bfbcab954f.yaml   |  6 +++++
 6 files changed, 80 insertions(+)
 create mode 100644 releasenotes/notes/bug_1565112-e0cea9bfbcab954f.yaml

diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst
index f02705d817..a4be351ae0 100644
--- a/doc/source/man/openstack.rst
+++ b/doc/source/man/openstack.rst
@@ -114,6 +114,12 @@ OPTIONS
 :option:`--verify` | :option:`--insecure`
     Verify or ignore server certificate (default: verify)
 
+:option:`--os-cert` <certificate-file>
+    Client certificate bundle file
+
+:option:`--os-key` <key-file>
+    Client certificate key file
+
 :option:`--os-identity-api-version` <identity-api-version>
     Identity API version (Default: 2.0)
 
@@ -367,6 +373,12 @@ The following environment variables can be set to alter the behaviour of :progra
 :envvar:`OS_CACERT`
     CA certificate bundle file
 
+:envvar:`OS_CERT`
+    Client certificate bundle file
+
+:envvar:`OS_KEY`
+    Client certificate key file
+
 :envvar:`OS_IDENTITY_API_VERSION`
     Identity API version (Default: 2.0)
 
diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
index 56ddcbad77..6d23b55e64 100644
--- a/openstackclient/common/clientmanager.py
+++ b/openstackclient/common/clientmanager.py
@@ -110,6 +110,15 @@ class ClientManager(object):
             self._cacert = verify
             self._insecure = False
 
+        # Set up client certificate and key
+        # NOTE(cbrandily): This converts client certificate/key to requests
+        #                  cert argument: None (no client certificate), a path
+        #                  to client certificate or a tuple with client
+        #                  certificate/key paths.
+        self._cert = self._cli_options.cert
+        if self._cert and self._cli_options.key:
+            self._cert = self._cert, self._cli_options.key
+
         # Get logging from root logger
         root_logger = logging.getLogger('')
         LOG.setLevel(root_logger.getEffectiveLevel())
@@ -194,6 +203,7 @@ class ClientManager(object):
             auth=self.auth,
             session=request_session,
             verify=self._verify,
+            cert=self._cert,
             user_agent=USER_AGENT,
         )
 
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index 7750f2a391..b7bc7b1a44 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -189,6 +189,18 @@ class OpenStackShell(app.App):
             dest='cacert',
             default=utils.env('OS_CACERT'),
             help='CA certificate bundle file (Env: OS_CACERT)')
+        parser.add_argument(
+            '--os-cert',
+            metavar='<certificate-file>',
+            dest='cert',
+            default=utils.env('OS_CERT'),
+            help='Client certificate bundle file (Env: OS_CERT)')
+        parser.add_argument(
+            '--os-key',
+            metavar='<key-file>',
+            dest='key',
+            default=utils.env('OS_KEY'),
+            help='Client certificate key file (Env: OS_KEY)')
         verify_group = parser.add_mutually_exclusive_group()
         verify_group.add_argument(
             '--verify',
diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py
index 2bd9e7836b..6fc5b41e69 100644
--- a/openstackclient/tests/common/test_clientmanager.py
+++ b/openstackclient/tests/common/test_clientmanager.py
@@ -58,6 +58,8 @@ class FakeOptions(object):
         self.interface = None
         self.url = None
         self.auth = {}
+        self.cert = None
+        self.key = None
         self.default_domain = 'default'
         self.__dict__.update(kwargs)
 
@@ -268,6 +270,21 @@ class TestClientManager(utils.TestCase):
         self.assertEqual('cafile', client_manager._cacert)
         self.assertTrue(client_manager.is_network_endpoint_enabled())
 
+    def test_client_manager_password_no_cert(self):
+        client_manager = clientmanager.ClientManager(
+            cli_options=FakeOptions())
+        self.assertIsNone(client_manager._cert)
+
+    def test_client_manager_password_client_cert(self):
+        client_manager = clientmanager.ClientManager(
+            cli_options=FakeOptions(cert='cert'))
+        self.assertEqual('cert', client_manager._cert)
+
+    def test_client_manager_password_client_cert_and_key(self):
+        client_manager = clientmanager.ClientManager(
+            cli_options=FakeOptions(cert='cert', key='key'))
+        self.assertEqual(('cert', 'key'), client_manager._cert)
+
     def _select_auth_plugin(self, auth_params, api_version, auth_plugin_name):
         auth_params['auth_type'] = auth_plugin_name
         auth_params['identity_api_version'] = api_version
diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py
index ea3c6fe25b..c134cb9351 100644
--- a/openstackclient/tests/test_shell.py
+++ b/openstackclient/tests/test_shell.py
@@ -79,6 +79,8 @@ CLOUD_2 = {
             'region_name': 'occ-cloud,krikkit,occ-env',
             'log_file': '/tmp/test_log_file',
             'log_level': 'debug',
+            'cert': 'mycert',
+            'key': 'mickey',
         }
     }
 }
@@ -567,6 +569,24 @@ class TestShellCli(TestShell):
         self.assertEqual('foo', _shell.options.cacert)
         self.assertFalse(_shell.verify)
 
+    def test_shell_args_cert_options(self):
+        _shell = make_shell()
+
+        # Default
+        fake_execute(_shell, "list user")
+        self.assertEqual('', _shell.options.cert)
+        self.assertEqual('', _shell.options.key)
+
+        # --os-cert
+        fake_execute(_shell, "--os-cert mycert list user")
+        self.assertEqual('mycert', _shell.options.cert)
+        self.assertEqual('', _shell.options.key)
+
+        # --os-key
+        fake_execute(_shell, "--os-key mickey list user")
+        self.assertEqual('', _shell.options.cert)
+        self.assertEqual('mickey', _shell.options.key)
+
     def test_default_env(self):
         flag = ""
         kwargs = {
@@ -670,6 +690,9 @@ class TestShellCli(TestShell):
             _shell.cloud.config['region_name'],
         )
 
+        self.assertEqual('mycert', _shell.cloud.config['cert'])
+        self.assertEqual('mickey', _shell.cloud.config['key'])
+
     @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
     @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
     def test_shell_args_precedence(self, config_mock, vendor_mock):
diff --git a/releasenotes/notes/bug_1565112-e0cea9bfbcab954f.yaml b/releasenotes/notes/bug_1565112-e0cea9bfbcab954f.yaml
new file mode 100644
index 0000000000..864b0ac855
--- /dev/null
+++ b/releasenotes/notes/bug_1565112-e0cea9bfbcab954f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - Support client certificate/key. Client certificate/key can be provided
+    using --os-cert/--os-key options, $OS_CERT/$OS_KEY environment
+    variables or os-client-config config.
+    [Bug `1565112 <https://bugs.launchpad.net/bugs/1565112>`_]