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_write lookup and module #223

Merged
merged 28 commits into from
Mar 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4a63ab3
add vault_write lookup
briantist Mar 2, 2022
4b7689c
add vault_write module
briantist Mar 3, 2022
aa860fd
lookup updates
briantist Mar 3, 2022
f84b5b1
update docstring
briantist Mar 4, 2022
03e1902
move fixtures to be more general
briantist Mar 6, 2022
11cf3a5
add units for vault_write lookup
briantist Mar 6, 2022
3bc3cca
improve tests
briantist Mar 6, 2022
ef100bb
add units for vault_write module
briantist Mar 11, 2022
4abd92c
remove unused fixture
briantist Mar 11, 2022
1c05465
update patch_ansible_module with parametrize compatibility
briantist Mar 13, 2022
e60e315
add wrapping doc fragment
briantist Mar 13, 2022
8ecd7b4
vault_write module did not properly set changed status
briantist Mar 13, 2022
21f2d62
update vault_write module to support wrapping, fix assert order
briantist Mar 13, 2022
ac63994
fix typo is orphan policy configure
briantist Mar 13, 2022
5c9203b
typo in doc fragment name
briantist Mar 13, 2022
21783aa
add wrapping support to vault_write lookup
briantist Mar 13, 2022
344904b
add integration for module_vault_write
briantist Mar 13, 2022
ea8349d
add integration for vault_write lookup
briantist Mar 13, 2022
e6b58af
update examples
briantist Mar 13, 2022
85d9937
update version_added on module
briantist Mar 13, 2022
e746f5d
fix check mode support
briantist Mar 14, 2022
19287cf
improve documentation and examples
briantist Mar 15, 2022
ef0c331
fix module reference
briantist Mar 15, 2022
9298635
empty
briantist Mar 15, 2022
e47533b
add description for ref
briantist Mar 15, 2022
81f51b6
empty
briantist Mar 15, 2022
3ebdb3f
typo
briantist Mar 15, 2022
810863e
empty
briantist Mar 15, 2022
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
24 changes: 24 additions & 0 deletions plugins/doc_fragments/wrapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-

# Copyright: (c) 2022, Brian Scholer (@briantist)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

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


class ModuleDocFragment(object):

DOCUMENTATION = r'''
options:
wrap_ttl:
description: Specifies response wrapping token creation with duration. For example C(15s), C(20m), C(25h).
type: str
'''

PLUGINS = r'''
options:
wrap_ttl:
vars:
- name: ansible_hashi_vault_wrap_ttl
'''
190 changes: 190 additions & 0 deletions plugins/lookup/vault_write.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# (c) 2022, Brian Scholer (@briantist)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

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

DOCUMENTATION = """
name: vault_write
version_added: 2.4.0
author:
- Brian Scholer (@briantist)
short_description: Perform a write operation against HashiCorp Vault
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:
- Performs a generic write operation against a given path in HashiCorp Vault, returning any output.
seealso:
- module: community.hashi_vault.vault_write
- ref: community.hashi_vault.vault_read lookup <ansible_collections.community.hashi_vault.vault_read_lookup>
description: The official documentation for the C(community.hashi_vault.vault_read) lookup plugin.
- module: community.hashi_vault.vault_read
- ref: community.hashi_vault Lookup Guide <ansible_collections.community.hashi_vault.docsite.lookup_guide>
description: Guidance on using lookups in C(community.hashi_vault).
notes:
- C(vault_write) is a generic plugin to do operations that do not yet have a dedicated plugin. Where a specific plugin exists, that should be used instead.
- In the vast majority of cases, it will be better to do writes as a task, with the M(community.hashi_vault.vault_write) module.
- The lookup can be used in cases where you need a value directly in templating, but there is risk of executing the write many times unintentionally.
- The lookup is best used for endpoints that directly manipulate the input data and return a value, while not changing state in Vault.
- See the R(Lookup Guide,ansible_collections.community.hashi_vault.docsite.lookup_guide) for more information.
extends_documentation_fragment:
- community.hashi_vault.connection
- community.hashi_vault.connection.plugins
- community.hashi_vault.auth
- community.hashi_vault.auth.plugins
- community.hashi_vault.wrapping
- community.hashi_vault.wrapping.plugins
options:
_terms:
description: Vault path(s) to be written to.
type: str
required: true
data:
description: A dictionary to be serialized to JSON and then sent as the request body.
type: dict
required: false
default: {}
"""

EXAMPLES = """
# These examples show some uses that might work well as a lookup.
# For most uses, the vault_write module should be used.

- name: Retrieve and display random data
vars:
data:
format: hex
num_bytes: 64
ansible.builtin.debug:
msg: "{{ lookup('community.hashi_vault.vault_write', 'sys/tools/random/' ~ num_bytes, data=data) }}"

- name: Hash some data and display the hash
vars:
input: |
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Pellentesque posuere dui a ipsum dapibus, et placerat nibh bibendum.
data:
input: '{{ input | b64encode }}'
hash_algo: sha2-256
ansible.builtin.debug:
msg: "The hash is {{ lookup('community.hashi_vault.vault_write', 'sys/tools/hash/' ~ hash_algo, data=data) }}"


# In this next example, the Ansible controller's token does not have permission to read the secrets we need.
# It does have permission to generate new secret IDs for an approle which has permission to read the secrets,
# however the approle is configured to:
# 1) allow a maximum of 1 use per secret ID
# 2) restrict the IPs allowed to use login using the approle to those of the remote hosts
#
# Normally, the fact that a new secret ID would be generated on every loop iteration would not be desirable,
# but here it's quite convenient.

- name: Retrieve secrets from the remote host with one-time-use approle creds
vars:
role_id: "{{ lookup('community.hashi_vault.vault_read', 'auth/approle/role/role-name/role-id') }}"
secret_id: "{{ lookup('community.hashi_vault.vault_write', 'auth/approle/role/role-name/secret-id') }}"
community.hashi_vault.vault_read:
auth_method: approle
role_id: '{{ role_id }}'
secret_id: '{{ secret_id }}'
path: '{{ item }}'
register: secret_data
loop:
- secret/data/secret1
- secret/data/app/deploy-key
- secret/data/access-codes/self-destruct


# This time we have a secret values on the controller, and we need to run a command the remote host,
# that is expecting to a use single-use token as input, so we need to use wrapping to send the data.

- name: Run a command that needs wrapped secrets
vars:
secrets:
secret1: '{{ my_secret_1 }}'
secret2: '{{ second_secret }}'
wrapped: "{{ lookup('community.hashi_vault.vault_write', 'sys/wrapping/wrap', data=secrets) }}"
ansible.builtin.command: 'vault unwrap {{ wrapped }}'
"""

RETURN = """
_raw:
description: The raw result of the write against the given path.
type: list
elements: dict
"""

from ansible.errors import AnsibleError
from ansible.utils.display import Display

from ansible.module_utils.six import raise_from

from ansible_collections.community.hashi_vault.plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase
from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultValueError

display = Display()

try:
import hvac
except ImportError as imp_exc:
HVAC_IMPORT_ERROR = imp_exc
else:
HVAC_IMPORT_ERROR = None


class LookupModule(HashiVaultLookupBase):
def run(self, terms, variables=None, **kwargs):
if HVAC_IMPORT_ERROR:
raise_from(
AnsibleError("This plugin requires the 'hvac' Python library"),
HVAC_IMPORT_ERROR
)

ret = []

self.set_options(direct=kwargs, var_options=variables)
# TODO: remove process_deprecations() if backported fix is available (see method definition)
self.process_deprecations()

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

data = self._options_adapter.get_option('data')
wrap_ttl = self._options_adapter.get_option_default('wrap_ttl')

try:
self.authenticator.validate()
self.authenticator.authenticate(client)
except (NotImplementedError, HashiVaultValueError) as e:
raise_from(AnsibleError(e), e)

for term in terms:
try:
response = client.write(path=term, wrap_ttl=wrap_ttl, **data)
except hvac.exceptions.Forbidden as e:
raise_from(AnsibleError("Forbidden: Permission Denied to path '%s'." % term), e)
except hvac.exceptions.InvalidPath as e:
raise_from(AnsibleError("The path '%s' doesn't seem to exist." % term), e)
except hvac.exceptions.InternalServerError as e:
raise_from(AnsibleError("Internal Server Error: %s" % str(e)), e)

# 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:
display.warning('Vault returned status code %i and an unparsable body.' % response.status_code)
output = response.content
else:
output = response

ret.append(output)

return ret
174 changes: 174 additions & 0 deletions plugins/modules/vault_write.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2022, Brian Scholer (@briantist)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

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

DOCUMENTATION = """
module: vault_write
version_added: 2.4.0
author:
- Brian Scholer (@briantist)
short_description: Perform a write operation against HashiCorp Vault
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:
- Performs a generic write operation against a given path in HashiCorp Vault, returning any output.
notes:
- C(vault_write) is a generic module to do operations that do not yet have a dedicated module. Where a specific module exists, that should be used instead.
- The I(data) option is not treated as secret and may be logged. Use the C(no_log) keyword if I(data) contains sensitive values.
- 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.
seealso:
- ref: community.hashi_vault.vault_write lookup <ansible_collections.community.hashi_vault.vault_write_lookup>
description: The official documentation for the C(community.hashi_vault.vault_write) lookup plugin.
- module: community.hashi_vault.vault_read
- ref: community.hashi_vault.vault_read lookup <ansible_collections.community.hashi_vault.vault_read_lookup>
description: The official documentation for the C(community.hashi_vault.vault_read) lookup plugin.
extends_documentation_fragment:
- community.hashi_vault.connection
- community.hashi_vault.auth
- community.hashi_vault.wrapping
options:
path:
description: Vault path to be written to.
type: str
required: True
data:
description: A dictionary to be serialized to JSON and then sent as the request body.
type: dict
required: false
default: {}
"""

EXAMPLES = """
- name: Write a value to the cubbyhole via the remote host with userpass auth
community.hashi_vault.vault_write:
url: https://vault:8201
path: cubbyhole/mysecret
data:
key1: val1
key2: val2
auth_method: userpass
username: user
password: '{{ passwd }}'
register: result

- name: Display the result of the write (this can be empty)
ansible.builtin.debug:
msg: "{{ result.data }}"

- name: Retrieve an approle role ID from Vault via the remote host
community.hashi_vault.vault_read:
url: https://vault:8201
path: auth/approle/role/role-name/role-id
register: approle_id

- name: Generate a secret-id for the given approle
community.hashi_vault.vault_write:
url: https://vault:8201
path: auth/approle/role/role-name/secret-id
register: secret_id

- name: Display the role ID and secret ID
ansible.builtin.debug:
msg:
- "role-id: {{ approle_id.data.data.role_id }}"
- "secret-id: {{ secret_id.data.data.secret_id }}"
"""

RETURN = """
data:
description: The raw result of the write against the given path.
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:
HAS_HVAC = True


def run_module():
argspec = HashiVaultModule.generate_argspec(
path=dict(type='str', required=True),
data=dict(type='dict', required=False, default={}),
wrap_ttl=dict(type='str'),
)

module = HashiVaultModule(
argument_spec=argspec,
supports_check_mode=True
)

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

path = module.params.get('path')
data = module.params.get('data')
wrap_ttl = module.params.get('wrap_ttl')

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:
if module.check_mode:
response = {}
else:
response = client.write(path=path, wrap_ttl=wrap_ttl, **data)
except hvac.exceptions.Forbidden:
module.fail_json(msg="Forbidden: Permission Denied to path '%s'." % path, exception=traceback.format_exc())
except hvac.exceptions.InvalidPath:
module.fail_json(msg="The path '%s' doesn't seem to exist." % path, exception=traceback.format_exc())
except hvac.exceptions.InternalServerError as e:
module.fail_json(msg="Internal Server Error: %s" % to_native(e), 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/lookup_vault_write/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty
3 changes: 3 additions & 0 deletions tests/integration/targets/lookup_vault_write/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
dependencies:
- setup_vault_test_plugins
Loading