-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add
vault_write
lookup and module (#223)
* add vault_write lookup * add vault_write module * lookup updates * update docstring * move fixtures to be more general * add units for vault_write lookup * improve tests * add units for vault_write module * remove unused fixture * update patch_ansible_module with parametrize compatibility * add wrapping doc fragment * vault_write module did not properly set changed status * update vault_write module to support wrapping, fix assert order * fix typo is orphan policy configure * typo in doc fragment name * add wrapping support to vault_write lookup * add integration for module_vault_write * add integration for vault_write lookup * update examples * update version_added on module * fix check mode support * improve documentation and examples * fix module reference * empty * add description for ref * empty * typo * empty
- Loading branch information
Showing
19 changed files
with
1,039 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
''' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# empty |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
--- | ||
dependencies: | ||
- setup_vault_test_plugins |
Oops, something went wrong.