From d8bfff76e4a907ac8825a95fb2a511f70ef8d868 Mon Sep 17 00:00:00 2001 From: Jeff Hillman Date: Tue, 26 Oct 2021 12:53:42 -0500 Subject: [PATCH] Add action to generate certificate against the PKI. Created action to utilize the existing generate_certificate function for on demand certificates agains the existing vault PKI. Closes-Bug: #1948837 Change-Id: Ia1a169623c81d6aede7dc52eabd2de94007fde80 --- src/actions.yaml | 22 +++++++++++ src/actions/actions.py | 29 +++++++++++++- src/actions/generate-certificate | 1 + unit_tests/test_actions.py | 68 ++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 120000 src/actions/generate-certificate create mode 100644 unit_tests/test_actions.py diff --git a/src/actions.yaml b/src/actions.yaml index 7ba6f36..fc723f0 100644 --- a/src/actions.yaml +++ b/src/actions.yaml @@ -143,3 +143,25 @@ reload: description: >- Reloads the vault unit. This allows for limited configuration options to be re-read. Vault will not become sealed. +generate-certificate: + description: Generate a certificate agains the Vault PKI + properties: + ttl: + type: string + default: 87599h + description: >- + Specifies the Time To Live for the certificate + common-name: + type: string + description: >- + CN field of the new certificate + sans: + type: string + description: >- + Space delimited list of Subject Altername Name/IP addresse(s) + max-ttl: + type: string + default: 8760h + description: >- + Specifies the maximum Time To Live for generated certificates. + diff --git a/src/actions/actions.py b/src/actions/actions.py index b54a1f9..f00b27f 100755 --- a/src/actions/actions.py +++ b/src/actions/actions.py @@ -193,6 +193,32 @@ def reload(args): host.service_reload(service_name='vault') +def generate_cert(*args): + """Generates a certificate and sets it in the action output. + + The certificate parameters are provided via the action parameters from + the user. If the current unit is not the leader or the vault calls fail, + this will result in a failed command. + """ + + if not hookenv.is_leader(): + hookenv.action_fail('Please run action on lead unit') + return + + action_config = hookenv.action_get() + sans_list = action_config.get('sans') + try: + new_crt = vault_pki.generate_certificate( + cert_type='server', + common_name=action_config.get('common-name'), + sans=list(sans_list.split()), + ttl=action_config.get('ttl'), + max_ttl=action_config.get('max-ttl')) + hookenv.action_set({'output': new_crt}) + except vault.VaultError as e: + hookenv.action_fail(str(e)) + + # Actions to function mapping, to allow for illegal python action names that # can map to a python function. ACTIONS = { @@ -207,7 +233,8 @@ ACTIONS = { "pause": pause, "resume": resume, "restart": restart, - "reload": reload + "reload": reload, + "generate-certificate": generate_cert } diff --git a/src/actions/generate-certificate b/src/actions/generate-certificate new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/src/actions/generate-certificate @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/unit_tests/test_actions.py b/unit_tests/test_actions.py new file mode 100644 index 0000000..fd1459f --- /dev/null +++ b/unit_tests/test_actions.py @@ -0,0 +1,68 @@ +from unittest.mock import patch + +import src.actions.actions as actions +import unit_tests.test_utils + + +class TestActions(unit_tests.test_utils.CharmTestCase): + + def setUp(self): + super(TestActions, self).setUp() + self.patches = [] + self.patch_all() + self.patch_object(actions, 'hookenv', name='mock_hookenv') + + def test_generate_cert_not_leader(self): + """Test when not leader, action fails""" + self.mock_hookenv.is_leader.return_value = False + + actions.generate_cert() + + # Action should fail + self.mock_hookenv.action_fail.assert_called_with( + 'Please run action on lead unit' + ) + self.mock_hookenv.action_set.assert_not_called() + + @patch.object(actions, 'vault_pki') + def test_generate_cert(self, mock_vault_pki): + self.mock_hookenv.is_leader.return_value = True + self.mock_hookenv.action_get.return_value = { + 'sans': 'foobar 1.2.3.4', + 'common-name': 'bazbuz', + 'ttl': '5m', + 'max-ttl': '5y', + } + mock_vault_pki.generate_certificate.return_value = 'shiny-cert' + + actions.generate_cert() + + # Validate the request for the cert was called + mock_vault_pki.generate_certificate.assert_called_with( + cert_type='server', common_name='bazbuz', + sans=['foobar', '1.2.3.4'], ttl='5m', max_ttl='5y', + ) + self.mock_hookenv.action_set.assert_called_with({ + 'output': 'shiny-cert', + }) + + @patch.object(actions, 'vault_pki') + def test_generate_cert_vault_failure(self, mock_vault_pki): + """Test failure interacting with vault_pki""" + self.mock_hookenv.is_leader.return_value = True + self.mock_hookenv.action_get.return_value = { + 'sans': 'foobar', + 'common-name': 'bazbuz', + 'ttl': '5m', + 'max-ttl': '5y', + } + mock_vault_pki.generate_certificate.side_effect = \ + actions.vault.VaultNotReady(1) + + actions.generate_cert() + + # Validate the request for the cert was called + self.mock_hookenv.action_set.assert_not_called + self.mock_hookenv.action_fail.assert_called_with( + 'Vault is not ready (1)' + )