Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vault_kv2_delete module #304

Merged
merged 21 commits into from
Oct 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ action_groups:
# let's keep this in alphabetical order
vault:
- vault_kv1_get
- vault_kv2_delete
- vault_kv2_get
- vault_login
- vault_pki_generate_certificate
Expand Down
172 changes: 172 additions & 0 deletions plugins/modules/vault_kv2_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2022, Isaac Wagner (@idwagner)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
module: vault_kv2_delete
author:
- Isaac Wagner (@idwagner)
short_description: Delete one or more versions of a secret from HashiCorp Vault's KV version 2 secret store
requirements:
- C(hvac) (L(Python library,https://hvac.readthedocs.io/en/stable/overview.html))
- For detailed requirements, see R(the collection requirements page,ansible_collections.community.hashi_vault.docsite.user_guide.requirements).
description:
- Delete one or more versions of a secret from HashiCorp Vault's KV version 2 secret store.
notes:
- This module always reports C(changed) status because it cannot guarantee idempotence.
- Use C(changed_when) to control that in cases where the operation is known to not change state.
idwagner marked this conversation as resolved.
Show resolved Hide resolved
- In check mode, the module returns C(changed) status without contacting Vault.
Consider using M(community.hashi_vault.vault_kv2_get) to verify the existence of the secret first.
seealso:
idwagner marked this conversation as resolved.
Show resolved Hide resolved
- module: community.hashi_vault.vault_kv2_get
- name: KV2 Secrets Engine
description: Documentation for the Vault KV secrets engine, version 2.
link: https://www.vaultproject.io/docs/secrets/kv/kv-v2
extends_documentation_fragment:
- community.hashi_vault.connection
- community.hashi_vault.auth
- community.hashi_vault.engine_mount
options:
engine_mount_point:
default: secret
path:
description:
- Vault KV path to be deleted.
- This is relative to the I(engine_mount_point), so the mount path should not be included.
- For kv2, do not include C(/data/) or C(/metadata/).
type: str
required: True
versions:
description:
- One or more versions of the secret to delete.
- When omitted, the latest version of the secret is deleted.
type: list
elements: int
required: False
'''

EXAMPLES = """
- name: Delete the latest version of the secret/mysecret secret.
community.hashi_vault.vault_kv2_delete:
url: https://vault:8201
path: secret/mysecret
auth_method: userpass
username: user
password: '{{ passwd }}'
register: result

- name: Delete versions 1 and 3 of the secret/mysecret secret.
community.hashi_vault.vault_kv2_delete:
url: https://vault:8201
path: secret/mysecret
versions: [1, 3]
auth_method: userpass
username: user
password: '{{ passwd }}'
"""

RETURN = """
data:
description:
- The raw result of the delete against the given path.
- This is usually empty, but may contain warnings or other information.
returned: success
type: dict
"""

import traceback

from ansible.module_utils._text import to_native
from ansible.module_utils.basic import missing_required_lib

from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_module import HashiVaultModule
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultValueError

try:
import hvac
except ImportError:
HAS_HVAC = False
HVAC_IMPORT_ERROR = traceback.format_exc()
else:
HVAC_IMPORT_ERROR = None
HAS_HVAC = True


def run_module():

argspec = HashiVaultModule.generate_argspec(
engine_mount_point=dict(type='str', default='secret'),
path=dict(type='str', required=True),
versions=dict(type='list', elements='int', required=False)
)

module = HashiVaultModule(
argument_spec=argspec,
supports_check_mode=True
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, the module does not account for check mode, and so with this set to True, check mode would delete secrets 😱

One issue currently (same reason status is always changed) is that without checking the current status of the secret, check mode has nothing to do but return changed.

I'm mulling over the idea of using metadata reads to get current versions status/secret existence to be able to solve both of those, but their use should be optional (a client may have permission to delete but not to hit those other endpoints).

I'll look at this again tomorrow and think about whether that makes sense at all, and if so whether it should be in this PR or a follow-up.

The first thing we should do is add an integration test and unit test (example) for check mode before making any additional changes, and those tests should fail right now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I think for now the empty result with no Vault connection is fine.

The unit tests are great! Please also add an integration test for check mode; it should be easy, just checking that the result is changed and that the deletion did not happen.

Even though the unit test covers this a little more thoroughly, it's always good to have it covered by integration too just to have the code actually invoked by ansible.

)

if not HAS_HVAC:
module.fail_json(
msg=missing_required_lib('hvac'),
exception=HVAC_IMPORT_ERROR
)

engine_mount_point = module.params.get('engine_mount_point')
path = module.params.get('path')
versions = module.params.get('versions')

module.connection_options.process_connection_options()
client_args = module.connection_options.get_hvac_connection_options()
client = module.helper.get_vault_client(**client_args)

try:
module.authenticator.validate()
module.authenticator.authenticate(client)
except (NotImplementedError, HashiVaultValueError) as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())

try:
# Vault has two separate methods, one for delete latest version,
# and delete specific versions.
if module.check_mode:
response = {}
elif not versions:
response = client.secrets.kv.v2.delete_latest_version_of_secret(
path=path, mount_point=engine_mount_point)
else:
response = client.secrets.kv.v2.delete_secret_versions(
path=path, versions=versions, mount_point=engine_mount_point)

except hvac.exceptions.Forbidden as e:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to check for some more exceptions, depending on what hvac might raise/handle (best to check the source there to get an idea what to handle here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like hvac does not do any error handling for this. I also peeked at the Vault source, and it doesn't do anything special in the delete itself, just passing through any errors it gets such as any errors updating the metadata. If theres anything else you can think of, I'd be happy to add it.

module.fail_json(msg="Forbidden: Permission Denied to path ['%s']." % path, exception=traceback.format_exc())

# https://github.com/hvac/hvac/issues/797
# HVAC returns a raw response object when the body is not JSON.
# That includes 204 responses, which are successful with no body.
# So we will try to detect that and a act accordingly.
# A better way may be to implement our own adapter for this
# collection, but it's a little premature to do that.
if hasattr(response, 'json') and callable(response.json):
if response.status_code == 204:
output = {}
else:
module.warn(
'Vault returned status code %i and an unparsable body.' % response.status_code)
output = response.content
else:
output = response

module.exit_json(changed=True, data=output)


def main():
run_module()


if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions tests/integration/targets/module_vault_kv2_delete/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
context/target
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
dependencies:
- setup_vault_test_plugins
- setup_vault_configure
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
- import_tasks: module_vault_kv2_delete_setup.yml
- import_tasks: module_vault_kv2_delete_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
- name: Configuration tasks
module_defaults:
vault_ci_token_create: '{{ vault_plugins_module_defaults_common }}'
block:
- name: Create a test non-root token
vault_ci_token_create:
policies: [test-policy]
register: user_token_cmd

- name: Configuration tasks
module_defaults:
vault_ci_kv2_destroy_all: '{{ vault_plugins_module_defaults_common }}'
vault_ci_kv_put: '{{ vault_plugins_module_defaults_common }}'
block:
- name: Remove existing multi-version secret
vault_ci_kv2_destroy_all:
mount_point: '{{ vault_kv2_mount_point }}'
path: '{{ vault_kv2_versioned_path }}/secret6'

- name: Set up a multi versioned secret for delete (v2)
vault_ci_kv_put:
version: 2
mount_point: '{{ vault_kv2_mount_point }}'
path: '{{ vault_kv2_versioned_path }}/secret6'
secret:
v: value{{ item }}
loop: ["1", "2", "3", "4", "5"]
Loading