From 68fecd9ba8e5853d7f7fdf22cd9196eced21e4e2 Mon Sep 17 00:00:00 2001 From: Samuel Walladge Date: Thu, 30 Jun 2022 11:39:18 +0930 Subject: [PATCH] Update hvac library to latest version Update deprecated method calls where possible, and use new methods instead of lower level read/write calls. Change-Id: I991435cdf8d36016e75c46823ec47f3290a42fe4 --- .gitignore | 1 + src/lib/charm/vault.py | 57 +-- src/lib/charm/vault_pki.py | 128 ++++--- src/reactive/vault_handlers.py | 12 +- src/wheelhouse.txt | 2 +- test-requirements.txt | 2 +- unit_tests/test_lib_charm_vault.py | 100 ++++-- unit_tests/test_lib_charm_vault_pki.py | 381 +++++++++------------ unit_tests/test_reactive_vault_handlers.py | 8 +- 9 files changed, 342 insertions(+), 349 deletions(-) diff --git a/.gitignore b/.gitignore index ccc4789..303a972 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *~ build/* .tox/* +src/.tox/* .stestr/* __pycache__ .unit-state.db diff --git a/src/lib/charm/vault.py b/src/lib/charm/vault.py index 9b3bdba..d83b5a2 100644 --- a/src/lib/charm/vault.py +++ b/src/lib/charm/vault.py @@ -179,8 +179,8 @@ def enable_approle_auth(client): :param client: Vault client :type client: hvac.Client""" - if 'approle/' not in client.list_auth_backends(): - client.enable_auth_backend('approle') + if 'approle/' not in client.sys.list_auth_methods(): + client.sys.enable_auth_method('approle') def create_local_charm_access_role(client, policies): @@ -192,14 +192,15 @@ def create_local_charm_access_role(client, policies): :type policies: [str, str, ...] :returns: Id of created role :rtype: str""" - client.create_role( + client.auth.approle.create_or_update_approle( CHARM_ACCESS_ROLE, token_ttl='60s', token_max_ttl='60s', - policies=policies, + token_policies=policies, bind_secret_id='false', - bound_cidr_list='127.0.0.1/32') - return client.get_role_id(CHARM_ACCESS_ROLE) + token_bound_cidrs=['127.0.0.1/32']) + response = client.auth.approle.read_role_id(CHARM_ACCESS_ROLE) + return response["data"]["role_id"] def setup_charm_vault_access(token=None): @@ -216,7 +217,7 @@ def setup_charm_vault_access(token=None): token=token) enable_approle_auth(client) policies = [CHARM_POLICY_NAME] - client.set_policy(CHARM_POLICY_NAME, CHARM_POLICY) + client.sys.create_or_update_policy(CHARM_POLICY_NAME, CHARM_POLICY) return create_local_charm_access_role(client, policies=policies) @@ -254,8 +255,7 @@ def get_local_client(): if not app_role_id: hookenv.log('Could not retrieve app_role_id', level=hookenv.DEBUG) raise VaultNotReady("Cannot initialise local client") - client = hvac.Client(url=VAULT_LOCALHOST_URL) - client.auth_approle(app_role_id) + client.auth.approle.login(app_role_id) return client @@ -312,7 +312,7 @@ def initialize_vault(shares=1, threshold=1): :type threshold: int """ client = get_client(url=VAULT_LOCALHOST_URL) - result = client.initialize(shares, threshold) + result = client.sys.initialize(shares, threshold) client.token = result['root_token'] hookenv.leader_set( root_token=result['root_token'], @@ -326,7 +326,7 @@ def unseal_vault(keys=None): if not keys: keys = json.loads(hookenv.leader_get()['keys']) for key in keys: - client.unseal(key) + client.sys.submit_unseal_key(key) def can_restart(): @@ -342,9 +342,9 @@ def can_restart(): safe_restart = True else: client = get_client(url=VAULT_LOCALHOST_URL) - if not client.is_initialized(): + if not client.sys.is_initialized(): safe_restart = True - elif client.is_sealed(): + elif client.sys.is_sealed(): safe_restart = True hookenv.log( "Safe to restart: {}".format(safe_restart), @@ -359,11 +359,13 @@ def configure_secret_backend(client, name): :ptype client: hvac.Client :param name: Name of backend to enable :ptype name: str""" - if '{}/'.format(name) not in client.list_secret_backends(): - client.enable_secret_backend(backend_type='kv', - description='Charm created KV backend', - mount_point=name, - options={'version': 1}) + if '{}/'.format(name) not in client.sys.list_mounted_secrets_engines(): + client.sys.enable_secrets_engine( + backend_type='kv', + description='Charm created KV backend', + path=name, + options={'version': 1} + ) def configure_policy(client, name, hcl): @@ -375,7 +377,7 @@ def configure_policy(client, name, hcl): :ptype name: str :param hcl: Vault policy HCL :ptype hcl: str""" - client.set_policy(name, hcl) + client.sys.create_or_update_policy(name, hcl) def configure_approle(client, name, cidr, policies): @@ -391,14 +393,14 @@ def configure_approle(client, name, cidr, policies): :ptype policies: [str, str, ...] :returns: Id of created role :rtype: str""" - client.create_role( + client.auth.approle.create_or_update_approle( name, token_ttl='60s', token_max_ttl='60s', - policies=policies, + token_policies=policies, bind_secret_id='true', - bound_cidr_list=cidr) - return client.get_role_id(name) + token_bound_cidrs=[cidr]) + return client.auth.approle.read_role_id(name)["data"]["role_id"] def generate_role_secret_id(client, name, cidr): @@ -412,6 +414,9 @@ def generate_role_secret_id(client, name, cidr): :ptype cidr: str :returns: Vault token to retrieve the response-wrapped response :rtype: str""" + # NOTE: cannot use client.auth.approle.generate_secret_id() + # because that doesn't support wrap_ttl + # https://github.com/hvac/hvac/issues/683 response = client.write('auth/approle/role/{}/secret-id'.format(name), wrap_ttl='1h', cidr_list=cidr) return response['wrap_info']['token'] @@ -423,7 +428,7 @@ def is_backend_mounted(client, name): :returns: Whether mount point is in use :rtype: bool """ - return '{}/'.format(name) in client.list_secret_backends() + return '{}/'.format(name) in client.sys.list_mounted_secrets_engines() def vault_ready_for_clients(): @@ -433,8 +438,8 @@ def vault_ready_for_clients(): reraise=True) def _check_vault_status(client): if (not host.service_running('vault') or - not client.is_initialized() or - client.is_sealed()): + not client.sys.is_initialized() or + client.sys.is_sealed()): return False return True diff --git a/src/lib/charm/vault_pki.py b/src/lib/charm/vault_pki.py index c6f0631..2c8a8b5 100644 --- a/src/lib/charm/vault_pki.py +++ b/src/lib/charm/vault_pki.py @@ -28,10 +28,10 @@ def configure_pki_backend(client, name, ttl=None, max_ttl=None): :type ttl: str """ if not vault.is_backend_mounted(client, name): - client.enable_secret_backend( + client.sys.enable_secrets_engine( backend_type='pki', description='Charm created PKI backend', - mount_point=name, + path=name, # Default ttl to 10 years config={ 'default_lease_ttl': ttl or '8759h', @@ -43,12 +43,12 @@ def disable_pki_backend(): """ client = vault.get_local_client() if vault.is_backend_mounted(client, CHARM_PKI_MP): - client.delete('{}/root'.format(CHARM_PKI_MP)) - client.delete('{}/roles/{}'.format(CHARM_PKI_MP, - CHARM_PKI_ROLE_CLIENT)) - client.delete('{}/roles/{}'.format(CHARM_PKI_MP, - CHARM_PKI_ROLE)) - client.disable_secret_backend(CHARM_PKI_MP) + client.secrets.pki.delete_root(mount_point=CHARM_PKI_MP) + client.secrets.pki.delete_role(CHARM_PKI_ROLE_CLIENT, + mount_point=CHARM_PKI_MP) + client.secrets.pki.delete_role(CHARM_PKI_ROLE, + mount_point=CHARM_PKI_MP) + client.sys.disable_secrets_engine(CHARM_PKI_MP) def tune_pki_backend(ttl=None, max_ttl=None): @@ -59,9 +59,8 @@ def tune_pki_backend(ttl=None, max_ttl=None): """ client = vault.get_local_client() if vault.is_backend_mounted(client, CHARM_PKI_MP): - client.tune_secret_backend( - backend_type='pki', - mount_point=CHARM_PKI_MP, + client.sys.tune_mount_configuration( + path=CHARM_PKI_MP, default_lease_ttl=ttl or '8759h', max_lease_ttl=max_ttl or '87600h') @@ -72,7 +71,7 @@ def is_ca_ready(client, name, role): :returns: Whether CA is ready :rtype: bool """ - return client.read('{}/roles/{}'.format(name, role)) is not None + return client.secrets.pki.read_role(role, mount_point=name) is not None def get_chain(name=None): @@ -84,7 +83,9 @@ def get_chain(name=None): client = vault.get_local_client() if not name: name = CHARM_PKI_MP - return client.read('{}/cert/ca_chain'.format(name))['data']['certificate'] + response = client.secrets.pki.read_certificate('ca_chain', + mount_point=name) + return response['data']['certificate'] def get_ca(): @@ -120,9 +121,7 @@ def generate_certificate(cert_type, common_name, sans, ttl, max_ttl): else: raise vault.VaultInvalidRequest('Unsupported cert_type: ' '{}'.format(cert_type)) - config = { - 'common_name': common_name, - } + config = {} if sans: ip_sans, alt_names = sort_sans(sans) if ip_sans: @@ -130,8 +129,12 @@ def generate_certificate(cert_type, common_name, sans, ttl, max_ttl): if alt_names: config['alt_names'] = ','.join(alt_names) try: - response = client.write('{}/issue/{}'.format(CHARM_PKI_MP, role), - **config) + response = client.secrets.pki.generate_certificate( + role, + common_name, + extra_params=config, + mount_point=CHARM_PKI_MP, + ) if not response['data']: raise vault.VaultError(response.get('warnings', 'unknown error')) except hvac.exceptions.InvalidRequest as e: @@ -168,6 +171,11 @@ def get_csr(ttl=None, common_name=None, locality=None, """ client = vault.get_local_client() configure_pki_backend(client, CHARM_PKI_MP) + if common_name is None: + common_name = ( + "Vault Intermediate Certificate Authority " + "({})".format(CHARM_PKI_MP) + ) config = { # Year - 1 hour 'ttl': ttl or '87599h', @@ -175,14 +183,14 @@ def get_csr(ttl=None, common_name=None, locality=None, 'province': province, 'ou': organizational_unit, 'organization': organization, - 'common_name': common_name or ("Vault Intermediate Certificate " - "Authority " "({})".format(CHARM_PKI_MP) - ), 'locality': locality} config = {k: v for k, v in config.items() if v} - csr_info = client.write( - '{}/intermediate/generate/internal'.format(CHARM_PKI_MP), - **config) + csr_info = client.secrets.pki.generate_intermediate( + 'internal', + common_name, + extra_params=config, + mount_point=CHARM_PKI_MP, + ) if not csr_info['data']: raise vault.VaultError(csr_info.get('warnings', 'unknown error')) return csr_info['data']['csr'] @@ -210,17 +218,20 @@ def upload_signed_csr(pem, allowed_domains, allow_subdomains=True, client = vault.get_local_client() # Set the intermediate certificate authorities signing certificate to the # signed certificate. - # (hvac module doesn't expose a method for this, hence the _post call) - client._post( - 'v1/{}/intermediate/set-signed'.format(CHARM_PKI_MP), - json={'certificate': pem.rstrip()}) + client.secrets.pki.set_signed_intermediate( + pem.rstrip(), + mount_point=CHARM_PKI_MP + ) # Generated certificates can have the CRL location and the location of the # issuing certificate encoded. addr = vault.get_access_address() - client.write( - '{}/config/urls'.format(CHARM_PKI_MP), - issuing_certificates="{}/v1/{}/ca".format(addr, CHARM_PKI_MP), - crl_distribution_points="{}/v1/{}/crl".format(addr, CHARM_PKI_MP) + client.secrets.pki.set_urls( + { + "issuing_certificates": "{}/v1/{}/ca".format(addr, CHARM_PKI_MP), + "crl_distribution_points": + "{}/v1/{}/crl".format(addr, CHARM_PKI_MP), + }, + mount_point=CHARM_PKI_MP ) # Configure a role which maps to a policy for accessing this pki if not max_ttl: @@ -271,23 +282,28 @@ def generate_root_ca(ttl='87599h', allow_any_name=True, allowed_domains=None, if is_ca_ready(client, CHARM_PKI_MP, CHARM_PKI_ROLE): raise vault.VaultError('PKI CA already configured') config = { - 'common_name': ("Vault Root Certificate Authority " - "({})".format(CHARM_PKI_MP)), 'ttl': ttl, } - csr_info = client.write( - '{}/root/generate/internal'.format(CHARM_PKI_MP), - **config) + common_name = "Vault Root Certificate Authority ({})".format(CHARM_PKI_MP) + csr_info = client.secrets.pki.generate_root( + 'internal', + common_name, + extra_params=config, + mount_point=CHARM_PKI_MP, + ) if not csr_info['data']: raise vault.VaultError(csr_info.get('warnings', 'unknown error')) cert = csr_info['data']['certificate'] # Generated certificates can have the CRL location and the location of the # issuing certificate encoded. addr = vault.get_access_address() - client.write( - '{}/config/urls'.format(CHARM_PKI_MP), - issuing_certificates="{}/v1/{}/ca".format(addr, CHARM_PKI_MP), - crl_distribution_points="{}/v1/{}/crl".format(addr, CHARM_PKI_MP) + client.secrets.pki.set_urls( + { + "issuing_certificates": "{}/v1/{}/ca".format(addr, CHARM_PKI_MP), + "crl_distribution_points": + "{}/v1/{}/crl".format(addr, CHARM_PKI_MP), + }, + mount_point=CHARM_PKI_MP ) write_roles(client, @@ -318,23 +334,33 @@ def sort_sans(sans): def write_roles(client, **kwargs): # Configure a role for using this PKI to issue server certs - client.write( - '{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE), - server_flag=True, - **kwargs) + client.secrets.pki.create_or_update_role( + CHARM_PKI_ROLE, + extra_params={ + 'server_flag': True, + **kwargs, + }, + mount_point=CHARM_PKI_MP, + ) # Configure a role for using this PKI to issue client-only certs - client.write( - '{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE_CLIENT), - server_flag=False, # client certs cannot be used as server certs - **kwargs) + client.secrets.pki.create_or_update_role( + CHARM_PKI_ROLE_CLIENT, + extra_params={ + # client certs cannot be used as server certs + 'server_flag': False, + **kwargs, + }, + mount_point=CHARM_PKI_MP, + ) def update_roles(**kwargs): client = vault.get_local_client() # local and local-client contain the same data except for server_flag, # so we only need to read one, but update both - local = client.read( - '{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE))['data'] + local = client.secrets.pki.read_role( + CHARM_PKI_ROLE, mount_point=CHARM_PKI_MP + )['data'] # the reason we handle as kwargs here is because updating n-1 fields # causes all the others to reset. Therefore we always need to read what # the current values of all fields are, and apply all of them as well diff --git a/src/reactive/vault_handlers.py b/src/reactive/vault_handlers.py index 2baed15..0a87d13 100644 --- a/src/reactive/vault_handlers.py +++ b/src/reactive/vault_handlers.py @@ -505,8 +505,8 @@ def configure_secrets_backend(): reraise=True) def _check_vault_status(client): if (not service_running('vault') or - not client.is_initialized() or - client.is_sealed()): + not client.sys.is_initialized() or + client.sys.is_sealed()): return False return True @@ -525,7 +525,7 @@ def configure_secrets_backend(): log('Charm access to vault not configured, deferring' ' secrets backend setup', level=DEBUG) return - client.auth_approle(charm_role_id) + client.auth.approle.login(charm_role_id) secrets = (endpoint_from_flag('endpoint.secrets.new-request') or endpoint_from_flag('secrets.connected')) @@ -568,7 +568,7 @@ def configure_secrets_backend(): ) cidr = '{}/32'.format(access_address) - new_role = (approle_name not in client.list_roles()) + new_role = (approle_name not in client.auth.approle.list_roles()) approle_id = vault.configure_approle( client, @@ -965,7 +965,7 @@ def publish_ca_info(): return client = vault.get_client(url=vault.VAULT_LOCALHOST_URL) tls = endpoint_from_flag('certificates.available') - if client.is_sealed(): + if client.sys.is_sealed(): log("Unable to publish ca info, service sealed.") else: tls.set_ca(vault_pki.get_ca()) @@ -1165,7 +1165,7 @@ def tune_pki_backend_config_changed(): set_flag('failed.to.start') return client = vault.get_client(url=vault.VAULT_LOCALHOST_URL) - if client.is_sealed(): + if client.sys.is_sealed(): log("Unable to tune pki backend, service sealed.") else: ttl = config()['default-ttl'] diff --git a/src/wheelhouse.txt b/src/wheelhouse.txt index 46021f3..3b07459 100644 --- a/src/wheelhouse.txt +++ b/src/wheelhouse.txt @@ -1,5 +1,5 @@ netifaces -hvac<0.7.0 +hvac<0.12.0 # for xenial support, tenacity 8.0.0+ drops support for py35 tenacity<8.0.0 pbr diff --git a/test-requirements.txt b/test-requirements.txt index a11a7d0..b9f4518 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -50,5 +50,5 @@ tenacity # vault pbr==5.6.0 # vault cryptography<3.4 # vault, keystone-saml-mellon lxml # keystone-saml-mellon -hvac # vault, barbican-vault +hvac<0.12.0 # vault, barbican-vault psutil # cinder-lvm diff --git a/unit_tests/test_lib_charm_vault.py b/unit_tests/test_lib_charm_vault.py index d927c0f..712663f 100644 --- a/unit_tests/test_lib_charm_vault.py +++ b/unit_tests/test_lib_charm_vault.py @@ -25,9 +25,9 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): def test_enable_approle_auth(self): client_mock = mock.MagicMock() - client_mock.list_auth_backends.return_value = [] + client_mock.sys.list_auth_methods.return_value = [] vault.enable_approle_auth(client_mock) - client_mock.enable_auth_backend.assert_called_once_with('approle') + client_mock.sys.enable_auth_method.assert_called_once_with('approle') def test_enable_approle_auth_mounted(self): client_mock = mock.MagicMock() @@ -37,17 +37,19 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): def test_create_local_charm_access_role(self): client_mock = mock.MagicMock() - client_mock.get_role_id.return_value = '123' + client_mock.auth.approle.read_role_id.return_value = { + 'data': {'role_id': '123'}} policies = ['policy1', 'pilicy2'] role_id = vault.create_local_charm_access_role(client_mock, policies) self.assertEqual(role_id, '123') - client_mock.create_role.assert_called_once_with( - 'local-charm-access', - bind_secret_id='false', - bound_cidr_list='127.0.0.1/32', - policies=['policy1', 'pilicy2'], - token_max_ttl='60s', - token_ttl='60s') + client_mock.auth.approle.create_or_update_approle.\ + assert_called_once_with( + 'local-charm-access', + bind_secret_id='false', + token_bound_cidrs=['127.0.0.1/32'], + token_policies=['policy1', 'pilicy2'], + token_max_ttl='60s', + token_ttl='60s') @patch.object(vault.hvac, 'Client') @patch.object(vault, 'get_api_url') @@ -64,7 +66,7 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): mock_enable_approle_auth.assert_called_once_with(client_mock) policy_calls = [ mock.call('local-charm-policy', mock.ANY)] - client_mock.set_policy.assert_has_calls(policy_calls) + client_mock.sys.create_or_update_policy.assert_has_calls(policy_calls) mock_create_local_charm_access_role.assert_called_once_with( client_mock, policies=['local-charm-policy']) @@ -164,10 +166,10 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): config.return_value = False service_running.return_value = True hvac_mock = mock.MagicMock() - hvac_mock.is_initialized.return_value = False + hvac_mock.sys.is_initialized.return_value = False get_client.return_value = hvac_mock self.assertTrue(vault.can_restart()) - hvac_mock.is_initialized.assert_called_once_with() + hvac_mock.sys.is_initialized.assert_called_once_with() @patch.object(vault.host, 'service_running') @patch.object(vault.hookenv, 'config') @@ -176,12 +178,12 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): config.return_value = False service_running.return_value = True hvac_mock = mock.MagicMock() - hvac_mock.is_initialized.return_value = True - hvac_mock.is_sealed.return_value = True + hvac_mock.sys.is_initialized.return_value = True + hvac_mock.sys.is_sealed.return_value = True get_client.return_value = hvac_mock self.assertTrue(vault.can_restart()) - hvac_mock.is_initialized.assert_called_once_with() - hvac_mock.is_sealed.assert_called_once_with() + hvac_mock.sys.is_initialized.assert_called_once_with() + hvac_mock.sys.is_sealed.assert_called_once_with() @patch.object(vault.host, 'service_running') @patch.object(vault.hookenv, 'config') @@ -190,8 +192,8 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): config.return_value = False service_running.return_value = True hvac_mock = mock.MagicMock() - hvac_mock.is_initialized.return_value = True - hvac_mock.is_sealed.return_value = False + hvac_mock.sys.is_initialized.return_value = True + hvac_mock.sys.is_sealed.return_value = False get_client.return_value = hvac_mock self.assertFalse(vault.can_restart()) @@ -328,15 +330,15 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): @patch.object(vault, 'get_client') def test_initialize_vault(self, get_client, leader_set): hvac_mock = mock.MagicMock() - hvac_mock.is_initialized.return_value = True - hvac_mock.initialize.return_value = { + hvac_mock.sys.is_initialized.return_value = True + hvac_mock.sys.initialize.return_value = { 'keys': ['c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e'], 'keys_base64': ['xX35oUPVVCNIO5B26nu6SbY65DK/dHKfd6+05y1Afcw='], 'root_token': 'dee94df7-23a3-9bf2-cb96-e943537c2b76' } get_client.return_value = hvac_mock vault.initialize_vault() - hvac_mock.initialize.assert_called_once_with(1, 1) + hvac_mock.sys.initialize.assert_called_once_with(1, 1) leader_set.assert_called_once_with( keys='["c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e"]', root_token='dee94df7-23a3-9bf2-cb96-e943537c2b76') @@ -351,7 +353,7 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): 'keys': '["c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e"]' } vault.unseal_vault() - hvac_mock.unseal.assert_called_once_with( + hvac_mock.sys.submit_unseal_key.assert_called_once_with( 'c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e') @patch.object(vault.hookenv, 'log') @@ -373,12 +375,13 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): def test_configure_secret_backend(self): hvac_client = mock.MagicMock() - hvac_client.list_secret_backends.return_value = ['secrets/'] + hvac_client.sys.list_mounted_secrets_engines.return_value = [ + 'secrets/'] vault.configure_secret_backend(hvac_client, 'test') - hvac_client.enable_secret_backend.assert_called_once_with( + hvac_client.sys.enable_secrets_engine.assert_called_once_with( backend_type='kv', description=mock.ANY, - mount_point='test', + path='test', options={'version': 1}) def test_configure_secret_backend_noop(self): @@ -404,14 +407,15 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): def test_configure_policy(self): hvac_client = mock.MagicMock() vault.configure_policy(hvac_client, 'test-policy', 'test-hcl') - hvac_client.set_policy.assert_called_once_with( + hvac_client.sys.create_or_update_policy.assert_called_once_with( 'test-policy', 'test-hcl', ) def test_configure_approle(self): hvac_client = mock.MagicMock() - hvac_client.get_role_id.return_value = 'some-UUID' + hvac_client.auth.approle.read_role_id.return_value = { + 'data': {'role_id': 'some-UUID'}} self.assertEqual( vault.configure_approle(hvac_client, 'test-role', @@ -419,12 +423,34 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase): ['test-policy']), 'some-UUID' ) - hvac_client.create_role.assert_called_once_with( - 'test-role', - token_ttl='60s', - token_max_ttl='60s', - policies=['test-policy'], - bind_secret_id='true', - bound_cidr_list='10.5.0.20/32' - ) - hvac_client.get_role_id.assert_called_with('test-role') + hvac_client.auth.approle.create_or_update_approle.\ + assert_called_once_with( + 'test-role', + token_ttl='60s', + token_max_ttl='60s', + token_policies=['test-policy'], + bind_secret_id='true', + token_bound_cidrs=['10.5.0.20/32'] + ) + hvac_client.auth.approle.read_role_id.assert_called_with('test-role') + + @patch.object(vault.hookenv, 'leader_get') + @patch.object(vault, 'get_client') + def test_get_local_client(self, get_client, mock_leader_get): + leader_db = {'local-charm-access-id': '12'} + mock_leader_get.side_effect = lambda x: leader_db[x] + hvac_client = mock.MagicMock() + get_client.return_value = hvac_client + client = vault.get_local_client() + self.assertEqual(client, hvac_client) + hvac_client.auth.approle.login.assert_called_once_with('12') + + @patch.object(vault.hookenv, 'leader_get') + @patch.object(vault, 'get_client') + def test_get_local_client_not_ready(self, get_client, mock_leader_get): + leader_db = {'local-charm-access-id': None} + mock_leader_get.side_effect = lambda x: leader_db[x] + hvac_client = mock.MagicMock() + get_client.return_value = hvac_client + with self.assertRaises(vault.VaultNotReady): + vault.get_local_client() diff --git a/unit_tests/test_lib_charm_vault_pki.py b/unit_tests/test_lib_charm_vault_pki.py index ea7a72a..7fbea58 100644 --- a/unit_tests/test_lib_charm_vault_pki.py +++ b/unit_tests/test_lib_charm_vault_pki.py @@ -24,13 +24,13 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): client_mock, 'my_backend', ttl=42, max_ttl=42) - client_mock.enable_secret_backend.assert_called_once_with( + client_mock.sys.enable_secrets_engine.assert_called_once_with( backend_type='pki', config={ 'default_lease_ttl': 42, 'max_lease_ttl': 42}, description='Charm created PKI backend', - mount_point='my_backend') + path='my_backend') @patch.object(vault_pki.vault, 'is_backend_mounted') def test_configure_pki_backend_default_ttl(self, is_backend_mounted): @@ -39,13 +39,13 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): vault_pki.configure_pki_backend( client_mock, 'my_backend') - client_mock.enable_secret_backend.assert_called_once_with( + client_mock.sys.enable_secrets_engine.assert_called_once_with( backend_type='pki', config={ 'default_lease_ttl': '8759h', 'max_lease_ttl': '87600h'}, description='Charm created PKI backend', - mount_point='my_backend') + path='my_backend') @patch.object(vault_pki.vault, 'is_backend_mounted') def test_configure_pki_backend_noop(self, is_backend_mounted): @@ -59,34 +59,42 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): def test_is_ca_ready(self): client_mock = mock.MagicMock() - vault_pki.is_ca_ready(client_mock, 'my_backend', 'local') - client_mock.read.assert_called_once_with('my_backend/roles/local') + + def read_role(role, mount_point=None): + if role == "role": + return "role info" + + client_mock.secrets.pki.read_role.side_effect = read_role + self.assertTrue(vault_pki.is_ca_ready(client_mock, 'mp', 'role')) + self.assertFalse( + vault_pki.is_ca_ready(client_mock, 'mp', 'doesnotexist') + ) @patch.object(vault_pki.vault, 'get_local_client') def test_get_chain(self, get_local_client): client_mock = mock.MagicMock() - client_mock.read.return_value = { + client_mock.secrets.pki.read_certificate.return_value = { 'data': { 'certificate': 'somecert'}} get_local_client.return_value = client_mock self.assertEqual( vault_pki.get_chain('my_backend'), 'somecert') - client_mock.read.assert_called_once_with( - 'my_backend/cert/ca_chain') + client_mock.secrets.pki.read_certificate.assert_called_once_with( + 'ca_chain', mount_point='my_backend') @patch.object(vault_pki.vault, 'get_local_client') def test_get_chain_default_pki(self, get_local_client): client_mock = mock.MagicMock() - client_mock.read.return_value = { + client_mock.secrets.pki.read_certificate.return_value = { 'data': { 'certificate': 'somecert'}} get_local_client.return_value = client_mock self.assertEqual( vault_pki.get_chain(), 'somecert') - client_mock.read.assert_called_once_with( - 'charm-pki-local/cert/ca_chain') + client_mock.secrets.pki.read_certificate.assert_called_once_with( + 'ca_chain', mount_point=vault_pki.CHARM_PKI_MP) @patch.object(vault_pki.hookenv, 'leader_get') def test_get_ca(self, leader_get): @@ -102,28 +110,11 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): is_ca_ready, sort_sans): client_mock = mock.MagicMock() - client_mock.write.return_value = {'data': 'data'} + client_mock.secrets.pki.generate_certificate.return_value = { + 'data': 'data'} get_local_client.return_value = client_mock is_ca_ready.return_value = True sort_sans.side_effect = lambda l: (l[0], l[1]) - write_calls = [ - mock.call( - 'charm-pki-local/issue/local', - common_name='example.com', - ), - mock.call( - 'charm-pki-local/issue/local', - common_name='example.com', - ip_sans='ip1', - alt_names='alt1', - ), - mock.call( - 'charm-pki-local/issue/local-client', - common_name='example.com', - ip_sans='ip1,ip2', - alt_names='alt1,alt2', - ), - ] vault_pki.generate_certificate('server', 'example.com', ([], []), @@ -136,7 +127,29 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): 'example.com', (['ip1', 'ip2'], ['alt1', 'alt2']), ttl='3456h', max_ttl='3456h') - client_mock.write.assert_has_calls(write_calls) + client_mock.secrets.pki.generate_certificate.assert_has_calls([ + mock.call( + vault_pki.CHARM_PKI_ROLE, 'example.com', + mount_point=vault_pki.CHARM_PKI_MP, + extra_params={}, + ), + mock.call( + vault_pki.CHARM_PKI_ROLE, 'example.com', + mount_point=vault_pki.CHARM_PKI_MP, + extra_params={ + 'ip_sans': 'ip1', + 'alt_names': 'alt1', + } + ), + mock.call( + vault_pki.CHARM_PKI_ROLE_CLIENT, 'example.com', + mount_point=vault_pki.CHARM_PKI_MP, + extra_params={ + 'ip_sans': 'ip1,ip2', + 'alt_names': 'alt1,alt2', + } + ), + ]) @patch.object(vault_pki, 'is_ca_ready') @patch.object(vault_pki, 'configure_pki_backend') @@ -173,7 +186,9 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): client_mock = mock.MagicMock() get_local_client.return_value = client_mock is_ca_ready.return_value = True - client_mock.write.side_effect = hvac.exceptions.InvalidRequest + client_mock.secrets.pki.generate_certificate.side_effect = ( + hvac.exceptions.InvalidRequest + ) with self.assertRaises(vault_pki.vault.VaultInvalidRequest): vault_pki.generate_certificate('server', 'example.com', [], ttl='3456h', max_ttl='3456h') @@ -183,24 +198,23 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): def test_get_csr(self, get_local_client, configure_pki_backend): client_mock = mock.MagicMock() get_local_client.return_value = client_mock - client_mock.write.return_value = { + client_mock.secrets.pki.generate_intermediate.return_value = { 'data': { 'csr': 'somecert'}} self.assertEqual(vault_pki.get_csr(), 'somecert') - client_mock.write.assert_called_once_with( - 'charm-pki-local/intermediate/generate/internal', - common_name=('Vault Intermediate Certificate Authority' - ' (charm-pki-local)'), - ttl='87599h') + client_mock.secrets.pki.generate_intermediate.assert_called_once_with( + 'internal', + 'Vault Intermediate Certificate Authority (charm-pki-local)', + extra_params={'ttl': '87599h'}, + mount_point=vault_pki.CHARM_PKI_MP) @patch.object(vault_pki, 'configure_pki_backend') @patch.object(vault_pki.vault, 'get_local_client') def test_get_csr_explicit(self, get_local_client, configure_pki_backend): client_mock = mock.MagicMock() get_local_client.return_value = client_mock - client_mock.write.return_value = { - 'data': { - 'csr': 'somecert'}} + client_mock.secrets.pki.generate_intermediate.return_value = { + 'data': {'csr': 'somecert'}} self.assertEqual( vault_pki.get_csr( ttl='2h', @@ -210,16 +224,19 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): organizational_unit='My Department', organization='My Company'), 'somecert') - client_mock.write.assert_called_once_with( - 'charm-pki-local/intermediate/generate/internal', - common_name=('Vault Intermediate Certificate Authority ' - '(charm-pki-local)'), - country='GB', - locality='here', - organization='My Company', - ou='My Department', - province='Kent', - ttl='2h') + client_mock.secrets.pki.generate_intermediate.assert_called_once_with( + 'internal', + 'Vault Intermediate Certificate Authority (charm-pki-local)', + extra_params=dict( + country='GB', + locality='here', + organization='My Company', + ou='My Department', + province='Kent', + ttl='2h' + ), + mount_point=vault_pki.CHARM_PKI_MP + ) @patch.object(vault_pki.vault, 'get_access_address') @patch.object(vault_pki.vault, 'get_local_client') @@ -227,36 +244,11 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): get_access_address.return_value = 'https://vault.local:8200' client_mock = mock.MagicMock() get_local_client.return_value = client_mock - local_url = 'https://vault.local:8200/v1/charm-pki-local' - write_calls = [ - mock.call( - 'charm-pki-local/config/urls', - issuing_certificates='{}/ca'.format(local_url), - crl_distribution_points='{}/crl'.format(local_url)), - mock.call( - 'charm-pki-local/roles/local', - allowed_domains='example.com', - allow_subdomains=True, - enforce_hostnames=False, - allow_any_name=True, - max_ttl='87598h', - server_flag=True, - client_flag=True), - mock.call( - 'charm-pki-local/roles/local-client', - allowed_domains='example.com', - allow_subdomains=True, - enforce_hostnames=False, - allow_any_name=True, - max_ttl='87598h', - server_flag=False, - client_flag=True), - ] vault_pki.upload_signed_csr('MYPEM', 'example.com') - client_mock._post.assert_called_once_with( - 'v1/charm-pki-local/intermediate/set-signed', - json={'certificate': 'MYPEM'}) - client_mock.write.assert_has_calls(write_calls) + client_mock.secrets.pki.set_signed_intermediate.\ + assert_called_once_with( + 'MYPEM', mount_point=vault_pki.CHARM_PKI_MP + ) @patch.object(vault_pki.vault, 'get_access_address') @patch.object(vault_pki.vault, 'get_local_client') @@ -267,35 +259,43 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): client_mock = mock.MagicMock() get_local_client.return_value = client_mock local_url = 'https://127.0.0.1:8200/v1/charm-pki-local' - write_calls = [ - mock.call( - 'charm-pki-local/config/urls', - issuing_certificates='{}/ca'.format(local_url), - crl_distribution_points='{}/crl'.format(local_url)), - mock.call( - 'charm-pki-local/roles/local', - allowed_domains='example.com', - allow_subdomains=True, - enforce_hostnames=False, - allow_any_name=True, - max_ttl='87598h', - server_flag=True, - client_flag=True), - mock.call( - 'charm-pki-local/roles/local-client', - allowed_domains='example.com', - allow_subdomains=True, - enforce_hostnames=False, - allow_any_name=True, - max_ttl='87598h', - server_flag=False, - client_flag=True), - ] vault_pki.upload_signed_csr('MYPEM', 'example.com') - client_mock._post.assert_called_once_with( - 'v1/charm-pki-local/intermediate/set-signed', - json={'certificate': 'MYPEM'}) - client_mock.write.assert_has_calls(write_calls) + client_mock.secrets.pki.set_signed_intermediate.\ + assert_called_once_with( + 'MYPEM', mount_point=vault_pki.CHARM_PKI_MP + ) + client_mock.secrets.pki.set_urls.assert_called_once_with({ + 'issuing_certificates': '{}/ca'.format(local_url), + 'crl_distribution_points': '{}/crl'.format(local_url), + }, mount_point=vault_pki.CHARM_PKI_MP) + client_mock.secrets.pki.create_or_update_role.assert_has_calls([ + mock.call( + vault_pki.CHARM_PKI_ROLE, + extra_params=dict( + allowed_domains='example.com', + allow_subdomains=True, + enforce_hostnames=False, + allow_any_name=True, + max_ttl='87598h', + server_flag=True, + client_flag=True, + ), + mount_point=vault_pki.CHARM_PKI_MP, + ), + mock.call( + vault_pki.CHARM_PKI_ROLE_CLIENT, + extra_params=dict( + allowed_domains='example.com', + allow_subdomains=True, + enforce_hostnames=False, + allow_any_name=True, + max_ttl='87598h', + server_flag=False, + client_flag=True, + ), + mount_point=vault_pki.CHARM_PKI_MP, + ), + ]) @patch.object(vault_pki.vault, 'get_access_address') @patch.object(vault_pki.vault, 'get_local_client') @@ -305,36 +305,11 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): get_access_address.return_value = 'https://[::1]:8200' client_mock = mock.MagicMock() get_local_client.return_value = client_mock - local_url = 'https://[::1]:8200/v1/charm-pki-local' - write_calls = [ - mock.call( - 'charm-pki-local/config/urls', - issuing_certificates='{}/ca'.format(local_url), - crl_distribution_points='{}/crl'.format(local_url)), - mock.call( - 'charm-pki-local/roles/local', - allowed_domains='example.com', - allow_subdomains=True, - enforce_hostnames=False, - allow_any_name=True, - max_ttl='87598h', - server_flag=True, - client_flag=True), - mock.call( - 'charm-pki-local/roles/local-client', - allowed_domains='example.com', - allow_subdomains=True, - enforce_hostnames=False, - allow_any_name=True, - max_ttl='87598h', - server_flag=False, - client_flag=True), - ] vault_pki.upload_signed_csr('MYPEM', 'example.com') - client_mock._post.assert_called_once_with( - 'v1/charm-pki-local/intermediate/set-signed', - json={'certificate': 'MYPEM'}) - client_mock.write.assert_has_calls(write_calls) + client_mock.secrets.pki.set_signed_intermediate.\ + assert_called_once_with( + 'MYPEM', mount_point=vault_pki.CHARM_PKI_MP + ) @patch.object(vault_pki.vault, 'get_access_address') @patch.object(vault_pki.vault, 'get_local_client') @@ -344,30 +319,6 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): get_access_address.return_value = 'https://vault.local:8200' get_local_client.return_value = client_mock local_url = 'https://vault.local:8200/v1/charm-pki-local' - write_calls = [ - mock.call( - 'charm-pki-local/config/urls', - issuing_certificates='{}/ca'.format(local_url), - crl_distribution_points='{}/crl'.format(local_url)), - mock.call( - 'charm-pki-local/roles/local', - allowed_domains='example.com', - allow_subdomains=False, - enforce_hostnames=True, - allow_any_name=False, - max_ttl='42h', - server_flag=True, - client_flag=True), - mock.call( - 'charm-pki-local/roles/local-client', - allowed_domains='example.com', - allow_subdomains=False, - enforce_hostnames=True, - allow_any_name=False, - max_ttl='42h', - server_flag=False, - client_flag=True), - ] vault_pki.upload_signed_csr( 'MYPEM', 'example.com', @@ -375,10 +326,17 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): enforce_hostnames=True, allow_any_name=False, max_ttl='42h') - client_mock._post.assert_called_once_with( - 'v1/charm-pki-local/intermediate/set-signed', - json={'certificate': 'MYPEM'}) - client_mock.write.assert_has_calls(write_calls) + client_mock.secrets.pki.set_signed_intermediate.\ + assert_called_once_with( + 'MYPEM', mount_point='charm-pki-local' + ) + client_mock.secrets.pki.set_urls.assert_called_once_with( + { + 'issuing_certificates': '{}/ca'.format(local_url), + 'crl_distribution_points': '{}/crl'.format(local_url), + }, + mount_point=vault_pki.CHARM_PKI_MP + ) @patch.object(vault_pki.vault, 'get_access_address') @patch.object(vault_pki, 'is_ca_ready') @@ -390,7 +348,8 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): is_ca_ready, get_access_address): mock_client = get_local_client.return_value - mock_client.write.return_value = {'data': {'certificate': 'cert'}} + mock_client.secrets.pki.generate_root.return_value = { + 'data': {'certificate': 'cert'}} is_ca_ready.return_value = False get_access_address.return_value = 'addr' rv = vault_pki.generate_root_ca(ttl='0h', @@ -402,35 +361,6 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): enforce_hostnames=True, max_ttl='0h') self.assertEqual(rv, 'cert') - mock_client.write.assert_has_calls([ - mock.call('charm-pki-local/root/generate/internal', - common_name='Vault Root Certificate Authority ' - '(charm-pki-local)', - ttl='0h'), - mock.call('charm-pki-local/config/urls', - issuing_certificates='addr/v1/charm-pki-local/ca', - crl_distribution_points='addr/v1/charm-pki-local/crl'), - mock.call('charm-pki-local/roles/local', - allow_any_name=True, - allowed_domains='domains', - allow_bare_domains=True, - allow_subdomains=True, - allow_glob_domains=False, - enforce_hostnames=True, - max_ttl='0h', - server_flag=True, - client_flag=True), - mock.call('charm-pki-local/roles/local-client', - allow_any_name=True, - allowed_domains='domains', - allow_bare_domains=True, - allow_subdomains=True, - allow_glob_domains=False, - enforce_hostnames=True, - max_ttl='0h', - server_flag=False, - client_flag=True), - ]) @patch.object(vault_pki.vault, 'get_access_address') @patch.object(vault_pki, 'is_ca_ready') @@ -467,9 +397,8 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): vault_pki.tune_pki_backend(ttl='3456h', max_ttl='3456h') is_backend_mounted.assert_called_with(mock_client, vault_pki.CHARM_PKI_MP) - mock_client.tune_secret_backend.assert_called_with( - backend_type='pki', - mount_point=vault_pki.CHARM_PKI_MP, + mock_client.sys.tune_mount_configuration.assert_called_with( + path=vault_pki.CHARM_PKI_MP, max_lease_ttl='3456h', default_lease_ttl='3456h' ) @@ -478,7 +407,7 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): def test_update_roles(self, get_local_client): mock_client = mock.MagicMock() get_local_client.return_value = mock_client - mock_client.read.return_value = { + mock_client.secrets.pki.read_role.return_value = { 'data': { 'allow_any_name': True, 'allowed_domains': 'domains', @@ -492,29 +421,35 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase): } } vault_pki.update_roles(max_ttl='20h') - mock_client.write.assert_has_calls([ - mock.call('{}/roles/{}'.format( - vault_pki.CHARM_PKI_MP, vault_pki.CHARM_PKI_ROLE), - allow_any_name=True, - allowed_domains='domains', - allow_bare_domains=True, - allow_subdomains=True, - allow_glob_domains=False, - enforce_hostnames=True, - max_ttl='20h', - server_flag=True, - client_flag=True), - mock.call('{}/roles/{}'.format( - vault_pki.CHARM_PKI_MP, vault_pki.CHARM_PKI_ROLE_CLIENT), - allow_any_name=True, - allowed_domains='domains', - allow_bare_domains=True, - allow_subdomains=True, - allow_glob_domains=False, - enforce_hostnames=True, - max_ttl='20h', - server_flag=False, - client_flag=True), + mock_client.secrets.pki.create_or_update_role.assert_has_calls([ + mock.call( + vault_pki.CHARM_PKI_ROLE, + mount_point=vault_pki.CHARM_PKI_MP, + extra_params=dict( + allow_any_name=True, + allowed_domains='domains', + allow_bare_domains=True, + allow_subdomains=True, + allow_glob_domains=False, + enforce_hostnames=True, + max_ttl='20h', + server_flag=True, + client_flag=True) + ), + mock.call( + vault_pki.CHARM_PKI_ROLE_CLIENT, + mount_point=vault_pki.CHARM_PKI_MP, + extra_params=dict( + allow_any_name=True, + allowed_domains='domains', + allow_bare_domains=True, + allow_subdomains=True, + allow_glob_domains=False, + enforce_hostnames=True, + max_ttl='20h', + server_flag=False, + client_flag=True) + ), ]) @patch.object(vault_pki.hookenv, 'leader_get') diff --git a/unit_tests/test_reactive_vault_handlers.py b/unit_tests/test_reactive_vault_handlers.py index f7c966f..f25cea1 100644 --- a/unit_tests/test_reactive_vault_handlers.py +++ b/unit_tests/test_reactive_vault_handlers.py @@ -670,8 +670,8 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase): hvac_client = mock.MagicMock() _vault.get_client.return_value = hvac_client # Vault is up and running, init'ed and unsealed - hvac_client.is_initialized.return_value = True - hvac_client.is_sealed.return_value = False + hvac_client.sys.is_initialized.return_value = True + hvac_client.sys.is_sealed.return_value = False self.service_running.return_value = True _vault.get_local_charm_access_role_id.return_value = 'local-approle' @@ -687,7 +687,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase): handlers.configure_secrets_backend() - hvac_client.auth_approle.assert_called_once_with('local-approle') + hvac_client.auth.approle.login.assert_called_once_with('local-approle') _vault.configure_secret_backend.assert_has_calls([ mock.call(hvac_client, name='charm-vaultlocker'), mock.call(hvac_client, name='charm-supersecrets'), @@ -883,7 +883,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase): def _set_sealed(self, _vault, status): hvac_client = mock.MagicMock() _vault.get_client.return_value = hvac_client - hvac_client.is_sealed.return_value = status + hvac_client.sys.is_sealed.return_value = status @mock.patch.object(handlers, 'client_approle_authorized') @mock.patch.object(handlers, 'vault')