Merge "Implement cert cache for vault units (v4)" into stable/1.7

This commit is contained in:
Zuul 2024-01-10 15:52:54 +00:00 committed by Gerrit Code Review
commit 2fc73dbbcd
5 changed files with 1415 additions and 28 deletions

View File

@ -1,3 +1,8 @@
import json
import re
from subprocess import check_output, CalledProcessError
from tempfile import NamedTemporaryFile
import hvac
import charmhelpers.contrib.network.ip as ch_ip
@ -337,3 +342,446 @@ def update_roles(**kwargs):
local.update(**kwargs)
del local['server_flag']
write_roles(client, **local)
def is_cert_from_vault(cert, name=None):
"""Return True if the cert is issued by vault and not revoked.
Looking at the cert, check to see if it was issued by Vault and not on the
revoked list. In order to do this, the cert must be in x509 format as
openssl is used to extract the ID of the cert. Then the certificate is
extracted from vault and the signatures compared.
:param cert: the certificate in x509 form
:type cert: str
:param name: the mount point in value, default CHARM_PKI_MP
:type name: str
:returns: True if issued by vault, False if unknown.
:raises VaultDown: if vault is down.
:raises VaultNotReady: if vault is sealed.
:raises VaultError: for any other vault issue.
"""
# first get the ID from the client
serial = get_serial_number_from_cert(cert)
if serial is None:
return False
try:
# now get a list of serial numbers from vault.
client = vault.get_local_client()
if not name:
name = CHARM_PKI_MP
vault_certs_response = client.secrets.pki.list_certificates(
mount_point=name)
vault_certs = [k.replace('-', '').upper()
for k in vault_certs_response['data']['keys']]
if serial not in vault_certs:
hookenv.log("Certificate with serial {} not issed by vault."
.format(serial), level=hookenv.DEBUG)
return False
revoked_serials = get_revoked_serials_from_vault(name)
if serial in revoked_serials:
hookenv.log("Serial {} is revoked.".format(serial),
level=hookenv.DEBUG)
return False
return True
except (
vault.hvac.exceptions.InvalidPath,
vault.hvac.exceptions.InternalServerError,
vault.hvac.exceptions.VaultDown,
vault.VaultNotReady,
):
# vault is not available for some reason, return None, None as nothing
# else is particularly useful here.
return False
except Exception as e:
hookenv.log("General failure verifying cert: {}".format(str(e)),
level=hookenv.DEBUG)
return False
def get_serial_number_from_cert(cert, name=None):
"""Extract the serial number from the cert, or return None.
:param cert: the certificate in x509 form
:type cert: str
:returns: the cert serial number or None.
:rtype: str | None
"""
with NamedTemporaryFile() as f:
f.write(cert.encode())
f.flush()
command = ["openssl", "x509", "-in", f.name, "-noout", "-serial"]
try:
# output in form of 'serial=xxxxx'
output = check_output(command).decode().strip()
serial = output.split("=")[1]
return serial
except CalledProcessError as e:
hookenv.log("Couldn't process certificate: reason: {}"
.format(str(e)),
level=hookenv.DEBUG)
except (TypeError, IndexError):
hookenv.log(
"Couldn't extract serial number from passed certificate",
level=hookenv.DEBUG)
return None
def get_revoked_serials_from_vault(name=None):
"""Get a list of revoked serial numbers from vault.
This fetches the CRL from vault; this is in PEM format. We ought to use
python cryptography.x509.load_pem_x509_crl(), but adding cryptography
requires converting the charm to binary, and seems a lot for one function.
Thus, the format for no certificates revoked is:
.. code-block:: text
Certificate Revocation List (CRL):
Version 2 (0x1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = Vault Intermediate Certificate Authority ...
Last Update: Jul 17 11:58:57 2023 GMT
Next Update: Jul 20 11:58:57 2023 GMT
No Revoked Certificates.
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
...
And for two (and the pattern repeats):
.. code-block:: text
Certificate Revocation List (CRL):
Version 2 (0x1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = Vault Intermediate Certificate Authority ...
Last Update: Jul 18 11:38:17 2023 GMT
Next Update: Jul 21 11:38:17 2023 GMT
Revoked Certificates:
Serial Number: 6EAE52225CB7AB452F37D4FBAC127DDF9542D3DC
Revocation Date: Jul 18 11:38:17 2023 GMT
Serial Number: 78FBEEE4E419C5A335113E4F1EF41F463534B698
Revocation Date: Jul 18 11:33:36 2023 GMT
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
Thus we just need to grep the output for "Serial Number:"
:param name: the mount point in value, default CHARM_PKI_MP
:type name: str
:returns: a list of serial numbers, uppercase, no hyphens
:rtype: List[str]
:raises VaultDown: if vault is down.
:raises VaultNotReady: if vault is sealed.
:raises VaultError: for any other vault issue.
:raises subprocess.CalledProcessError: if openssl command fails
"""
client = vault.get_local_client()
revoked_certs_response = client.secrets.pki.read_crl(mount_point=name)
with NamedTemporaryFile() as f:
f.write(revoked_certs_response.encode())
f.flush()
command = ["openssl", "crl", "-in", f.name, "-noout", "-text"]
output = check_output(command).decode().strip()
pattern = re.compile(r"Serial Number: (\S+)$")
serials = []
# for line in output.split("\n"):
for line in output.splitlines():
match = pattern.match(line.strip())
if match:
serials.append(match[1])
return serials
class CertCache:
"""A class to store the cert and key for a request.
This class provides a mechanism to CRUD a cached pair of (cert, key) in
storage, which is as loosely coupled to leader storage as possible.
As the key and cert is stored in leader settings, it's available across the
units and therefore, any unit can access the key and cert for any unit that
is related to the application.
The actually storing of the key and cert is done in as flat a way as
possible in leader-settings. This is to minimise the size of the
get and store operations for units that might have many certificate
requests. The key and cert are stored as values to a key which is
constructed from the unit_name, publish_key, common_name and item. See
PUBLISH_KEY_FORMAT for details.
Although, it has a dependency on the request (from tls_certificates), this
was deemed acceptable to keep the interface obvious and pleasing to use.
"""
PUBLISH_KEY_FORMAT = "pki:{unit_name}:{publish_key}:{common_name}:{item}"
PUBLISH_KEY_PREFIX = "pki:{unit_name}:"
TOP_LEVEL_PUBLISH_KEY = "top_level_publish_key"
def __init__(self, request):
"""Initialise a proxy for the the cert and key in leader-settings.
:param request: the request from which the cert/cache is cached.
:type request: tls_certificates_common.CertificateRequest
"""
self._request = request
def _cache_key_for(self, item):
"""
Return a cache key for the request by the item.
:param item: the item to return a key for, either 'cert' or 'key'
:type item: str
:returns: the unique key for the unit, request, and item
:rtype: str
"""
assert item in ('cert', 'key'), "Error in argument passed"
if self._request._is_top_level_server_cert:
return self.PUBLISH_KEY_FORMAT.format(
unit_name=self._request.unit_name,
publish_key=self.TOP_LEVEL_PUBLISH_KEY,
common_name=self._request.common_name,
item=item)
else:
return self.PUBLISH_KEY_FORMAT.format(
unit_name=self._request.unit_name,
publish_key=self._request._publish_key,
common_name=self._request.common_name,
item=item)
@staticmethod
def _fetch(key):
"""Fetch from the storage using a store pre key and key.
Note the _store() method dumps it as json so it is fetched as json.
:param key: the key value to fetch from leader settings
:type key: str
:returns: the value from leader settings or ""
:rtype: str
"""
value = hookenv.leader_get(key)
if value:
return json.loads(value)
return ""
@staticmethod
def _store(key, value):
"""Store a value by key into the actual storage.
:param key: the key value to set in leader settings
:type key: str
:param value: the value to store.
:type value: str
:raises: RuntimeError if not the leader
:raises: TypeError if value couldn't be converted.
"""
try:
hookenv.leader_set({key: json.dumps(value)})
except TypeError:
raise
except Exception as e:
raise RuntimeError(str(e))
@staticmethod
def _clear(key):
"""Explicitly clear a valye in the actual storage.
:param key: the key value to clear.
:type key: str
:raises: RuntimeError if not the leader
:raises: TypeError if value couldn't be converted.
"""
try:
hookenv.leader_set({key: None})
except Exception as e:
raise RuntimeError(str(e))
def clear(self):
self._clear(self._cache_key_for('key'))
self._clear(self._cache_key_for('cert'))
@property
def key(self):
"""Get the key."""
return self._fetch(self._cache_key_for('key'))
@key.setter
def key(self, key_value):
"""Set the key value."""
self._store(self._cache_key_for('key'), key_value)
@property
def cert(self):
"""The the cert."""
return self._fetch(self._cache_key_for('cert'))
@cert.setter
def cert(self, cert_value):
"""Set the cert value."""
self._store(self._cache_key_for('cert'), cert_value)
@classmethod
def remove_all_for(cls, unit_name):
"""Remove all the cached keys for a unit name.
This is an awkward function, as the cache in leader settings is 'flat'
to ensure that the set payloads are as small as possible.
This iterates through all the keys and if they match the prefix for the
unit_name, it clears them.
:param unit_name: The unit_name to clear.
:type unit_name: str
"""
prefix = cls.PUBLISH_KEY_PREFIX.format(unit_name=unit_name)
leader_keys = (cls._fetch(None) or {}).keys()
for key in leader_keys:
if key.startswith(prefix):
cls._clear(key)
def find_cert_in_cache(request):
"""Return certificate and key from cache that match the request.
Returned certificate is validated against the current CA cert. If CA cert
is missing then the function returns (None, None).
If the certificate can't be found in vault, then a warning is logged, but
the cert is still returned as it is in leader_settings; the leader may
decide to remove it at a later date.
:param request: Request for certificate from "client" unit.
:type request: tls_certificates_common.CertificateRequest
:return: Certificate and private key from cache
:rtype: (str, str) | (None, None)
"""
request_pki_cache = CertCache(request)
cert = request_pki_cache.cert
key = request_pki_cache.key
if cert is None or key is None:
return None, None
if not is_cert_from_vault(cert, name=CHARM_PKI_MP):
hookenv.log('Certificate from cache for "{}" (cn: "{}") was not found '
'in vault, but is in the cache. Using, but may not be '
'valid.'.format(request.unit_name, request.common_name),
level=hookenv.WARNING)
return cert, key
def update_cert_cache(request, cert, key):
"""Store certificate and key in the cache.
Stored values are associated with the request from "client" unit,
so it can be later retrieved when the request is handled again.
:param request: Request for certificate from "client" unit.
:type request: tls_certificates_common.CertificateRequest
:param cert: Issued certificate for the "client" request (in PEM format)
:type cert: str
:param key: Issued private key from the "client" request (in PEM format)
:type key: str
:return: None
"""
request_pki_cache = CertCache(request)
hookenv.log('Saving certificate for "{}" '
'(cn: "{}") into cache.'.format(request.unit_name,
request.common_name),
hookenv.DEBUG)
request_pki_cache.key = key
request_pki_cache.cert = cert
def remove_unit_from_cache(unit_name):
"""Clear certificates and keys related to the unit from the cache.
:param unit_name: Name of the unit to be removed from the cache.
:type unit_name: str
:return: None
"""
hookenv.log('Removing certificates for unit "{}" from '
'cache.'.format(unit_name), hookenv.DEBUG)
CertCache.remove_all_for(unit_name)
def populate_cert_cache(tls_endpoint):
"""Store previously issued certificates in the cache.
This function is used when vault charm is upgraded from older version
that may not have a certificate cache to a version that has it. It
goes through all previously issued certificates and stores them in
cache.
:param tls_endpoint: Endpoint of "certificates" relation
:type tls_endpoint: interface_tls_certificates.provides.TlsProvides
:return: None
"""
hookenv.log(
"Populating certificate cache with data from relations", hookenv.INFO
)
for request in tls_endpoint.all_requests:
try:
if request._is_top_level_server_cert:
relation_data = request._unit.relation.to_publish_raw
cert = relation_data[request._server_cert_key]
key = relation_data[request._server_key_key]
else:
relation_data = request._unit.relation.to_publish
cert = relation_data[request._publish_key][
request.common_name
]['cert']
key = relation_data[request._publish_key][
request.common_name
]['key']
except (KeyError, TypeError):
if request._is_top_level_server_cert:
cert_id = request._server_cert_key
else:
cert_id = request.common_name
hookenv.log(
'Certificate "{}" (or associated key) issued for unit "{}" '
'not found in relation data.'.format(
cert_id, request._unit.unit_name
),
hookenv.WARNING
)
continue
update_cert_cache(request, cert, key)
def set_global_client_cert(bundle):
"""Set the global cert for all units in the app.
:param bundle: the bundle returned from generate_certificates()
:type bundle: Dict[str, str]
:raises: RuntimeError if leader_set fails.
:raises: TypeError if the bundle can't be serialised.
"""
try:
hookenv.leader_set(
{'charm.vault.global-client-cert': json.dumps(bundle)})
except TypeError:
raise
except Exception as e:
raise RuntimeError("Couldn't run leader_settings: {}".format(str(e)))
def get_global_client_cert():
"""Return the bundle returned from leader_settings.
Will return an empty dictionary if key is not present.
:returns: the bundle previously stored, or {}
:rtype: Dict[str, str]
"""
bundle = hookenv.leader_get('charm.vault.global-client-cert')
if bundle:
return json.loads(bundle)
return {}

View File

@ -35,6 +35,7 @@ from charmhelpers.core.hookenv import (
log,
network_get_primary_address,
open_port,
remote_unit,
status_set,
unit_private_ip,
)
@ -293,6 +294,11 @@ def upgrade_charm():
remove_state('vault.nrpe.configured')
remove_state('vault.ssl.configured')
remove_state('vault.requested-lb')
# When upgrading from version of a charm that did not have a certificate
# cache, we need to populate the cache with already issued certificates.
# Otherwise the non-leader units would not be able to sync their
# certificate data via cache.
set_flag('needs-cert-cache-repopulation')
@when_not("is-update-status-hook")
@ -977,7 +983,8 @@ def publish_ca_info():
'certificates.available')
@when_not('config.changed')
def publish_global_client_cert():
"""
"""publish the global certificate.
This is for backwards compatibility with older tls-certificate clients
only. Obviously, it's not good security / design to have clients sharing
a certificate, but it seems that there are clients that depend on this
@ -988,23 +995,104 @@ def publish_global_client_cert():
log("Vault not authorized: Skipping publish_global_client_cert",
"WARNING")
return
cert_created = is_flag_set('charm.vault.global-client-cert.created')
reissue_requested = is_flag_set('certificates.reissue.global.requested')
tls = endpoint_from_flag('certificates.available')
if not cert_created or reissue_requested:
bundle = vault_pki.get_global_client_cert()
certificate_present = "certificate" in bundle and "private_key" in bundle
if not certificate_present or reissue_requested:
ttl = config()['default-ttl']
max_ttl = config()['max-ttl']
bundle = vault_pki.generate_certificate('client',
'global-client',
[], ttl, max_ttl)
unitdata.kv().set('charm.vault.global-client-cert', bundle)
vault_pki.set_global_client_cert(bundle)
set_flag('charm.vault.global-client-cert.created')
clear_flag('certificates.reissue.global.requested')
else:
bundle = unitdata.kv().get('charm.vault.global-client-cert')
tls.set_client_cert(bundle['certificate'], bundle['private_key'])
@when_not("is-update-status-hook")
@when('certificates.available')
@when('charm.vault.ca.ready')
@when('leadership.is_leader')
@when('needs-cert-cache-repopulation')
def repopulate_cert_cache():
"""Force repopulation of cert cache on the leader.
Certain circumstances such as 'upgrade-charm' hook should force the leader
to populate the cert cache, so then non-leaders will follow in the
'sync_cert_from_cache' method."""
tls = endpoint_from_flag('certificates.available')
if tls:
vault_pki.populate_cert_cache(tls)
clear_flag('needs-cert-cache-repopulation')
@when_not("is-update-status-hook")
@when("certificates.available")
@when_not('leadership.is_leader')
def sync_cert_from_cache():
"""Sync cert and key data in the tls-certificate relation.
Non-leader units should keep the relation data up-to-date according
to the data from PKI cache that's maintained by the leader. This ensures
that "client" units can use data from any of the related vault units to
receive valid keys and certificates.
"""
tls = endpoint_from_flag('certificates.available')
# propagate the ca stored by the leader if it can be obtained.
ca = vault_pki.get_ca()
if ca:
tls.set_ca(ca)
ca_chain = None
# propagate the chain if it can be obtained.
try:
# this might fail if we were restarted and need to be unsealed
ca_chain = vault_pki.get_chain()
except (
vault.hvac.exceptions.InvalidPath,
vault.hvac.exceptions.InternalServerError,
vault.hvac.exceptions.VaultDown,
vault.VaultNotReady,
) as e:
log("Couldn't get the chain from vault. Reason: {}".format(str(e)))
else:
tls.set_chain(ca_chain)
# propagate global client cert from cache
bundle = vault_pki.get_global_client_cert()
if bundle.get('certificate') and bundle.get('private_key'):
tls.set_client_cert(bundle['certificate'], bundle['private_key'])
# update certificate data in relations
cert_requests = tls.all_requests
for request in cert_requests:
cache_cert, cache_key = vault_pki.find_cert_in_cache(request)
if cache_cert and cache_key:
request.set_cert(cache_cert, cache_key)
@hook('certificates-relation-departed')
def cert_client_leaving(relation):
"""Remove certs and keys of the departing unit from cache.
Note: this uses the hook as the interface code doesn't provide a mechanism
to notify departing units.
"""
if is_flag_set('leadership.is_leader'):
# Due to certificates requests replacing "/" in the unit
# name with "_" (see: tls_certificates_common.CertificateRequest),
# we must emulate the same behavior when removing unit certs from
# cache.
departing_unit = remote_unit()
log("Removing certificates for {} from cache.".format(departing_unit))
unit_name = departing_unit.replace('/', '_')
vault_pki.remove_unit_from_cache(unit_name)
@when_not("is-update-status-hook")
@when('leadership.is_leader',
'charm.vault.ca.ready',
@ -1032,6 +1120,7 @@ def create_certs():
processed_applications.append(request.application_name)
else:
cert_type = request.cert_type
try:
ttl = config()['default-ttl']
max_ttl = config()['max-ttl']
@ -1039,6 +1128,9 @@ def create_certs():
request.common_name,
request.sans, ttl, max_ttl)
request.set_cert(bundle['certificate'], bundle['private_key'])
vault_pki.update_cert_cache(request,
bundle["certificate"],
bundle["private_key"])
except vault.VaultInvalidRequest as e:
log(str(e), level=ERROR)
continue # TODO: report failure back to client

View File

@ -31,10 +31,7 @@ target_deploy_status:
tests:
- zaza.openstack.charm_tests.vault.tests.VaultTest
# This second run of the tests is to ensure that Vault can handle updating the
# root CA in Vault with a refreshed CSR and won't end up in a hook-error
# state. (LP: #1866150).
- zaza.openstack.charm_tests.vault.tests.VaultTest
- zaza.openstack.charm_tests.vault.tests.VaultCacheTest
tests_options:
force_deploy:

View File

@ -1,5 +1,7 @@
import collections
import json
from unittest import mock
from unittest.mock import patch
from unittest.mock import call, patch, MagicMock
import hvac
@ -515,3 +517,625 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
server_flag=False,
client_flag=True),
])
@patch.object(vault_pki, 'get_serial_number_from_cert')
def test_is_cert_from_vault_no_serial(
self,
mock_get_serial_number_from_cert,
):
mock_get_serial_number_from_cert.return_value = None
self.assertFalse(vault_pki.is_cert_from_vault('the-cert'))
mock_get_serial_number_from_cert.assert_called_once_with('the-cert')
@patch.object(vault_pki, 'get_serial_number_from_cert')
@patch.object(vault_pki.vault, 'get_local_client')
@patch.object(vault_pki.hookenv, 'log')
def test_is_cert_from_vault_not_from_vault(
self,
mock_log,
mock_get_local_client,
mock_get_serial_number_from_cert,
):
mock_get_serial_number_from_cert.return_value = "1234567890"
mock_client = MagicMock()
mock_get_local_client.return_value = mock_client
mock_client.secrets.pki.list_certificates.return_value = {
"data": {
"keys": []
}
}
self.assertFalse(
vault_pki.is_cert_from_vault('the-cert', name='a-name'))
mock_get_serial_number_from_cert.assert_called_once_with('the-cert')
mock_client.secrets.pki.list_certificates.assert_called_once_with(
mount_point='a-name')
mock_log.assert_called_once_with(
"Certificate with serial 1234567890 not issed by vault.",
level=vault_pki.hookenv.DEBUG
)
@patch.object(vault_pki, 'get_serial_number_from_cert')
@patch.object(vault_pki.vault, 'get_local_client')
@patch.object(vault_pki, 'get_revoked_serials_from_vault')
@patch.object(vault_pki.hookenv, 'log')
def test_is_cert_from_vault_not_revoked_serial(
self,
mock_log,
mock_get_revoked_serials_from_vault,
mock_get_local_client,
mock_get_serial_number_from_cert,
):
mock_get_serial_number_from_cert.return_value = "1234567890"
mock_client = MagicMock()
mock_get_local_client.return_value = mock_client
mock_client.secrets.pki.list_certificates.return_value = {
"data": {
"keys": ["1234567890"]
}
}
mock_get_revoked_serials_from_vault.return_value = []
self.assertTrue(
vault_pki.is_cert_from_vault('the-cert', name='a-name'))
mock_get_revoked_serials_from_vault.assert_called_once_with('a-name')
mock_log.assert_not_called()
@patch.object(vault_pki, 'get_serial_number_from_cert')
@patch.object(vault_pki.vault, 'get_local_client')
@patch.object(vault_pki, 'get_revoked_serials_from_vault')
@patch.object(vault_pki.hookenv, 'log')
def test_is_cert_from_vault_revoked_serial(
self,
mock_log,
mock_get_revoked_serials_from_vault,
mock_get_local_client,
mock_get_serial_number_from_cert,
):
mock_get_serial_number_from_cert.return_value = "1234567890"
mock_client = MagicMock()
mock_get_local_client.return_value = mock_client
mock_client.secrets.pki.list_certificates.return_value = {
"data": {
"keys": ["12-34-56-78-90"]
}
}
mock_get_revoked_serials_from_vault.return_value = [
"DEADBEEF",
"1234567890",
"notme",
]
self.assertFalse(
vault_pki.is_cert_from_vault('the-cert', name='a-name'))
mock_log.assert_called_once_with(
"Serial 1234567890 is revoked.", level=vault_pki.hookenv.DEBUG)
@patch.object(vault_pki, 'get_serial_number_from_cert')
@patch.object(vault_pki.vault, 'get_local_client')
@patch.object(vault_pki, 'get_revoked_serials_from_vault')
@patch.object(vault_pki.hookenv, 'log')
def test_is_cert_from_vault_raised_exceptions(
self,
mock_log,
mock_get_revoked_serials_from_vault,
mock_get_local_client,
mock_get_serial_number_from_cert,
):
mock_get_serial_number_from_cert.return_value = "1234567890"
mock_client = MagicMock()
mock_get_local_client.return_value = mock_client
mock_client.secrets.pki.list_certificates.return_value = {
"data": {
"keys": ["12-34-56-78-90"]
}
}
mock_get_revoked_serials_from_vault.return_value = [
"DEADBEEF",
"1234567890",
"notme",
]
def make_raiser(exc):
def _raiser(*args, **kwargs):
raise exc
return _raiser
exceptions = [
vault_pki.vault.hvac.exceptions.InvalidPath('wrong-path'),
vault_pki.vault.hvac.exceptions.InternalServerError('bang'),
vault_pki.vault.hvac.exceptions.VaultDown(),
vault_pki.vault.VaultNotReady("really-not-ready"),
]
for exception in exceptions:
mock_get_local_client.side_effect = make_raiser(exception)
self.assertFalse(
vault_pki.is_cert_from_vault('the-cert', name='a-name'))
mock_log.assert_not_called()
class OtherException(Exception):
pass
mock_get_local_client.side_effect = make_raiser(
OtherException("on noes"))
self.assertFalse(
vault_pki.is_cert_from_vault('the-cert', name='a-name'))
mock_log.assert_called_once_with(
"General failure verifying cert: on noes",
level=vault_pki.hookenv.DEBUG)
@patch.object(vault_pki, 'check_output')
@patch.object(vault_pki, 'NamedTemporaryFile')
@patch.object(vault_pki.hookenv, 'log')
def test_get_serial_number_from_cert(
self,
mock_log,
mock_named_temporary_file,
mock_check_output
):
mock_f = MagicMock()
mock_f.name = "filename"
mock_named_temporary_file.return_value.__enter__.return_value = mock_f
mock_check_output.return_value = b" serial=12345678 "
self.assertEqual(vault_pki.get_serial_number_from_cert(
"this is a cert"), "12345678")
mock_f.write.assert_called_once_with(b"this is a cert")
mock_f.flush.assert_called_once_with()
mock_check_output.assert_called_once_with(
['openssl', 'x509', '-in', 'filename', '-noout', '-serial'])
@patch.object(vault_pki, 'check_output')
@patch.object(vault_pki, 'NamedTemporaryFile')
@patch.object(vault_pki.hookenv, 'log')
def test_get_serial_number_from_cert_subprocess_error(
self,
mock_log,
mock_named_temporary_file,
mock_check_output
):
mock_f = MagicMock()
mock_f.name = "filename"
mock_named_temporary_file.return_value.__enter__.return_value = mock_f
mock_check_output.return_value = b" serial=12345678 "
def _raise(*args, **kwargs):
raise vault_pki.CalledProcessError(cmd="bang", returncode=1)
mock_check_output.side_effect = _raise
self.assertEqual(vault_pki.get_serial_number_from_cert(
"this is a cert"), None)
mock_log.assert_called_once_with(
"Couldn't process certificate: reason: Command 'bang' returned "
"non-zero exit status 1.",
level=vault_pki.hookenv.DEBUG)
@patch.object(vault_pki, 'check_output')
@patch.object(vault_pki, 'NamedTemporaryFile')
@patch.object(vault_pki.hookenv, 'log')
def test_get_serial_number_from_cert_other_error(
self,
mock_log,
mock_named_temporary_file,
mock_check_output
):
mock_f = MagicMock()
mock_f.name = "filename"
mock_named_temporary_file.return_value.__enter__.return_value = mock_f
mock_check_output.return_value = b"thing"
self.assertEqual(vault_pki.get_serial_number_from_cert(
"this is a cert"), None)
mock_log.assert_called_once_with(
"Couldn't extract serial number from passed certificate",
level=vault_pki.hookenv.DEBUG)
@patch.object(vault_pki, 'check_output')
@patch.object(vault_pki, 'NamedTemporaryFile')
@patch.object(vault_pki.vault, 'get_local_client')
def test_get_revoked_serials_from_vault_no_serials(
self,
mock_get_local_client,
mock_named_temporary_file,
mock_check_output
):
mock_f = MagicMock()
mock_f.name = "filename"
mock_named_temporary_file.return_value.__enter__.return_value = mock_f
mock_check_output.return_value = b"\n\n\n"
mock_client = MagicMock()
mock_get_local_client.return_value = mock_client
mock_client.secrets.pki.read_crl.return_value = "the crl"
self.assertEqual(vault_pki.get_revoked_serials_from_vault(
name=vault_pki.CHARM_PKI_MP), [])
mock_check_output.assert_called_once_with(
['openssl', 'crl', '-in', 'filename', '-noout', '-text'])
mock_f.write.assert_called_once_with(b"the crl")
mock_client.secrets.pki.read_crl.assert_called_once_with(
mount_point=vault_pki.CHARM_PKI_MP)
@patch.object(vault_pki, 'check_output')
@patch.object(vault_pki, 'NamedTemporaryFile')
@patch.object(vault_pki.vault, 'get_local_client')
def test_get_revoked_serials_from_vault_some_serials(
self,
mock_get_local_client,
mock_named_temporary_file,
mock_check_output
):
mock_f = MagicMock()
mock_f.name = "filename"
mock_named_temporary_file.return_value.__enter__.return_value = mock_f
mock_check_output.return_value = "\n".join([
"Some interesting line",
" Serial Number: DEADBEEF",
"another interesting line.",
" and another.",
" Serial Number: 1234567890",
" and finally this one."
]).encode()
mock_client = MagicMock()
mock_get_local_client.return_value = mock_client
mock_client.secrets.pki.read_crl.return_value = "the crl"
self.assertEqual(vault_pki.get_revoked_serials_from_vault(
name=vault_pki.CHARM_PKI_MP), ['DEADBEEF', '1234567890'])
def test_certcache__init(self):
item = vault_pki.CertCache('a-request')
self.assertEqual(item._request, 'a-request')
class ReadOnlyDict(collections.OrderedDict):
"""The ReadOnly dictionary accessible via attributes."""
def __init__(self, data):
for k, v in data.items():
super().__setitem__(k, v)
def __getitem__(self, key):
return super().__getitem__(key)
def __setattr__(self, *_):
raise TypeError("{} does not allow setting of attributes"
.format(self.__class__.__name__))
def __setitem__(self, *_):
raise TypeError("{} does not allow setting of items"
.format(self.__class__.__name__))
__getattr__ = __getitem__
def _default_request(self):
return self.ReadOnlyDict({
'unit_name': 'the-name',
'_is_top_level_server_cert': False,
'_publish_key': "subbed",
'common_name': 'cn1'
})
def test_certcache__cache_key_for(self):
request = self.ReadOnlyDict({
'unit_name': 'the-name',
'_is_top_level_server_cert': True,
'_publish_key': None,
'common_name': 'cn1'
})
self.assertEqual(vault_pki.CertCache(request)._cache_key_for('cert'),
"pki:the-name:top_level_publish_key:cn1:cert")
self.assertEqual(vault_pki.CertCache(request)._cache_key_for('key'),
"pki:the-name:top_level_publish_key:cn1:key")
request = self._default_request()
self.assertEqual(vault_pki.CertCache(request)._cache_key_for('cert'),
"pki:the-name:subbed:cn1:cert")
self.assertEqual(vault_pki.CertCache(request)._cache_key_for('key'),
"pki:the-name:subbed:cn1:key")
with self.assertRaises(AssertionError):
vault_pki.CertCache(request)._cache_key_for('thing')
@patch.object(vault_pki.hookenv, 'leader_get')
def test_certcache__fetch(self, mock_leader_get):
mock_leader_get.return_value = None
request = self._default_request()
self.assertEqual(vault_pki.CertCache(request)._fetch("mine"), "")
mock_leader_get.assert_called_once_with('mine')
mock_leader_get.reset_mock()
mock_leader_get.return_value = '"the-value"'
self.assertEqual(vault_pki.CertCache(request)._fetch("mine"),
'the-value')
@patch.object(vault_pki.hookenv, 'leader_set')
def test_certcache__store(self, mock_leader_set):
request = self._default_request()
vault_pki.CertCache(request)._store("mine", "a value")
mock_leader_set.assert_called_once_with({"mine": '"a value"'})
# type error
class A:
pass
with self.assertRaises(TypeError):
vault_pki.CertCache(request)._store("mine", A())
# leader-set failure (subprocess call!)
def _raise(*args, **kwargs):
raise vault_pki.CalledProcessError(cmd="bang", returncode=1)
mock_leader_set.side_effect = _raise
with self.assertRaises(RuntimeError):
vault_pki.CertCache(request)._store("mine", "thing")
@patch.object(vault_pki.hookenv, 'leader_set')
def test_certcache__clear(self, mock_leader_set):
request = self._default_request()
vault_pki.CertCache(request)._clear("mine")
mock_leader_set.assert_called_once_with({"mine": None})
# leader-set failure (subprocess call!)
def _raise(*args, **kwargs):
raise vault_pki.CalledProcessError(cmd="bang", returncode=1)
mock_leader_set.side_effect = _raise
with self.assertRaises(RuntimeError):
vault_pki.CertCache(request)._clear("mine")
@patch.object(vault_pki.CertCache, '_clear')
def test_certcache_clear(self, mock__clear):
request = self._default_request()
vault_pki.CertCache(request).clear()
mock__clear.assert_has_calls([
call('pki:the-name:subbed:cn1:key'),
call('pki:the-name:subbed:cn1:cert'),
])
# leader-set failure (subprocess call!)
def _raise(*args, **kwargs):
raise RuntimeError("bang")
mock__clear.side_effect = _raise
with self.assertRaises(RuntimeError):
vault_pki.CertCache(request).clear()
@patch.object(vault_pki.CertCache, '_store')
@patch.object(vault_pki.CertCache, '_fetch')
@patch.object(vault_pki.CertCache, '_cache_key_for')
def test_certcache__key_property(
self,
mock__cache_key_for,
mock__fetch,
mock__store,
):
request = self._default_request()
mock__cache_key_for.return_value = "cache-key"
mock__fetch.return_value = "the-value"
# read
self.assertEqual(vault_pki.CertCache(request).key, "the-value")
mock__cache_key_for.assert_called_once_with('key')
mock__fetch.assert_called_once_with('cache-key')
# write
vault_pki.CertCache(request).key = 'new-value'
mock__store.assert_called_once_with('cache-key', 'new-value')
@patch.object(vault_pki.CertCache, '_store')
@patch.object(vault_pki.CertCache, '_fetch')
@patch.object(vault_pki.CertCache, '_cache_key_for')
def test_certcache__cert_property(
self,
mock__cache_key_for,
mock__fetch,
mock__store,
):
request = self._default_request()
mock__cache_key_for.return_value = "cache-key"
mock__fetch.return_value = "the-value"
# read
self.assertEqual(vault_pki.CertCache(request).cert, "the-value")
mock__cache_key_for.assert_called_once_with('cert')
mock__fetch.assert_called_once_with('cache-key')
# write
vault_pki.CertCache(request).cert = 'new-value'
mock__store.assert_called_once_with('cache-key', 'new-value')
@patch.object(vault_pki.CertCache, '_clear')
@patch.object(vault_pki.CertCache, '_fetch')
def test_certcache__remove_all_for(
self,
mock__fetch,
mock__clear,
):
mock__fetch.return_value = {
'pki:the-name:subbed:cn1:key': "thing1",
'pki:the-name:subbed:cn1:cert': "thing2",
'pki:the-name2:subbed:cn1:key': "thing3",
'pki:the-name2:subbed:cn1:cert': "thing4",
}
vault_pki.CertCache.remove_all_for('the-name')
mock__clear.assert_has_calls([
call('pki:the-name:subbed:cn1:key'),
call('pki:the-name:subbed:cn1:cert'),
])
@patch.object(vault_pki.hookenv, 'log')
@patch.object(vault_pki, 'is_cert_from_vault')
@patch.object(vault_pki, 'CertCache')
def test_find_cert_in_cache(self,
mock_cert_cache,
mock_is_cert_from_vault,
mock_log):
mock_cert_cache_object = MagicMock()
mock_cert_cache_object.cert = "a-cert"
mock_cert_cache_object.key = "a-key"
mock_cert_cache.return_value = mock_cert_cache_object
mock_is_cert_from_vault.return_value = True
request = MagicMock()
cert, key = vault_pki.find_cert_in_cache(request)
self.assertEqual((cert, key), ("a-cert", "a-key"))
mock_cert_cache.assert_called_once_with(request)
@patch.object(vault_pki.hookenv, 'log')
@patch.object(vault_pki, 'is_cert_from_vault')
@patch.object(vault_pki.CertCache, 'cert', new_callable=mock.PropertyMock)
@patch.object(vault_pki.CertCache, 'key', new_callable=mock.PropertyMock)
def test_find_cert_in_cache_not_found(self,
mock_key, mock_cert,
mock_is_cert_from_vault,
mock_log):
mock_cert.return_value = None
mock_key.return_value = "a-key"
mock_is_cert_from_vault.return_value = True
request = MagicMock()
cert, key = vault_pki.find_cert_in_cache(request)
self.assertEqual((cert, key), (None, None))
mock_cert.return_value = "a-cert"
mock_key.return_value = None
cert, key = vault_pki.find_cert_in_cache(request)
self.assertEqual((cert, key), (None, None))
@patch.object(vault_pki.hookenv, 'log')
@patch.object(vault_pki, 'is_cert_from_vault')
@patch.object(vault_pki.CertCache, 'cert', new_callable=mock.PropertyMock)
@patch.object(vault_pki.CertCache, 'key', new_callable=mock.PropertyMock)
def test_find_cert_in_cache_not_in_vault(self,
mock_key, mock_cert,
mock_is_cert_from_vault,
mock_log):
mock_cert.return_value = "a-cert"
mock_key.return_value = "a-key"
mock_is_cert_from_vault.return_value = False
request = MagicMock()
cert, key = vault_pki.find_cert_in_cache(request)
self.assertEqual((cert, key), ("a-cert", "a-key"))
mock_is_cert_from_vault.assert_called_once_with(
'a-cert', name=vault_pki.CHARM_PKI_MP)
@patch.object(vault_pki.CertCache, 'cert', new_callable=mock.PropertyMock)
@patch.object(vault_pki.CertCache, 'key', new_callable=mock.PropertyMock)
def test_update_cert_cache_top_level_cert(self, mock_key, mock_cert):
"""Test storing top-level cert in cache."""
cert_data = "cert data"
key_data = "key data"
cert_name = "server.cert"
key_name = "server.key"
client_name = "client_unit_0"
# setup cert request
request = MagicMock()
request.unit_name = client_name
request.common_name = client_name
request._is_top_level_server_cert = True
request._server_cert_key = cert_name
request._server_key_key = key_name
vault_pki.update_cert_cache(request, cert_data, key_data)
mock_cert.assert_called_once_with(cert_data)
mock_key.assert_called_once_with(key_data)
@patch.object(vault_pki.CertCache, 'remove_all_for')
def test_remove_unit_from_cache(self, mock_remove_all_for):
"""Test removing unit certificates from cache."""
vault_pki.remove_unit_from_cache('client_0')
mock_remove_all_for.assert_called_once_with('client_0')
@patch.object(vault_pki, 'update_cert_cache')
def test_populate_cert_cache(self, update_cert_cache):
# Define data for top level certificate and key
top_level_cert_name = "server.crt"
top_level_key_name = "server.key"
top_level_cert_data = "top level cert"
top_level_key_data = "top level key"
# Define data for non-top level certificate
processed_request_cn = "juju_unit_service.crt"
processed_request_publish_key = "juju_unit_service.processed"
processed_cert_data = "processed cert"
processed_key_data = "processed key"
# Mock request for top level certificate
top_level_request = MagicMock()
top_level_request._is_top_level_server_cert = True
top_level_request._server_cert_key = top_level_cert_name
top_level_request._server_key_key = top_level_key_name
top_level_request._unit.relation.to_publish_raw = {
top_level_cert_name: top_level_cert_data,
top_level_key_name: top_level_key_data,
}
# Mock request for non-top level certificate
processed_request = MagicMock()
processed_request._is_top_level_server_cert = False
processed_request.common_name = processed_request_cn
processed_request._publish_key = processed_request_publish_key
processed_request._unit.relation.to_publish = {
processed_request_publish_key: {processed_request_cn: {
"cert": processed_cert_data,
"key": processed_key_data
}}
}
tls_endpoint = MagicMock()
tls_endpoint.all_requests = [top_level_request, processed_request]
vault_pki.populate_cert_cache(tls_endpoint)
expected_update_calls = [
call(top_level_request, top_level_cert_data, top_level_key_data),
call(processed_request, processed_cert_data, processed_key_data),
]
update_cert_cache.assert_has_calls(expected_update_calls)
@patch.object(vault_pki.hookenv, 'leader_set')
def test_set_global_client_cert(self, mock_leader_set):
bundle = {
'key1': 'value1',
'key2': 'value2',
}
vault_pki.set_global_client_cert(bundle)
mock_leader_set.assert_called_once_with(
{'charm.vault.global-client-cert': mock.ANY})
v = mock_leader_set.call_args[0][0]['charm.vault.global-client-cert']
self.assertEqual(json.loads(v), bundle)
# Type error
class A:
pass
with self.assertRaises(TypeError):
vault_pki.set_global_client_cert(A())
# leader-set error.
def _raise(*args, **kwargs):
raise vault_pki.CalledProcessError(cmd="bang", returncode=1)
mock_leader_set.side_effect = _raise
with self.assertRaises(RuntimeError):
vault_pki.set_global_client_cert(bundle)
@patch.object(vault_pki.hookenv, 'leader_get')
def test_get_global_client_cert(self, mock_leader_get):
mock_leader_get.return_value = '{"a":"a-value"}'
self.assertEqual(vault_pki.get_global_client_cert(), {'a': 'a-value'})
mock_leader_get.return_value = None
self.assertEqual(vault_pki.get_global_client_cert(), {})

View File

@ -2,6 +2,7 @@ from unittest import mock
from unittest.mock import patch, call
import charms.reactive
import hvac
# Mock out reactive decorators prior to importing reactive.vault
dec_mock = mock.MagicMock()
@ -245,6 +246,16 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
mock.call('vault.ssl.configured')]
handlers.upgrade_charm()
self.remove_state.assert_has_calls(calls)
self.set_flag.assert_called_once_with(
'needs-cert-cache-repopulation')
@mock.patch.object(handlers, 'vault_pki')
def test_repopulate_cert_cache(self, mock_vault_pki):
handlers.repopulate_cert_cache()
mock_vault_pki.populate_cert_cache.assert_called_once_with(
self.endpoint_from_flag.return_value)
self.clear_flag.assert_called_once_with(
'needs-cert-cache-repopulation')
def test_request_db(self):
psql = mock.MagicMock()
@ -933,18 +944,22 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
@mock.patch.object(handlers, 'client_approle_authorized')
@mock.patch.object(handlers, 'vault_pki')
def test_publish_global_client_cert_already_gend(
def test_publish_global_client_cert_already_sent(
self, vault_pki, _client_approle_authorized):
_client_approle_authorized.return_value = True
tls = self.endpoint_from_flag.return_value
self.is_flag_set.side_effect = [True, False]
self.unitdata.kv().get.return_value = {'certificate': 'crt',
'private_key': 'key'}
self.is_flag_set.return_value = False
vault_pki.get_global_client_cert.return_value = {
'certificate': 'crt',
'private_key': 'key'
}
vault_pki.generate_certificate.return_value = "bundle"
handlers.publish_global_client_cert()
assert not vault_pki.generate_certificate.called
assert not self.set_flag.called
self.unitdata.kv().get.assert_called_with('charm.vault.'
'global-client-cert')
vault_pki.set_global_client_cert.assert_not_called()
tls.set_client_cert.assert_called_with('crt', 'key')
@mock.patch.object(handlers, 'client_approle_authorized')
@ -957,49 +972,57 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
'max-ttl': '3456h',
}
tls = self.endpoint_from_flag.return_value
vault_pki.get_global_client_cert.return_value = {
'certificate': 'stale_cert',
'private_key': 'stale_key'
}
self.is_flag_set.side_effect = [True, True]
tls = self.endpoint_from_flag.return_value
# the flag for re-issue return true.
self.is_flag_set.return_value = True
bundle = {'certificate': 'crt',
'private_key': 'key'}
vault_pki.generate_certificate.return_value = bundle
handlers.publish_global_client_cert()
vault_pki.generate_certificate.assert_called_with('client',
'global-client',
[],
'3456h',
'3456h')
self.unitdata.kv().set.assert_called_with('charm.vault.'
'global-client-cert',
bundle)
# cluster_relation.set_global_client_cert.assert_called_with(bundle)
vault_pki.set_global_client_cert.assert_called_with(bundle)
self.is_flag_set.assert_called_once_with(
'certificates.reissue.global.requested')
self.set_flag.assert_called_with('charm.vault.'
'global-client-cert.created')
tls.set_client_cert.assert_called_with('crt', 'key')
@mock.patch.object(handlers, 'client_approle_authorized')
@mock.patch.object(handlers, 'vault_pki')
def test_publish_global_client_certe(
def test_publish_global_client_cert(
self, vault_pki, _client_approle_authorized):
_client_approle_authorized.return_value = True
self.config.return_value = {
'default-ttl': '3456h',
'max-ttl': '3456h',
}
vault_pki.generate_certificate.return_value = {}
tls = self.endpoint_from_flag.return_value
self.is_flag_set.side_effect = [False, False]
self.is_flag_set.return_value = False
bundle = {'certificate': 'crt',
'private_key': 'key'}
vault_pki.generate_certificate.return_value = bundle
handlers.publish_global_client_cert()
vault_pki.generate_certificate.assert_called_with('client',
'global-client',
[],
'3456h',
'3456h')
self.unitdata.kv().set.assert_called_with('charm.vault.'
'global-client-cert',
bundle)
vault_pki.set_global_client_cert.assert_called_with(bundle)
self.set_flag.assert_called_with('charm.vault.'
'global-client-cert.created')
tls.set_client_cert.assert_called_with('crt', 'key')
@ -1027,6 +1050,10 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
handlers.vault.VaultInvalidRequest,
{'certificate': 'crt2', 'private_key': 'key2'},
]
expected_cache_update_calls = [
call(tls.new_requests[0], "crt1", "key1"),
call(tls.new_requests[2], "crt2", "key2"),
]
handlers.create_certs()
vault_pki.generate_certificate.assert_has_calls([
mock.call('cert_type1', 'common_name1', 'sans1',
@ -1043,6 +1070,205 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
tls.new_requests[2].set_cert.assert_has_calls([
mock.call('crt2', 'key2'),
])
vault_pki.update_cert_cache.assert_has_calls(
expected_cache_update_calls
)
@mock.patch.object(handlers, 'vault_pki')
def test_create_certs_reissue(self, vault_pki):
"""Test that certificates are not served from cache on reissue.
Even when certificates are available from cache, they should not
be reused if reissue was requested.
"""
self.config.return_value = {
'default-ttl': '3456h',
'max-ttl': '3456h',
}
cert_cache = (
("common_name1_cert", "common_name1_key"),
("common_name2_cert", "common_name2_key"),
)
new_certs = (
{"certificate": "cn1_new_cert", "private_key": "cn1_new_key"},
{"certificate": "cn2_new_cert", "private_key": "cn2_new_key"},
)
vault_pki.find_cert_in_cache.side_effect = cert_cache
vault_pki.generate_certificate.side_effect = new_certs
tls = self.endpoint_from_flag.return_value
self.is_flag_set.return_value = True
tls.all_requests = [mock.Mock(cert_type='cert_type1',
common_name='common_name1',
sans='sans1'),
mock.Mock(cert_type='cert_type2',
common_name='common_name2',
sans='sans2'),
]
expected_cache_update_calls = (
call(tls.all_requests[0],
new_certs[0]["certificate"],
new_certs[0]["private_key"]),
call(tls.all_requests[1],
new_certs[1]["certificate"],
new_certs[1]["private_key"]),
)
handlers.create_certs()
vault_pki.generate_certificate.assert_has_calls([
mock.call('cert_type1', 'common_name1', 'sans1',
'3456h', '3456h'),
mock.call('cert_type2', 'common_name2', 'sans2',
'3456h', '3456h')
])
for index, request in enumerate(tls.new_requests):
request.set_cert.assert_called_once_with(
new_certs[index]["certificate"],
new_certs[index]["private_key"],
)
vault_pki.update_cert_cache.assert_has_calls(
expected_cache_update_calls
)
@mock.patch.object(handlers, 'vault_pki')
@mock.patch.object(handlers, 'remote_unit')
def test_cert_client_leaving(self, remote_unit, vault_pki):
"""Test that certificates are removed from cache on unit departure."""
# This should be performed only on leader unit
self.is_flag_set.return_value = True
unit_name = "client/0"
cache_unit_id = "client_0"
remote_unit.return_value = unit_name
handlers.cert_client_leaving(mock.MagicMock())
vault_pki.remove_unit_from_cache.assert_called_once_with(cache_unit_id)
# non-leaders should not perform this action
vault_pki.remove_unit_from_cache.reset_mock()
self.is_flag_set.return_value = False
handlers.cert_client_leaving(mock.MagicMock())
vault_pki.remove_unit_from_cache.assert_not_called()
@mock.patch.object(handlers.vault_pki, 'get_global_client_cert')
@mock.patch.object(handlers.vault_pki, 'find_cert_in_cache')
@mock.patch.object(handlers.vault_pki, 'get_chain')
@mock.patch.object(handlers.vault_pki, 'get_ca')
def test_sync_cert_from_cache(self,
mock_get_ca,
mock_get_chain,
mock_find_cert_in_cache,
mock_get_global_client_cert):
"""Test that non-leaders copy data from cache to relations."""
global_client_bundle = {
"certificate": "Global client cert",
"private_key": "Global client key",
}
mock_get_global_client_cert.return_value = (
global_client_bundle
)
mock_get_chain.return_value = None
certs_in_cache = (
("cn1_cert", "cn1_key"),
("cn2_cert", "cn2_key"),
)
mock_find_cert_in_cache.side_effect = certs_in_cache
self.is_flag_set.return_value = False
tls = self.endpoint_from_flag.return_value
self.is_flag_set.return_value = True
tls.all_requests = [mock.Mock(cert_type='cert_type1',
common_name='common_name1',
sans='sans1'),
mock.Mock(cert_type='cert_type2',
common_name='common_name2',
sans='sans2'),
]
handlers.sync_cert_from_cache()
tls.set_client_cert.assert_called_once_with(
global_client_bundle["certificate"],
global_client_bundle["private_key"],
)
for index, request in enumerate(tls.all_requests):
request.set_cert.assert_called_once_with(
certs_in_cache[index][0],
certs_in_cache[index][1],
)
@mock.patch.object(handlers, 'vault_pki')
def test_sync_cert_from_cache_no_ca(self, vault_pki):
"""Test that non-leaders copy data from cache to relations."""
vault_pki.get_ca.return_value = None
handlers.sync_cert_from_cache()
vault_pki.get_ca.assert_called_once_with()
tls = self.endpoint_from_flag.return_value
tls.set_ca.assert_not_called()
@mock.patch.object(handlers, 'vault_pki')
def test_sync_cert_from_cache_no_chain_err(self, vault_pki):
"""Test that non-leaders copy data from cache to relations."""
vault_pki.get_chain.side_effect = hvac.exceptions.InternalServerError
handlers.sync_cert_from_cache()
vault_pki.get_ca.assert_called_once_with()
tls = self.endpoint_from_flag.return_value
tls.set_ca.assert_called_once_with(vault_pki.get_ca.return_value)
vault_pki.get_chain.assert_called_once_with()
tls.set_chain.assert_not_called()
@mock.patch.object(handlers, 'vault_pki')
@mock.patch.object(handlers, 'leader_get')
def test_sync_cert_from_cache_err(self, leader_get, vault_pki):
"""Test that it gracefully fails if get_chain doesn't succeed."""
global_client_bundle = {
"certificate": "Global client cert",
"private_key": "Global client key",
}
certs_in_cache = (
("cn1_cert", "cn1_key"),
("cn2_cert", "cn2_key"),
)
vault_pki.get_global_client_cert.return_value = global_client_bundle
vault_pki.find_cert_in_cache.side_effect = certs_in_cache
vault_pki.get_chain.side_effect = hvac.exceptions.InvalidPath
self.is_flag_set.return_value = False
tls = self.endpoint_from_flag.return_value
self.is_flag_set.return_value = True
tls.set_chain.assert_not_called()
tls.all_requests = [mock.Mock(cert_type='cert_type1',
common_name='common_name1',
sans='sans1'),
mock.Mock(cert_type='cert_type2',
common_name='common_name2',
sans='sans2'),
]
handlers.sync_cert_from_cache()
tls.set_client_cert.assert_called_once_with(
global_client_bundle["certificate"],
global_client_bundle["private_key"],
)
for index, request in enumerate(tls.all_requests):
request.set_cert.assert_called_once_with(
certs_in_cache[index][0],
certs_in_cache[index][1],
)
@mock.patch.object(handlers, 'vault_pki')
def test_tune_pki_backend(self, vault_pki):