diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/hashi_vault/plugins/modules | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/hashi_vault/plugins/modules')
10 files changed, 2022 insertions, 0 deletions
diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_kv1_get.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv1_get.py new file mode 100644 index 000000000..e21f4a813 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv1_get.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Brian Scholer (@briantist) +# 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 = r''' +module: vault_kv1_get +version_added: 2.5.0 +author: + - Brian Scholer (@briantist) +short_description: Get a secret from HashiCorp Vault's KV version 1 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: + - Gets a secret from HashiCorp Vault's KV version 1 secret store. +seealso: + - ref: community.hashi_vault.vault_kv1_get lookup <ansible_collections.community.hashi_vault.vault_kv1_get_lookup> + description: The official documentation for the C(community.hashi_vault.vault_kv1_get) lookup plugin. + - module: community.hashi_vault.vault_kv2_get + - name: KV1 Secrets Engine + description: Documentation for the Vault KV secrets engine, version 1. + link: https://www.vaultproject.io/docs/secrets/kv/kv-v1 +extends_documentation_fragment: + - community.hashi_vault.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.attributes.check_mode_read_only + - community.hashi_vault.connection + - community.hashi_vault.auth + - community.hashi_vault.engine_mount +options: + engine_mount_point: + default: kv + path: + description: + - Vault KV path to be read. + - This is relative to the I(engine_mount_point), so the mount path should not be included. + type: str + required: True +''' + +EXAMPLES = r''' +- name: Read a kv1 secret from Vault via the remote host with userpass auth + community.hashi_vault.vault_kv1_get: + url: https://vault:8201 + path: hello + auth_method: userpass + username: user + password: '{{ passwd }}' + register: response + # equivalent API path is kv/hello + +- name: Display the results + ansible.builtin.debug: + msg: + - "Secret: {{ response.secret }}" + - "Data: {{ response.data }} (same as secret in kv1)" + - "Metadata: {{ response.metadata }} (response info in kv1)" + - "Full response: {{ response.raw }}" + - "Value of key 'password' in the secret: {{ response.secret.password }}" + +- name: Read a secret from kv1 with a different mount via the remote host + community.hashi_vault.vault_kv1_get: + url: https://vault:8201 + engine_mount_point: custom/kv1/mount + path: hello + register: response + # equivalent API path is custom/kv1/mount/hello + +- name: Display the results + ansible.builtin.debug: + msg: + - "Secret: {{ response.secret }}" + - "Data: {{ response.data }} (same as secret in kv1)" + - "Metadata: {{ response.metadata }} (response info in kv1)" + - "Full response: {{ response.raw }}" +''' + +RETURN = r''' +raw: + description: The raw result of the read against the given path. + returned: success + type: dict + sample: + auth: null + data: + Key1: value1 + Key2: value2 + lease_duration: 2764800 + lease_id: "" + renewable: false + request_id: e99f145f-f02a-7073-1229-e3f191057a70 + warnings: null + wrap_info: null +data: + description: The C(data) field of raw result. This can also be accessed via C(raw.data). + returned: success + type: dict + sample: + Key1: value1 + Key2: value2 +secret: + description: The C(data) field of the raw result. This is identical to C(data) in the return values. + returned: success + type: dict + sample: + Key1: value1 + Key2: value2 +metadata: + description: This is a synthetic result. It is the same as C(raw) with C(data) removed. + returned: success + type: dict + sample: + auth: null + lease_duration: 2764800 + lease_id: "" + renewable: false + request_id: e99f145f-f02a-7073-1229-e3f191057a70 + warnings: null + wrap_info: null +''' + +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='kv'), + path=dict(type='str', required=True), + ) + + 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 + ) + + engine_mount_point = module.params.get('engine_mount_point') + path = module.params.get('path') + + 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: + raw = client.secrets.kv.v1.read_secret(path=path, mount_point=engine_mount_point) + except hvac.exceptions.Forbidden as e: + module.fail_json(msg="Forbidden: Permission Denied to path ['%s']." % path, exception=traceback.format_exc()) + except hvac.exceptions.InvalidPath as e: + if 'Invalid path for a versioned K/V secrets engine' in to_native(e): + msg = "Invalid path for a versioned K/V secrets engine ['%s']. If this is a KV version 2 path, use community.hashi_vault.vault_kv2_get." + else: + msg = "Invalid or missing path ['%s']." + + module.fail_json(msg=msg % (path,), exception=traceback.format_exc()) + + metadata = raw.copy() + data = metadata.pop('data') + module.exit_json(raw=raw, data=data, secret=data, metadata=metadata) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_delete.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_delete.py new file mode 100644 index 000000000..3145e4a5d --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_delete.py @@ -0,0 +1,180 @@ +#!/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 +version_added: 3.4.0 +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. +attributes: + check_mode: + support: partial + details: + - 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: + - module: community.hashi_vault.vault_kv2_get + - module: community.hashi_vault.vault_kv2_write + - 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.attributes + - community.hashi_vault.attributes.action_group + - 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 + ) + + 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: + 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() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_get.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_get.py new file mode 100644 index 000000000..04a549d59 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_get.py @@ -0,0 +1,213 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Brian Scholer (@briantist) +# 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 = r''' +module: vault_kv2_get +version_added: 2.5.0 +author: + - Brian Scholer (@briantist) +short_description: Get 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: + - Gets a secret from HashiCorp Vault's KV version 2 secret store. +seealso: + - ref: community.hashi_vault.vault_kv2_get lookup <ansible_collections.community.hashi_vault.vault_kv2_get_lookup> + description: The official documentation for the C(community.hashi_vault.vault_kv2_get) lookup plugin. + - module: community.hashi_vault.vault_kv1_get + - module: community.hashi_vault.vault_kv2_write + - 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.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.attributes.check_mode_read_only + - 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 read. + - 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 + version: + description: Specifies the version to return. If not set the latest version is returned. + type: int +''' + +EXAMPLES = r''' +- name: Read the latest version of a kv2 secret from Vault via the remote host with userpass auth + community.hashi_vault.vault_kv2_get: + url: https://vault:8201 + path: hello + auth_method: userpass + username: user + password: '{{ passwd }}' + register: response + # equivalent API path is secret/data/hello + +- name: Display the results + ansible.builtin.debug: + msg: + - "Secret: {{ response.secret }}" + - "Data: {{ response.data }} (contains secret data & metadata in kv2)" + - "Metadata: {{ response.metadata }}" + - "Full response: {{ response.raw }}" + - "Value of key 'password' in the secret: {{ response.secret.password }}" + +- name: Read version 5 of a secret from kv2 with a different mount via the remote host + community.hashi_vault.vault_kv2_get: + url: https://vault:8201 + engine_mount_point: custom/kv2/mount + path: hello + version: 5 + register: response + # equivalent API path is custom/kv2/mount/data/hello + +- name: Assert that the version returned is as expected + ansible.builtin.assert: + that: + - response.metadata.version == 5 +''' + +RETURN = r''' +raw: + description: The raw result of the read against the given path. + returned: success + type: dict + sample: + auth: null + data: + data: + Key1: value1 + Key2: value2 + metadata: + created_time: "2022-04-21T15:56:58.8525402Z" + custom_metadata: null + deletion_time: "" + destroyed: false + version: 2 + lease_duration: 0 + lease_id: "" + renewable: false + request_id: dc829675-9119-e831-ae74-35fc5d33d200 + warnings: null + wrap_info: null +data: + description: The C(data) field of raw result. This can also be accessed via C(raw.data). + returned: success + type: dict + sample: + data: + Key1: value1 + Key2: value2 + metadata: + created_time: "2022-04-21T15:56:58.8525402Z" + custom_metadata: null + deletion_time: "" + destroyed: false + version: 2 +secret: + description: The C(data) field within the C(data) field. Equivalent to C(raw.data.data). + returned: success + type: dict + sample: + Key1: value1 + Key2: value2 +metadata: + description: The C(metadata) field within the C(data) field. Equivalent to C(raw.data.metadata). + returned: success + type: dict + sample: + created_time: "2022-04-21T15:56:58.8525402Z" + custom_metadata: null + deletion_time: "" + destroyed: false + version: 2 +''' + +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), + version=dict(type='int'), + ) + + 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 + ) + + engine_mount_point = module.params.get('engine_mount_point') + path = module.params.get('path') + version = module.params.get('version') + + 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: + raw = client.secrets.kv.v2.read_secret_version(path=path, version=version, mount_point=engine_mount_point) + except hvac.exceptions.Forbidden as e: + module.fail_json(msg="Forbidden: Permission Denied to path ['%s']." % path, exception=traceback.format_exc()) + except hvac.exceptions.InvalidPath as e: + module.fail_json( + msg="Invalid or missing path ['%s'] with secret version '%s'. Check the path or secret version." % (path, version or 'latest'), + exception=traceback.format_exc() + ) + + data = raw['data'] + metadata = data['metadata'] + secret = data['data'] + module.exit_json(raw=raw, data=data, secret=secret, metadata=metadata) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_write.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_write.py new file mode 100644 index 000000000..d226987c6 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_write.py @@ -0,0 +1,278 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2023, Devon Mar (@devon-mar) +# 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_write +version_added: 4.2.0 +author: + - Devon Mar (@devon-mar) +short_description: Perform a write operation against a KVv2 secret in HashiCorp Vault +description: + - Perform a write operation against a KVv2 secret in 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). +seealso: + - module: community.hashi_vault.vault_write + - module: community.hashi_vault.vault_kv2_get + - module: community.hashi_vault.vault_kv2_delete + - 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. + - 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.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.connection + - community.hashi_vault.auth + - community.hashi_vault.engine_mount +attributes: + check_mode: + support: partial + details: + - If I(read_before_write) is C(true), full check mode functionality is supported. + - If I(read_before_write) is C(false), the status will always be C(changed) but a write will not be performed in check mode. +options: + engine_mount_point: + type: str + default: secret + path: + type: str + required: true + description: + - Vault KVv2 path to be written to. + - This is relative to the I(engine_mount_point), so the mount path should not be included. + data: + type: dict + required: true + description: + - KVv2 secret data to write. + cas: + type: int + description: + - Perform a check-and-set operation. + read_before_write: + type: bool + default: false + description: + - Read the secret first and write only when I(data) differs from the read data. + - Requires C(read) permission on the secret if C(true). + - If C(false), this module will always write to I(path) when not in check mode. +""" + +EXAMPLES = r""" +- name: Write/create a secret + community.hashi_vault.vault_kv2_write: + url: https://vault:8200 + path: hello + data: + foo: bar + +- name: Create a secret with CAS (the secret must not exist) + community.hashi_vault.vault_kv2_write: + url: https://vault:8200 + path: caspath + cas: 0 + data: + foo: bar + +- name: Update a secret with CAS + community.hashi_vault.vault_kv2_write: + url: https://vault:8200 + path: caspath + cas: 2 + data: + hello: world + +# This module does not have patch capability built in. +# Patching can be achieved with multiple tasks. + +- name: Retrieve current secret + register: current + community.hashi_vault.vault_kv2_get: + url: https://vault:8200 + path: hello + +## patch without CAS +- name: Update the secret + vars: + values_to_update: + foo: baz + hello: goodbye + community.hashi_vault.vault_kv2_write: + url: https://vault:8200 + path: hello + data: >- + {{ + current.secret + | combine(values_to_update) + }} + +## patch with CAS +- name: Update the secret + vars: + values_to_update: + foo: baz + hello: goodbye + community.hashi_vault.vault_kv2_write: + url: https://vault:8200 + path: hello + cas: '{{ current.metadata.version | int }}' + data: >- + {{ + current.secret + | combine(values_to_update) + }} +""" + +RETURN = r""" +raw: + type: dict + description: The raw Vault response. + returned: changed + sample: + auth: + data: + created_time: "2023-02-21T19:51:50.801757862Z" + custom_metadata: + deletion_time: "" + destroyed: false + version: 1 + lease_duration: 0 + lease_id: "" + renewable: false + request_id: 52eb1aa7-5a38-9a02-9246-efc5bf9581ec + warnings: null + wrap_info: null +""" + +import traceback + +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import missing_required_lib +from ..module_utils._hashi_vault_common import ( + HashiVaultValueError, +) +from ..module_utils._hashi_vault_module import ( + HashiVaultModule, +) + +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), + data=dict(type="dict", required=True, no_log=True), + cas=dict(type="int"), + read_before_write=dict(type="bool", default=False), + ) + + 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, + ) + + mount_point = module.params.get("engine_mount_point") + path = module.params.get("path") + cas = module.params.get("cas") + data = module.params.get("data") + read_before_write = module.params.get("read_before_write") + + 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()) + + if read_before_write is True: + try: + response = client.secrets.kv.v2.read_secret_version( + path=path, mount_point=mount_point + ) + if "data" not in response or "data" not in response["data"]: + module.fail_json( + msg="Vault response did not contain data: %s" % response + ) + current_data = response["data"]["data"] + except hvac.exceptions.InvalidPath: + current_data = {} + except hvac.exceptions.Forbidden: + module.fail_json( + msg="Permission denied reading %s" % path, + exception=traceback.format_exc(), + ) + except hvac.exceptions.VaultError: + module.fail_json( + msg="VaultError reading %s" % path, + exception=traceback.format_exc(), + ) + else: + current_data = {} + + changed = current_data != data + + if changed is True and module.check_mode is False: + args = { + "path": path, + "secret": data, + "mount_point": mount_point, + } + if cas is not None: + args["cas"] = cas + + try: + raw = client.secrets.kv.v2.create_or_update_secret(**args) + except hvac.exceptions.InvalidRequest: + module.fail_json( + msg="InvalidRequest writing to '%s'" % path, + exception=traceback.format_exc(), + ) + except hvac.exceptions.InvalidPath: + module.fail_json( + msg="InvalidPath writing to '%s'" % path, + exception=traceback.format_exc(), + ) + except hvac.exceptions.Forbidden: + module.fail_json( + msg="Permission denied writing to '%s'" % path, + exception=traceback.format_exc(), + ) + + module.exit_json(changed=True, raw=raw) + + module.exit_json(changed=changed) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_list.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_list.py new file mode 100644 index 000000000..a0823dc2d --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_list.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2023, Tom Kivlin (@tomkivlin) +# 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_list + version_added: 4.1.0 + author: + - Tom Kivlin (@tomkivlin) + short_description: Perform a list 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 list operation against a given path in HashiCorp Vault. + seealso: + - ref: community.hashi_vault.vault_list lookup <ansible_collections.community.hashi_vault.vault_list_lookup> + description: The official documentation for the C(community.hashi_vault.vault_list) lookup plugin. + extends_documentation_fragment: + - community.hashi_vault.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.attributes.check_mode_read_only + - community.hashi_vault.connection + - community.hashi_vault.auth + options: + path: + description: Vault path to be listed. + type: str + required: true +""" + +EXAMPLES = """ +- name: List kv2 secrets from Vault via the remote host with userpass auth + community.hashi_vault.vault_list: + url: https://vault:8201 + path: secret/metadata + # For kv2, the path needs to follow the pattern 'mount_point/metadata' or 'mount_point/metadata/path' to list all secrets in that path + auth_method: userpass + username: user + password: '{{ passwd }}' + register: secret + +- name: Display the secrets found at the path provided above + ansible.builtin.debug: + msg: "{{ secret.data.data['keys'] }}" + # Note that secret.data.data.keys won't work as 'keys' is a built-in method + +- name: List access policies from Vault via the remote host + community.hashi_vault.vault_list: + url: https://vault:8201 + path: sys/policies/acl + register: policies + +- name: Display the policy names + ansible.builtin.debug: + msg: "{{ policies.data.data['keys'] }}" + # Note that secret.data.data.keys won't work as 'keys' is a built-in method +""" + +RETURN = """ +data: + description: The raw result of the list 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: + HVAC_IMPORT_ERROR = None + HAS_HVAC = True + + +def run_module(): + argspec = HashiVaultModule.generate_argspec( + path=dict(type='str', required=True), + ) + + 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') + + 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: + data = client.list(path) + except hvac.exceptions.Forbidden as e: + module.fail_json(msg="Forbidden: Permission Denied to path '%s'." % path, exception=traceback.format_exc()) + + if data is None: + module.fail_json(msg="The path '%s' doesn't seem to exist." % path) + + module.exit_json(data=data) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_login.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_login.py new file mode 100644 index 000000000..fe0408da2 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_login.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021, Brian Scholer (@briantist) +# 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_login + version_added: 2.2.0 + author: + - Brian Scholer (@briantist) + short_description: Perform a login 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 login operation against a given path in HashiCorp Vault, returning the login response, including the token. + seealso: + - ref: community.hashi_vault.vault_login lookup <ansible_collections.community.hashi_vault.vault_login_lookup> + description: The official documentation for the C(community.hashi_vault.vault_login) lookup plugin. + - ref: community.hashi_vault.vault_login_token filter <ansible_collections.community.hashi_vault.vault_login_token_filter> + description: The official documentation for the C(community.hashi_vault.vault_login_token) filter plugin. + extends_documentation_fragment: + - community.hashi_vault.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.connection + - community.hashi_vault.auth + notes: + - "A login is a write operation (creating a token persisted to storage), so this module always reports C(changed=True), + except when used with C(token) auth, because no new token is created in that case. For the purposes of Ansible playbooks however, + it may be more useful to set C(changed_when=false) if you're doing idempotency checks against the target system." + - The C(none) auth method is not valid for this module because there is no response to return. + - "With C(token) auth, no actual login is performed. + Instead, the given token's additional information is returned in a structure that resembles what login responses look like." + - "The C(token) auth method will only return full information if I(token_validate=True). + If the token does not have the C(lookup-self) capability, this will fail. If I(token_validate=False), only the token value itself + will be returned in the structure." + attributes: + check_mode: + support: partial + details: + - In check mode, this module will not perform a login, and will instead return a basic structure with an empty token. + However this may not be useful if the token is required for follow on tasks. + - It may be better to use this module with C(check_mode=false) in order to have a valid token that can be used. + options: + token_validate: + default: true +""" + +EXAMPLES = """ +- name: Login and use the resulting token + community.hashi_vault.vault_login: + url: https://vault:8201 + auth_method: userpass + username: user + password: '{{ passwd }}' + register: login_data + +- name: Retrieve an approle role ID (token via filter) + community.hashi_vault.vault_read: + url: https://vault:8201 + auth_method: token + token: '{{ login_data | community.hashi_vault.vault_login_token }}' + path: auth/approle/role/role-name/role-id + register: approle_id + +- name: Retrieve an approle role ID (token via direct dict access) + community.hashi_vault.vault_read: + url: https://vault:8201 + auth_method: token + token: '{{ login_data.login.auth.client_token }}' + path: auth/approle/role/role-name/role-id + register: approle_id +""" + +RETURN = """ +login: + description: The result of the login against the given auth method. + returned: success + type: dict + contains: + auth: + description: The C(auth) member of the login response. + returned: success + type: dict + contains: + client_token: + description: Contains the token provided by the login operation (or the input token when I(auth_method=token)). + returned: success + type: str + data: + description: The C(data) member of the login response. + returned: success, when available + type: dict +""" + +import traceback + +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import missing_required_lib + +from ...plugins.module_utils._hashi_vault_module import HashiVaultModule +from ...plugins.module_utils._hashi_vault_common import HashiVaultValueError + +# we don't actually need to import hvac directly in this module +# because all of the hvac calls happen in module utils, but +# we would like to control the error message here for consistency. +try: + import hvac # pylint: disable=unused-import +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( + # we override this from the shared argspec in order to turn off no_log + # otherwise we would not be able to return the input token value + token=dict(type='str', no_log=False, default=None), + + # we override this from the shared argspec because the default for + # this module should be True, which differs from the rest of the + # collection since 4.0.0. + token_validate=dict(type='bool', default=True) + ) + + 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 + ) + + # a login is technically a write operation, using storage and resources + changed = True + auth_method = module.params.get('auth_method') + + if auth_method == 'none': + module.fail_json(msg="The 'none' auth method is not valid for this module.") + + if auth_method == 'token': + # with the token auth method, we don't actually perform a login operation + # nor change the state of Vault; it's read-only (to lookup the token's info) + changed = False + + 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() + if module.check_mode: + response = {'auth': {'client_token': None}} + else: + response = module.authenticator.authenticate(client) + except (NotImplementedError, HashiVaultValueError) as e: + module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + + module.exit_json(changed=changed, login=response) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_pki_generate_certificate.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_pki_generate_certificate.py new file mode 100644 index 000000000..66b9190b4 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_pki_generate_certificate.py @@ -0,0 +1,296 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Florent David (@Ripolin) +# 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_pki_generate_certificate + version_added: 2.3.0 + author: + - Florent David (@Ripolin) + short_description: Generates a new set of credentials (private key and certificate) using HashiCorp Vault PKI + requirements: + - C(hvac) (L(Python library,https://hvac.readthedocs.io/en/stable/changelog.html#may-25th-2019)) version C(0.9.1) or higher + - For detailed requirements, see R(the collection requirements page,ansible_collections.community.hashi_vault.docsite.user_guide.requirements). + description: + - Generates a new set of credentials (private key and certificate) based on a Vault PKI role. + seealso: + - name: HashiCorp Vault PKI Secrets Engine API + description: API documentation for the HashiCorp Vault PKI secrets engine. + link: https://www.vaultproject.io/api/secret/pki#generate-certificate + - name: HVAC library reference + description: HVAC library reference about the PKI engine. + link: https://hvac.readthedocs.io/en/stable/usage/secrets_engines/pki.html#generate-certificate + extends_documentation_fragment: + - community.hashi_vault.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.connection + - community.hashi_vault.auth + - community.hashi_vault.engine_mount + attributes: + check_mode: + support: partial + details: + - In check mode, this module will not contact Vault and will return an empty C(data) field and C(changed) status. + options: + alt_names: + description: + - Specifies requested Subject Alternative Names. + - These can be host names or email addresses; they will be parsed into their respective fields. + - If any requested names do not match role policy, the entire request will be denied. + type: list + elements: str + default: [] + common_name: + description: + - Specifies the requested CN for the certificate. + - If the CN is allowed by role policy, it will be issued. + type: str + required: true + exclude_cn_from_sans: + description: + - If true, the given I(common_name) will not be included in DNS or Email Subject Alternate Names (as appropriate). + - Useful if the CN is not a hostname or email address, but is instead some human-readable identifier. + type: bool + default: False + format: + description: + - Specifies the format for returned data. + - Can be C(pem), C(der), or C(pem_bundle). + - If C(der), the output is base64 encoded. + - >- + If C(pem_bundle), the C(certificate) field will contain the private key and certificate, concatenated. If the issuing CA is not a Vault-derived + self-signed root, this will be included as well. + type: str + choices: [pem, der, pem_bundle] + default: pem + ip_sans: + description: + - Specifies requested IP Subject Alternative Names. + - Only valid if the role allows IP SANs (which is the default). + type: list + elements: str + default: [] + role_name: + description: + - Specifies the name of the role to create the certificate against. + type: str + required: true + other_sans: + description: + - Specifies custom OID/UTF8-string SANs. + - These must match values specified on the role in C(allowed_other_sans). + - "The format is the same as OpenSSL: C(<oid>;<type>:<value>) where the only current valid type is C(UTF8)." + type: list + elements: str + default: [] + engine_mount_point: + description: + - Specify the mount point used by the PKI engine. + - Defaults to the default used by C(hvac). + private_key_format: + description: + - Specifies the format for marshaling the private key. + - Defaults to C(der) which will return either base64-encoded DER or PEM-encoded DER, depending on the value of I(format). + - The other option is C(pkcs8) which will return the key marshalled as PEM-encoded PKCS8. + type: str + choices: [der, pkcs8] + default: der + ttl: + description: + - Specifies requested Time To Live. + - Cannot be greater than the role's C(max_ttl) value. + - If not provided, the role's C(ttl) value will be used. + - Note that the role values default to system values if not explicitly set. + type: str + uri_sans: + description: + - Specifies the requested URI Subject Alternative Names. + type: list + elements: str + default: [] +""" + +EXAMPLES = """ +- name: Login and use the resulting token + community.hashi_vault.vault_login: + url: https://localhost:8200 + auth_method: ldap + username: "john.doe" + password: "{{ user_passwd }}" + register: login_data + +- name: Generate a certificate with an existing token + community.hashi_vault.vault_pki_generate_certificate: + role_name: test.example.org + common_name: test.example.org + ttl: 8760h + alt_names: + - test2.example.org + - test3.example.org + url: https://vault:8201 + auth_method: token + token: "{{ login_data.login.auth.client_token }}" + register: cert_data + +- name: Display generated certificate + debug: + msg: "{{ cert_data.data.data.certificate }}" +""" + +RETURN = """ +data: + description: Information about newly generated certificate + returned: success + type: complex + contains: + lease_id: + description: Vault lease attached to certificate. + returned: success + type: str + sample: pki/issue/test/7ad6cfa5-f04f-c62a-d477-f33210475d05 + renewable: + description: True if certificate is renewable. + returned: success + type: bool + sample: false + lease_duration: + description: Vault lease duration. + returned: success + type: int + sample: 21600 + data: + description: Payload + returned: success + type: complex + contains: + certificate: + description: Generated certificate. + returned: success + type: str + sample: "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----" + issuing_ca: + description: CA certificate. + returned: success + type: str + sample: "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----" + ca_chain: + description: Linked list of CA certificates. + returned: success + type: list + elements: str + sample: ["-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----"] + private_key: + description: Private key used to generate certificate. + returned: success + type: str + sample: "-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----" + private_key_type: + description: Private key algorithm. + returned: success + type: str + sample: rsa + serial_number: + description: Certificate's serial number. + returned: success + type: str + sample: 39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58 + warning: + description: Warnings returned by Vault during generation. + returned: success + type: str +""" + +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 + +HAS_HVAC = False +try: + import hvac + from hvac.api.secrets_engines.pki import DEFAULT_MOUNT_POINT +except ImportError: + HVAC_IMPORT_ERROR = traceback.format_exc() + HAS_HVAC = False +else: + HVAC_IMPORT_ERROR = None + HAS_HVAC = True + + +def run_module(): + argspec = HashiVaultModule.generate_argspec( + role_name=dict(type='str', required=True), + common_name=dict(type='str', required=True), + alt_names=dict(type='list', elements='str', required=False, default=[]), + ip_sans=dict(type='list', elements='str', required=False, default=[]), + uri_sans=dict(type='list', elements='str', required=False, default=[]), + other_sans=dict(type='list', elements='str', required=False, default=[]), + ttl=dict(type='str', required=False, default=None), + format=dict(type='str', required=False, choices=['pem', 'der', 'pem_bundle'], default='pem'), + private_key_format=dict(type='str', required=False, choices=['der', 'pkcs8'], default='der'), + exclude_cn_from_sans=dict(type='bool', required=False, default=False), + engine_mount_point=dict(type='str', required=False) + ) + + 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) + + role_name = module.params.get('role_name') + common_name = module.params.get('common_name') + engine_mount_point = module.params.get('engine_mount_point') or DEFAULT_MOUNT_POINT + + extra_params = { + 'alt_names': ','.join(module.params.get('alt_names')), + 'ip_sans': ','.join(module.params.get('ip_sans')), + 'uri_sans': ','.join(module.params.get('uri_sans')), + 'other_sans': ','.join(module.params.get('other_sans')), + 'ttl': module.params.get('ttl'), + 'format': module.params.get('format'), + 'private_key_format': module.params.get('private_key_format'), + 'exclude_cn_from_sans': module.params.get('exclude_cn_from_sans') + } + + 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: + data = {} + else: + data = client.secrets.pki.generate_certificate( + name=role_name, common_name=common_name, + extra_params=extra_params, mount_point=engine_mount_point + ) + except hvac.exceptions.VaultError as e: + module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + + # generate_certificate is a write operation which always return a new certificate + module.exit_json(changed=True, data=data) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_read.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_read.py new file mode 100644 index 000000000..6b6b209d5 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_read.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021, Brian Scholer (@briantist) +# 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_read + version_added: 1.4.0 + author: + - Brian Scholer (@briantist) + short_description: Perform a read 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 read operation against a given path in HashiCorp Vault. + seealso: + - 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. + - ref: community.hashi_vault.hashi_vault lookup <ansible_collections.community.hashi_vault.hashi_vault_lookup> + description: The official documentation for the C(community.hashi_vault.hashi_vault) lookup plugin. + extends_documentation_fragment: + - community.hashi_vault.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.attributes.check_mode_read_only + - community.hashi_vault.connection + - community.hashi_vault.auth + options: + path: + description: Vault path to be read. + type: str + required: True +""" + +EXAMPLES = """ +- name: Read a kv2 secret from Vault via the remote host with userpass auth + community.hashi_vault.vault_read: + url: https://vault:8201 + path: secret/data/hello + auth_method: userpass + username: user + password: '{{ passwd }}' + register: secret + +- name: Display the secret data + ansible.builtin.debug: + msg: "{{ secret.data.data.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: Display the role ID + ansible.builtin.debug: + msg: "{{ approle_id.data.data.role_id }}" +""" + +RETURN = """ +data: + description: The raw result of the read 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: + HVAC_IMPORT_ERROR = None + HAS_HVAC = True + + +def run_module(): + argspec = HashiVaultModule.generate_argspec( + path=dict(type='str', required=True), + ) + + 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') + + 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: + data = client.read(path) + except hvac.exceptions.Forbidden as e: + module.fail_json(msg="Forbidden: Permission Denied to path '%s'." % path, exception=traceback.format_exc()) + + if data is None: + module.fail_json(msg="The path '%s' doesn't seem to exist." % path) + + module.exit_json(data=data) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_token_create.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_token_create.py new file mode 100644 index 000000000..c2d19422f --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_token_create.py @@ -0,0 +1,223 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Brian Scholer (@briantist) +# 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_token_create + version_added: 2.3.0 + author: + - Brian Scholer (@briantist) + short_description: Create a HashiCorp Vault token + 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: + - Creates a token in HashiCorp Vault, returning the response, including the token. + seealso: + - ref: community.hashi_vault.vault_token_create lookup <ansible_collections.community.hashi_vault.vault_token_create_lookup> + description: The official documentation for the C(community.hashi_vault.vault_token_create) lookup plugin. + - module: community.hashi_vault.vault_login + - ref: community.hashi_vault.vault_login lookup <ansible_collections.community.hashi_vault.vault_login_lookup> + description: The official documentation for the C(community.hashi_vault.vault_login) lookup plugin. + - ref: community.hashi_vault.vault_login_token filter <ansible_collections.community.hashi_vault.vault_login_token_filter> + description: The official documentation for the C(community.hashi_vault.vault_login_token) filter plugin. + extends_documentation_fragment: + - community.hashi_vault.attributes + - community.hashi_vault.attributes.action_group + - community.hashi_vault.connection + - community.hashi_vault.auth + - community.hashi_vault.token_create + - community.hashi_vault.wrapping + notes: + - Token creation is a write operation (creating a token persisted to storage), so this module always reports C(changed=True). + - For the purposes of Ansible playbooks however, + it may be more useful to set I(changed_when=false) if you are doing idempotency checks against the target system. + attributes: + check_mode: + support: partial + details: + - In check mode, this module will not create a token, and will instead return a basic structure with an empty token. + However, this may not be useful if the token is required for follow on tasks. + - It may be better to use this module with I(check_mode=false) in order to have a valid token that can be used. + options: {} +""" + +EXAMPLES = """ +- name: Login via userpass and create a child token + community.hashi_vault.vault_token_create: + url: https://vault:8201 + auth_method: userpass + username: user + password: '{{ passwd }}' + register: token_data + +- name: Retrieve an approle role ID using the child token (token via filter) + community.hashi_vault.vault_read: + url: https://vault:8201 + auth_method: token + token: '{{ token_data | community.hashi_vault.vault_login_token }}' + path: auth/approle/role/role-name/role-id + register: approle_id + +- name: Retrieve an approle role ID using the child token (token via direct dict access) + community.hashi_vault.vault_read: + url: https://vault:8201 + auth_method: token + token: '{{ token_data.login.auth.client_token }}' + path: auth/approle/role/role-name/role-id + register: approle_id + +# implicitly uses token auth with a token from the environment +- name: Create an orphaned token with a short TTL + community.hashi_vault.vault_token_create: + url: https://vault:8201 + orphan: true + ttl: 60s + register: token_data + +- name: Display the full response + ansible.builtin.debug: + var: token_data.login +""" + +RETURN = """ +login: + description: The result of the token creation operation. + returned: success + type: dict + sample: + auth: + client_token: s.rlwajI2bblHAWU7uPqZhLru3 + data: null + contains: + auth: + description: The C(auth) member of the token response. + returned: success + type: dict + contains: + client_token: + description: Contains the newly created token. + returned: success + type: str + data: + description: The C(data) member of the token response. + returned: success, when available + type: dict +""" + +import traceback + +from ansible.module_utils._text import to_native + +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 + + +PASS_THRU_OPTION_NAMES = [ + 'no_parent', + 'no_default_policy', + 'policies', + 'id', + 'role_name', + 'meta', + 'renewable', + 'ttl', + 'type', + 'explicit_max_ttl', + 'display_name', + 'num_uses', + 'period', + 'entity_alias', + 'wrap_ttl', +] + + +ORPHAN_OPTION_TRANSLATION = { + 'id': 'token_id', + 'role_name': 'role', + 'type': 'token_type', +} + + +def run_module(): + argspec = HashiVaultModule.generate_argspec( + orphan=dict(type='bool', default=False), + no_parent=dict(type='bool'), + no_default_policy=dict(type='bool'), + policies=dict(type='list', elements='str'), + id=dict(type='str'), + role_name=dict(type='str'), + meta=dict(type='dict'), + renewable=dict(type='bool'), + ttl=dict(type='str'), + type=dict(type='str', choices=['batch', 'service']), + explicit_max_ttl=dict(type='str'), + display_name=dict(type='str'), + num_uses=dict(type='int'), + period=dict(type='str'), + entity_alias=dict(type='str'), + wrap_ttl=dict(type='str'), + ) + + module = HashiVaultModule( + argument_spec=argspec, + supports_check_mode=True + ) + + 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()) + + pass_thru_options = module.adapter.get_filled_options(*PASS_THRU_OPTION_NAMES) + + orphan_options = pass_thru_options.copy() + + for key in pass_thru_options.keys(): + if key in ORPHAN_OPTION_TRANSLATION: + orphan_options[ORPHAN_OPTION_TRANSLATION[key]] = orphan_options.pop(key) + + # token creation is a write operation, using storage and resources + changed = True + response = None + + if module.check_mode: + module.exit_json(changed=changed, login={'auth': {'client_token': None}}) + + if module.adapter.get_option('orphan'): + try: + try: + # this method was added in hvac 1.0.0 + # See: https://github.com/hvac/hvac/pull/869 + response = client.auth.token.create_orphan(**orphan_options) + except AttributeError: + # this method was removed in hvac 1.0.0 + # See: https://github.com/hvac/hvac/issues/758 + response = client.create_token(orphan=True, **orphan_options) + except Exception as e: + module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + else: + try: + response = client.auth.token.create(**pass_thru_options) + except Exception as e: + module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + + module.exit_json(changed=changed, login=response) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/hashi_vault/plugins/modules/vault_write.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_write.py new file mode 100644 index 000000000..35c7fcb60 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_write.py @@ -0,0 +1,191 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Brian Scholer (@briantist) +# 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_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. + attributes: + check_mode: + support: partial + details: + - In check mode, an empty response will be returned and the write will not be performed. + 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.attributes + - community.hashi_vault.attributes.action_group + - 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: Write secret to Vault using key value V2 engine + community.hashi_vault.vault_write: + path: secret/data/mysecret + data: + data: + key1: val1 + key2: val2 + +- 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: + HVAC_IMPORT_ERROR = None + 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() |