diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index f02705d81..a4be351ae 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` + Client certificate bundle file + +:option:`--os-key` + Client certificate key file + :option:`--os-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 56ddcbad7..6d23b55e6 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 7750f2a39..b7bc7b1a4 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='', + dest='cert', + default=utils.env('OS_CERT'), + help='Client certificate bundle file (Env: OS_CERT)') + parser.add_argument( + '--os-key', + metavar='', + 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 2bd9e7836..6fc5b41e6 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 ea3c6fe25..c134cb935 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 000000000..864b0ac85 --- /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 `_]