From 66cec45960ce1d9c794e9399de15c138acb18aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 18:03:42 +0200 Subject: Adding upstream version 7.3.0+dfsg. Signed-off-by: Daniel Baumann --- .../plugins/doc_fragments/attributes.py | 38 +++ .../hashi_vault/plugins/doc_fragments/auth.py | 308 ++++++++++++++++++ .../plugins/doc_fragments/connection.py | 161 ++++++++++ .../plugins/doc_fragments/engine_mount.py | 25 ++ .../plugins/doc_fragments/token_create.py | 93 ++++++ .../hashi_vault/plugins/doc_fragments/wrapping.py | 25 ++ .../plugins/filter/vault_login_token.py | 38 +++ .../plugins/filter/vault_login_token.yml | 98 ++++++ .../hashi_vault/plugins/lookup/hashi_vault.py | 349 +++++++++++++++++++++ .../plugins/lookup/vault_ansible_settings.py | 337 ++++++++++++++++++++ .../hashi_vault/plugins/lookup/vault_kv1_get.py | 220 +++++++++++++ .../hashi_vault/plugins/lookup/vault_kv2_get.py | 233 ++++++++++++++ .../hashi_vault/plugins/lookup/vault_list.py | 183 +++++++++++ .../hashi_vault/plugins/lookup/vault_login.py | 138 ++++++++ .../hashi_vault/plugins/lookup/vault_read.py | 137 ++++++++ .../plugins/lookup/vault_token_create.py | 195 ++++++++++++ .../hashi_vault/plugins/lookup/vault_write.py | 191 +++++++++++ .../plugins/module_utils/_auth_method_approle.py | 40 +++ .../plugins/module_utils/_auth_method_aws_iam.py | 97 ++++++ .../plugins/module_utils/_auth_method_azure.py | 105 +++++++ .../plugins/module_utils/_auth_method_cert.py | 42 +++ .../plugins/module_utils/_auth_method_jwt.py | 51 +++ .../plugins/module_utils/_auth_method_ldap.py | 40 +++ .../plugins/module_utils/_auth_method_none.py | 33 ++ .../plugins/module_utils/_auth_method_token.py | 105 +++++++ .../plugins/module_utils/_auth_method_userpass.py | 47 +++ .../plugins/module_utils/_authenticator.py | 102 ++++++ .../plugins/module_utils/_connection_options.py | 260 +++++++++++++++ .../plugins/module_utils/_hashi_vault_common.py | 302 ++++++++++++++++++ .../plugins/module_utils/_hashi_vault_module.py | 56 ++++ .../hashi_vault/plugins/modules/vault_kv1_get.py | 197 ++++++++++++ .../plugins/modules/vault_kv2_delete.py | 179 +++++++++++ .../hashi_vault/plugins/modules/vault_kv2_get.py | 212 +++++++++++++ .../hashi_vault/plugins/modules/vault_list.py | 134 ++++++++ .../hashi_vault/plugins/modules/vault_login.py | 177 +++++++++++ .../modules/vault_pki_generate_certificate.py | 296 +++++++++++++++++ .../hashi_vault/plugins/modules/vault_read.py | 133 ++++++++ .../plugins/modules/vault_token_create.py | 223 +++++++++++++ .../hashi_vault/plugins/modules/vault_write.py | 191 +++++++++++ .../plugin_utils/_hashi_vault_lookup_base.py | 47 +++ .../plugins/plugin_utils/_hashi_vault_plugin.py | 87 +++++ 41 files changed, 5925 insertions(+) create mode 100644 ansible_collections/community/hashi_vault/plugins/doc_fragments/attributes.py create mode 100644 ansible_collections/community/hashi_vault/plugins/doc_fragments/auth.py create mode 100644 ansible_collections/community/hashi_vault/plugins/doc_fragments/connection.py create mode 100644 ansible_collections/community/hashi_vault/plugins/doc_fragments/engine_mount.py create mode 100644 ansible_collections/community/hashi_vault/plugins/doc_fragments/token_create.py create mode 100644 ansible_collections/community/hashi_vault/plugins/doc_fragments/wrapping.py create mode 100644 ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.py create mode 100644 ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.yml create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/hashi_vault.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_ansible_settings.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_kv1_get.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_kv2_get.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_list.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_login.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_read.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_token_create.py create mode 100644 ansible_collections/community/hashi_vault/plugins/lookup/vault_write.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_approle.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_aws_iam.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_azure.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_cert.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_jwt.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_ldap.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_none.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_token.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_userpass.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_authenticator.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_connection_options.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_common.py create mode 100644 ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_module.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_kv1_get.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_delete.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_get.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_list.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_login.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_pki_generate_certificate.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_read.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_token_create.py create mode 100644 ansible_collections/community/hashi_vault/plugins/modules/vault_write.py create mode 100644 ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_lookup_base.py create mode 100644 ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_plugin.py (limited to 'ansible_collections/community/hashi_vault/plugins') diff --git a/ansible_collections/community/hashi_vault/plugins/doc_fragments/attributes.py b/ansible_collections/community/hashi_vault/plugins/doc_fragments/attributes.py new file mode 100644 index 00000000..7536fa79 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/doc_fragments/attributes.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# Copyright: (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 + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' +options: {} +attributes: + check_mode: + description: Can run in C(check_mode) and return changed status prediction without modifying target. +''' + + ACTION_GROUP = r''' +options: {} +attributes: + action_group: + description: Use C(group/community.hashi_vault.vault) in C(module_defaults) to set defaults for this module. + support: full + membership: + - community.hashi_vault.vault +''' + + # Should be used together with the standard fragment + CHECK_MODE_READ_ONLY = r''' +options: {} +attributes: + check_mode: + support: full + details: + - This module is "read only" and operates the same regardless of check mode. +''' diff --git a/ansible_collections/community/hashi_vault/plugins/doc_fragments/auth.py b/ansible_collections/community/hashi_vault/plugins/doc_fragments/auth.py new file mode 100644 index 00000000..8c6bd876 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/doc_fragments/auth.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- + +# Copyright: (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 + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' + options: + auth_method: + description: + - Authentication method to be used. + - C(none) auth method was added in collection version C(1.2.0). + - C(cert) auth method was added in collection version C(1.4.0). + - C(aws_iam_login) was renamed C(aws_iam) in collection version C(2.1.0) and was removed in C(3.0.0). + - C(azure) auth method was added in collection version C(3.2.0). + choices: + - token + - userpass + - ldap + - approle + - aws_iam + - azure + - jwt + - cert + - none + default: token + type: str + mount_point: + description: + - Vault mount point. + - If not specified, the default mount point for a given auth method is used. + - Does not apply to token authentication. + type: str + token: + description: + - Vault token. Token may be specified explicitly, through the listed [env] vars, and also through the C(VAULT_TOKEN) env var. + - If no token is supplied, explicitly or through env, then the plugin will check for a token file, as determined by I(token_path) and I(token_file). + - The order of token loading (first found wins) is C(token param -> ansible var -> ANSIBLE_HASHI_VAULT_TOKEN -> VAULT_TOKEN -> token file). + type: str + token_path: + description: If no token is specified, will try to read the I(token_file) from this path. + type: str + token_file: + description: If no token is specified, will try to read the token from this file in I(token_path). + default: '.vault-token' + type: str + token_validate: + description: + - For token auth, will perform a C(lookup-self) operation to determine the token's validity before using it. + - Disable if your token does not have the C(lookup-self) capability. + type: bool + default: false + version_added: 0.2.0 + username: + description: Authentication user name. + type: str + password: + description: Authentication password. + type: str + role_id: + description: + - Vault Role ID or name. Used in C(approle), C(aws_iam), C(azure) and C(cert) auth methods. + - For C(cert) auth, if no I(role_id) is supplied, the default behavior is to try all certificate roles and return any one that matches. + - For C(azure) auth, I(role_id) is required. + type: str + secret_id: + description: Secret ID to be used for Vault AppRole authentication. + type: str + jwt: + description: The JSON Web Token (JWT) to use for JWT authentication to Vault. + type: str + aws_profile: + description: The AWS profile + type: str + aliases: [ boto_profile ] + aws_access_key: + description: The AWS access key to use. + type: str + aliases: [ aws_access_key_id ] + aws_secret_key: + description: The AWS secret key that corresponds to the access key. + type: str + aliases: [ aws_secret_access_key ] + aws_security_token: + description: The AWS security token if using temporary access and secret keys. + type: str + region: + description: The AWS region for which to create the connection. + type: str + aws_iam_server_id: + description: If specified, sets the value to use for the C(X-Vault-AWS-IAM-Server-ID) header as part of C(GetCallerIdentity) request. + required: False + type: str + version_added: '0.2.0' + azure_tenant_id: + description: + - The Azure Active Directory Tenant ID (also known as the Directory ID) of the service principal. Should be a UUID. + - >- + Required when using a service principal to authenticate to Vault, + e.g. required when both I(azure_client_id) and I(azure_client_secret) are specified. + - Optional when using managed identity to authenticate to Vault. + required: False + type: str + version_added: '3.2.0' + azure_client_id: + description: + - The client ID (also known as application ID) of the Azure AD service principal or managed identity. Should be a UUID. + - If not specified, will use the system assigned managed identity. + required: False + type: str + version_added: '3.2.0' + azure_client_secret: + description: The client secret of the Azure AD service principal. + required: False + type: str + version_added: '3.2.0' + azure_resource: + description: The resource URL for the application registered in Azure Active Directory. Usually should not be changed from the default. + required: False + type: str + default: https://management.azure.com/ + version_added: '3.2.0' + cert_auth_public_key: + description: For C(cert) auth, path to the certificate file to authenticate with, in PEM format. + type: path + version_added: 1.4.0 + cert_auth_private_key: + description: For C(cert) auth, path to the private key file to authenticate with, in PEM format. + type: path + version_added: 1.4.0 + ''' + + PLUGINS = r''' + options: + auth_method: + env: + - name: ANSIBLE_HASHI_VAULT_AUTH_METHOD + version_added: 0.2.0 + ini: + - section: hashi_vault_collection + key: auth_method + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_auth_method + version_added: 1.2.0 + mount_point: + env: + - name: ANSIBLE_HASHI_VAULT_MOUNT_POINT + version_added: 1.5.0 + ini: + - section: hashi_vault_collection + key: mount_point + version_added: 1.5.0 + vars: + - name: ansible_hashi_vault_mount_point + version_added: 1.5.0 + token: + env: + - name: ANSIBLE_HASHI_VAULT_TOKEN + version_added: 0.2.0 + vars: + - name: ansible_hashi_vault_token + version_added: 1.2.0 + token_path: + env: + - name: ANSIBLE_HASHI_VAULT_TOKEN_PATH + version_added: 0.2.0 + ini: + - section: hashi_vault_collection + key: token_path + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_token_path + version_added: 1.2.0 + token_file: + env: + - name: ANSIBLE_HASHI_VAULT_TOKEN_FILE + version_added: 0.2.0 + ini: + - section: hashi_vault_collection + key: token_file + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_token_file + version_added: 1.2.0 + token_validate: + env: + - name: ANSIBLE_HASHI_VAULT_TOKEN_VALIDATE + ini: + - section: hashi_vault_collection + key: token_validate + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_token_validate + version_added: 1.2.0 + username: + env: + - name: ANSIBLE_HASHI_VAULT_USERNAME + version_added: '1.2.0' + vars: + - name: ansible_hashi_vault_username + version_added: '1.2.0' + password: + env: + - name: ANSIBLE_HASHI_VAULT_PASSWORD + version_added: '1.2.0' + vars: + - name: ansible_hashi_vault_password + version_added: '1.2.0' + role_id: + env: + - name: ANSIBLE_HASHI_VAULT_ROLE_ID + version_added: 0.2.0 + ini: + - section: hashi_vault_collection + key: role_id + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_role_id + version_added: 1.2.0 + secret_id: + env: + - name: ANSIBLE_HASHI_VAULT_SECRET_ID + version_added: 0.2.0 + vars: + - name: ansible_hashi_vault_secret_id + version_added: 1.2.0 + jwt: + env: + - name: ANSIBLE_HASHI_VAULT_JWT + aws_profile: + env: + - name: AWS_DEFAULT_PROFILE + - name: AWS_PROFILE + aws_access_key: + env: + - name: EC2_ACCESS_KEY + - name: AWS_ACCESS_KEY + - name: AWS_ACCESS_KEY_ID + aws_secret_key: + env: + - name: EC2_SECRET_KEY + - name: AWS_SECRET_KEY + - name: AWS_SECRET_ACCESS_KEY + aws_security_token: + env: + - name: EC2_SECURITY_TOKEN + - name: AWS_SESSION_TOKEN + - name: AWS_SECURITY_TOKEN + region: + env: + - name: EC2_REGION + - name: AWS_REGION + aws_iam_server_id: + env: + - name: ANSIBLE_HASHI_VAULT_AWS_IAM_SERVER_ID + ini: + - section: hashi_vault_collection + key: aws_iam_server_id + version_added: 1.4.0 + azure_tenant_id: + env: + - name: ANSIBLE_HASHI_VAULT_AZURE_TENANT_ID + ini: + - section: hashi_vault_collection + key: azure_tenant_id + vars: + - name: ansible_hashi_vault_azure_tenant_id + azure_client_id: + env: + - name: ANSIBLE_HASHI_VAULT_AZURE_CLIENT_ID + ini: + - section: hashi_vault_collection + key: azure_client_id + vars: + - name: ansible_hashi_vault_azure_client_id + azure_client_secret: + env: + - name: ANSIBLE_HASHI_VAULT_AZURE_CLIENT_SECRET + vars: + - name: ansible_hashi_vault_azure_client_secret + azure_resource: + env: + - name: ANSIBLE_HASHI_VAULT_AZURE_RESOURCE + ini: + - section: hashi_vault_collection + key: azure_resource + vars: + - name: ansible_hashi_vault_azure_resource + cert_auth_public_key: + env: + - name: ANSIBLE_HASHI_VAULT_CERT_AUTH_PUBLIC_KEY + ini: + - section: hashi_vault_collection + key: cert_auth_public_key + cert_auth_private_key: + env: + - name: ANSIBLE_HASHI_VAULT_CERT_AUTH_PRIVATE_KEY + ini: + - section: hashi_vault_collection + key: cert_auth_private_key + ''' diff --git a/ansible_collections/community/hashi_vault/plugins/doc_fragments/connection.py b/ansible_collections/community/hashi_vault/plugins/doc_fragments/connection.py new file mode 100644 index 00000000..e7ab8d07 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/doc_fragments/connection.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- + +# Copyright: (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 + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' + options: + url: + description: + - URL to the Vault service. + - If not specified by any other means, the value of the C(VAULT_ADDR) environment variable will be used. + - If C(VAULT_ADDR) is also not defined then an error will be raised. + type: str + proxies: + description: + - URL(s) to the proxies used to access the Vault service. + - It can be a string or a dict. + - If it's a dict, provide the scheme (eg. C(http) or C(https)) as the key, and the URL as the value. + - If it's a string, provide a single URL that will be used as the proxy for both C(http) and C(https) schemes. + - A string that can be interpreted as a dictionary will be converted to one (see examples). + - You can specify a different proxy for HTTP and HTTPS resources. + - If not specified, L(environment variables from the Requests library,https://requests.readthedocs.io/en/master/user/advanced/#proxies) are used. + type: raw + version_added: 1.1.0 + ca_cert: + description: + - Path to certificate to use for authentication. + - If not specified by any other means, the C(VAULT_CACERT) environment variable will be used. + aliases: [ cacert ] + type: str + validate_certs: + description: + - Controls verification and validation of SSL certificates, mostly you only want to turn off with self signed ones. + - Will be populated with the inverse of C(VAULT_SKIP_VERIFY) if that is set and I(validate_certs) is not explicitly provided. + - Will default to C(true) if neither I(validate_certs) or C(VAULT_SKIP_VERIFY) are set. + type: bool + namespace: + description: + - Vault namespace where secrets reside. This option requires HVAC 0.7.0+ and Vault 0.11+. + - Optionally, this may be achieved by prefixing the authentication mount point and/or secret path with the namespace + (e.g C(mynamespace/secret/mysecret)). + - If environment variable C(VAULT_NAMESPACE) is set, its value will be used last among all ways to specify I(namespace). + type: str + timeout: + description: + - Sets the connection timeout in seconds. + - If not set, then the C(hvac) library's default is used. + type: int + version_added: 1.3.0 + retries: + description: + - "Allows for retrying on errors, based on + the L(Retry class in the urllib3 library,https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html#urllib3.util.Retry)." + - This collection defines recommended defaults for retrying connections to Vault. + - This option can be specified as a positive number (integer) or dictionary. + - If this option is not specified or the number is C(0), then retries are disabled. + - A number sets the total number of retries, and uses collection defaults for the other settings. + - A dictionary value is used directly to initialize the C(Retry) class, so it can be used to fully customize retries. + - For detailed information on retries, see the collection User Guide. + type: raw + version_added: 1.3.0 + retry_action: + description: + - Controls whether and how to show messages on I(retries). + - This has no effect if a request is not retried. + type: str + choices: + - ignore + - warn + default: warn + version_added: 1.3.0 + ''' + + PLUGINS = r''' + options: + url: + env: + - name: ANSIBLE_HASHI_VAULT_ADDR + version_added: 0.2.0 + ini: + - section: hashi_vault_collection + key: url + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_url + version_added: 1.2.0 + - name: ansible_hashi_vault_addr + version_added: 1.2.0 + proxies: + env: + - name: ANSIBLE_HASHI_VAULT_PROXIES + ini: + - section: hashi_vault_collection + key: proxies + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_proxies + version_added: 1.2.0 + type: raw + version_added: 1.1.0 + ca_cert: + env: + - name: ANSIBLE_HASHI_VAULT_CA_CERT + version_added: 1.2.0 + ini: + - section: hashi_vault_collection + key: ca_cert + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_ca_cert + version_added: 1.2.0 + validate_certs: + vars: + - name: ansible_hashi_vault_validate_certs + version_added: 1.2.0 + namespace: + env: + - name: ANSIBLE_HASHI_VAULT_NAMESPACE + version_added: 0.2.0 + ini: + - section: hashi_vault_collection + key: namespace + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_namespace + version_added: 1.2.0 + timeout: + env: + - name: ANSIBLE_HASHI_VAULT_TIMEOUT + ini: + - section: hashi_vault_collection + key: timeout + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_timeout + retries: + env: + - name: ANSIBLE_HASHI_VAULT_RETRIES + ini: + - section: hashi_vault_collection + key: retries + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_retries + retry_action: + env: + - name: ANSIBLE_HASHI_VAULT_RETRY_ACTION + ini: + - section: hashi_vault_collection + key: retry_action + version_added: 1.4.0 + vars: + - name: ansible_hashi_vault_retry_action + ''' diff --git a/ansible_collections/community/hashi_vault/plugins/doc_fragments/engine_mount.py b/ansible_collections/community/hashi_vault/plugins/doc_fragments/engine_mount.py new file mode 100644 index 00000000..8187f28e --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/doc_fragments/engine_mount.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Copyright: (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 + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' +options: + engine_mount_point: + description: The path where the secret backend is mounted. + type: str +''' + + PLUGINS = r''' +options: + engine_mount_point: + vars: + - name: ansible_hashi_vault_engine_mount_point +''' diff --git a/ansible_collections/community/hashi_vault/plugins/doc_fragments/token_create.py b/ansible_collections/community/hashi_vault/plugins/doc_fragments/token_create.py new file mode 100644 index 00000000..30031f07 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/doc_fragments/token_create.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# Copyright: (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 + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' +options: + orphan: + description: + - When C(true), uses the C(/create-orphan) API endpoint, which requires C(sudo) (but not C(root)) to create an orphan. + - With C(hvac>=1.0.0), requires collection version C(>=3.3.0). + type: bool + default: false + no_parent: + description: + - This option only has effect if used by a C(root) or C(sudo) caller and only when I(orphan=false). + - When C(true), the token created will not have a parent. + type: bool + no_default_policy: + description: + - If C(true) the default policy will not be contained in this token's policy set. + - If the token will be used with this collection, set I(token_validate=false). + type: bool + policies: + description: + - A list of policies for the token. This must be a subset of the policies belonging to the token making the request, unless root. + - If not specified, defaults to all the policies of the calling token. + type: list + elements: str + id: + description: + - The ID of the client token. Can only be specified by a root token. + - The ID provided may not contain a C(.) character. + - Otherwise, the token ID is a randomly generated value. + type: str + role_name: + description: + - The name of the token role. If used, the token will be created against the specified role name which may override options set during this call. + type: str + meta: + description: A dict of string to string valued metadata. This is passed through to the audit devices. + type: dict + renewable: + description: + - Set to C(false) to disable the ability of the token to be renewed past its initial TTL. + - Setting the value to C(true) will allow the token to be renewable up to the system/mount maximum TTL. + type: bool + ttl: + description: + - The TTL period of the token, provided as C(1h) for example, where hour is the largest suffix. + - If not provided, the token is valid for the default lease TTL, or indefinitely if the root policy is used. + type: str + type: + description: The token type. The default is determined by the role configuration specified by I(role_name). + type: str + choices: + - batch + - service + explicit_max_ttl: + description: + - If set, the token will have an explicit max TTL set upon it. + - This maximum token TTL cannot be changed later, + and unlike with normal tokens, updates to the system/mount max TTL value will have no effect at renewal time. + - The token will never be able to be renewed or used past the value set at issue time. + type: str + display_name: + description: The display name of the token. + type: str + num_uses: + description: + - The maximum uses for the given token. This can be used to create a one-time-token or limited use token. + - The value of C(0) has no limit to the number of uses. + type: int + period: + description: + - If specified, the token will be periodic. + - It will have no maximum TTL (unless an I(explicit_max_ttl) is also set) but every renewal will use the given period. + - Requires a root token or one with the C(sudo) capability. + type: str + entity_alias: + description: + - Name of the entity alias to associate with during token creation. + - Only works in combination with I(role_name) option and used entity alias must be listed in C(allowed_entity_aliases). + - If this has been specified, the entity will not be inherited from the parent. + type: str +''' diff --git a/ansible_collections/community/hashi_vault/plugins/doc_fragments/wrapping.py b/ansible_collections/community/hashi_vault/plugins/doc_fragments/wrapping.py new file mode 100644 index 00000000..720f7bdf --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/doc_fragments/wrapping.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Copyright: (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 + + +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 +''' diff --git a/ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.py b/ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.py new file mode 100644 index 00000000..543d60c3 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.py @@ -0,0 +1,38 @@ +# -*- 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 + +from ansible.errors import AnsibleError + + +def vault_login_token(login_response, optional_field='login'): + '''Extracts the token value from a Vault login response. + Meant to be used with the vault_login module and lookup plugin. + ''' + + try: + deref = login_response[optional_field] + except TypeError: + raise AnsibleError("The 'vault_login_token' filter expects a dictionary.") + except KeyError: + deref = login_response + + try: + token = deref['auth']['client_token'] + except KeyError: + raise AnsibleError("Could not find 'auth' or 'auth.client_token' fields. Input may not be a Vault login response.") + + return token + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'vault_login_token': vault_login_token, + } diff --git a/ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.yml b/ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.yml new file mode 100644 index 00000000..e2946baf --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/filter/vault_login_token.yml @@ -0,0 +1,98 @@ +# (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 +--- +DOCUMENTATION: + name: vault_login_token + short_description: Extracts the Vault token from a login or token creation + version_added: 2.2.0 + description: + - Extracts the token value from the structure returned by a Vault token creation operation. + seealso: + - module: community.hashi_vault.vault_login + - module: community.hashi_vault.vault_token_create + - plugin: community.hashi_vault.vault_login + plugin_type: lookup + - plugin: community.hashi_vault.vault_token_create + plugin_type: lookup + - ref: Filter Guide + description: The C(community.hashi_vault) Filter Guide + notes: + - >- + This filter is the same as reading into the I(_input) dictionary directly, + but it provides semantic meaning and automatically works with the differing output of the modules and lookups. + See the Filter guide for more information. + options: + _input: + description: + - A dictionary matching the structure returned by a login or token creation. + type: dict + required: true + optional_field: + description: + - >- + If this field exists in the input dictionary, then the value of that field is used as the I(_input) value. + - >- + The default value deals with the difference between the output of lookup plugins, + and does not need to be changed in most cases. + - See the examples or the Filter guide for more information. + type: string + default: login + author: + - Brian Scholer (@briantist) + +EXAMPLES: | + - name: Set defaults + vars: + ansible_hashi_vault_url: https://vault:9801/ + ansible_hashi_vault_auth_method: userpass + ansible_hashi_vault_username: user + ansible_hashi_vault_password: "{{ lookup('env', 'MY_SECRET_PASSWORD') }}" + module_defaults: + community.hashi_vault.vault_login: + url: '{{ ansible_hashi_vault_url }}' + auth_method: '{{ ansible_hashi_vault_auth_method }}' + username: '{{ ansible_hashi_vault_username }}' + password: '{{ ansible_hashi_vault_password }}' + block: + - name: Perform a login with a lookup and display the token + vars: + login_response: "{{ lookup('community.hashi_vault.vault_login') }}" + debug: + msg: "The token is {{ login_response | community.hashi_vault.vault_login_token }}" + + - name: Perform a login with a module + community.hashi_vault.vault_login: + register: login_response + + - name: Display the token + debug: + msg: "The token is {{ login_response | community.hashi_vault.vault_login_token }}" + + - name: Use of optional_field + vars: + lookup_login_response: "{{ lookup('community.hashi_vault.vault_login') }}" + my_data: + something: somedata + vault_login: "{{ lookup_login_response }}" + + token_from_param: "{{ my_data | community.hashi_vault.vault_login_token(optional_field='vault_login') }}" + token_from_deref: "{{ my_data['vault_login'] | community.hashi_vault.vault_login_token }}" + # if the optional field doesn't exist, the dictionary itself is still checked + unused_optional: "{{ my_data['vault_login'] | community.hashi_vault.vault_login_token(optional_field='missing') }}" + block: + - name: Display the variables + ansible.builtin.debug: + var: '{{ item }}' + loop: + - my_data + - token_from_param + - token_from_deref + - unused_optional + +RETURN: + _value: + description: The token value. + returned: always + sample: s.nnrpog4i5gjizr6b8g1inwj3 + type: string diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/hashi_vault.py b/ansible_collections/community/hashi_vault/plugins/lookup/hashi_vault.py new file mode 100644 index 00000000..1ea9b2c9 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/hashi_vault.py @@ -0,0 +1,349 @@ +# (c) 2020, Brian Scholer (@briantist) +# (c) 2015, Julie Davila (@juliedavila) +# (c) 2017 Ansible Project +# 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 = """ + name: hashi_vault + author: + - Julie Davila (@juliedavila) + - Brian Scholer (@briantist) + short_description: Retrieve secrets from HashiCorp's 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: + - Retrieve secrets from HashiCorp's Vault. + - Consider R(migrating to other plugins in the collection,ansible_collections.community.hashi_vault.docsite.migration_hashi_vault_lookup). + seealso: + - ref: community.hashi_vault.hashi_vault Migration Guide + description: Migrating from the C(hashi_vault) lookup. + - ref: About the community.hashi_vault.hashi_vault lookup + description: The past, present, and future of the C(hashi_vault) lookup. + - ref: 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.vault_kv2_get lookup + description: The official documentation for the C(community.hashi_vault.vault_kv2_get) lookup plugin. + - module: community.hashi_vault.vault_kv2_get + - ref: 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_kv1_get + - ref: community.hashi_vault Lookup Guide + description: Guidance on using lookups in C(community.hashi_vault). + notes: + - Due to a current limitation in the HVAC library there won't necessarily be an error if a bad endpoint is specified. + - As of community.hashi_vault 0.1.0, only the latest version of a secret is returned when specifying a KV v2 path. + - As of community.hashi_vault 0.1.0, all options can be supplied via term string (space delimited key=value pairs) or by parameters (see examples). + - As of community.hashi_vault 0.1.0, when I(secret) is the first option in the term string, C(secret=) is not required (see examples). + extends_documentation_fragment: + - community.hashi_vault.connection + - community.hashi_vault.connection.plugins + - community.hashi_vault.auth + - community.hashi_vault.auth.plugins + options: + secret: + description: Vault path to the secret being requested in the format C(path[:field]). + required: True + return_format: + description: + - Controls how multiple key/value pairs in a path are treated on return. + - C(dict) returns a single dict containing the key/value pairs. + - C(values) returns a list of all the values only. Use when you don't care about the keys. + - C(raw) returns the actual API result (deserialized), which includes metadata and may have the data nested in other keys. + choices: + - dict + - values + - raw + default: dict + aliases: [ as ] +""" + +EXAMPLES = """ +- ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/hello:value token=c975b780-d1be-8016-866b-01d0f9b688a5 url=http://myvault:8200') }}" + +- name: Return all secrets from a path + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/hello token=c975b780-d1be-8016-866b-01d0f9b688a5 url=http://myvault:8200') }}" + +- name: Vault that requires authentication via LDAP + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/hello:value auth_method=ldap mount_point=ldap username=myuser password=mypas') }}" + +- name: Vault that requires authentication via username and password + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/hola:val auth_method=userpass username=myuser password=psw url=http://vault:8200') }}" + +- name: Connect to Vault using TLS + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/hola:value token=c975b780-d1be-8016-866b-01d0f9b688a5 validate_certs=False') }}" + +- name: using certificate auth + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/hi:val token=xxxx url=https://vault:8200 validate_certs=True cacert=/cacert/path/ca.pem') }}" + +- name: Authenticate with a Vault app role + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/hello:value auth_method=approle role_id=myroleid secret_id=mysecretid') }}" + +- name: Return all secrets from a path in a namespace + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/hello token=c975b780-d1be-8016-866b-01d0f9b688a5 namespace=teama/admins') }}" + +# When using KV v2 the PATH should include "data" between the secret engine mount and path (e.g. "secret/data/:path") +# see: https://www.vaultproject.io/api/secret/kv/kv-v2.html#read-secret-version +- name: Return latest KV v2 secret from path + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/data/hello token=my_vault_token url=http://myvault_url:8200') }}" + +# The following examples show more modern syntax, with parameters specified separately from the term string. + +- name: secret= is not required if secret is first + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/hello token= url=http://myvault_url:8200') }}" + +- name: options can be specified as parameters rather than put in term string + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/hello', token=my_token_var, url='http://myvault_url:8200') }}" + +# return_format (or its alias 'as') can control how secrets are returned to you +- name: return secrets as a dict (default) + ansible.builtin.set_fact: + my_secrets: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/manysecrets', token=my_token_var, url='http://myvault_url:8200') }}" +- ansible.builtin.debug: + msg: "{{ my_secrets['secret_key'] }}" +- ansible.builtin.debug: + msg: "Secret '{{ item.key }}' has value '{{ item.value }}'" + loop: "{{ my_secrets | dict2items }}" + +- name: return secrets as values only + ansible.builtin.debug: + msg: "A secret value: {{ item }}" + loop: "{{ query('community.hashi_vault.hashi_vault', 'secret/data/manysecrets', token=my_token_var, url='http://vault_url:8200', return_format='values') }}" + +- name: return raw secret from API, including metadata + ansible.builtin.set_fact: + my_secret: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/hello:value', token=my_token_var, url='http://myvault_url:8200', as='raw') }}" +- ansible.builtin.debug: + msg: "This is version {{ my_secret['metadata']['version'] }} of hello:value. The secret data is {{ my_secret['data']['data']['value'] }}" + +# AWS IAM authentication method +# uses Ansible standard AWS options + +- name: authenticate with aws_iam + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/hello:value', auth_method='aws_iam', role_id='myroleid', profile=my_boto_profile) }}" + +# JWT auth + +- name: Authenticate with a JWT + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/hola:val', auth_method='jwt', role_id='myroleid', jwt='myjwt', url='https://vault:8200') }}" + +# Disabling Token Validation +# Use this when your token does not have the lookup-self capability. Usually this is applied to all tokens via the default policy. +# However you can choose to create tokens without applying the default policy, or you can modify your default policy not to include it. +# When disabled, your invalid or expired token will be indistinguishable from insufficent permissions. + +- name: authenticate without token validation + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/hello:value', token=my_token, token_validate=False) }}" + +# "none" auth method does no authentication and does not send a token to the Vault address. +# One example of where this could be used is with a Vault agent where the agent will handle authentication to Vault. +# https://www.vaultproject.io/docs/agent + +- name: authenticate with vault agent + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/hello:value', auth_method='none', url='http://127.0.0.1:8100') }}" + +# Use a proxy + +- name: use a proxy with login/password + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=... token=... url=https://... proxies=https://user:pass@myproxy:8080') }}" + +- name: 'use a socks proxy (need some additional dependencies, see: https://requests.readthedocs.io/en/master/user/advanced/#socks )' + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=... token=... url=https://... proxies=socks5://myproxy:1080') }}" + +- name: use proxies with a dict (as param) + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', '...', proxies={'http': 'http://myproxy1', 'https': 'http://myproxy2'}) }}" + +- name: use proxies with a dict (as param, pre-defined var) + vars: + prox: + http: http://myproxy1 + https: https://myproxy2 + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', '...', proxies=prox }}" + +- name: use proxies with a dict (as direct ansible var) + vars: + ansible_hashi_vault_proxies: + http: http://myproxy1 + https: https://myproxy2 + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', '...' }}" + +- name: use proxies with a dict (in the term string, JSON syntax) + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', '... proxies={\\"http\\":\\"http://myproxy1\\",\\"https\\":\\"http://myproxy2\\"}') }}" + +- name: use ansible vars to supply some options + vars: + ansible_hashi_vault_url: 'https://myvault:8282' + ansible_hashi_vault_auth_method: token + set_fact: + secret1: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/secret1') }}" + secret2: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/secret2') }}" + +- name: use a custom timeout + debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/secret1', timeout=120) }}" + +- name: use a custom timeout and retry on failure 3 times (with collection retry defaults) + vars: + ansible_hashi_vault_timeout: 5 + ansible_hashi_vault_retries: 3 + debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/secret1') }}" + +- name: retry on failure (with custom retry settings and no warnings) + vars: + ansible_hashi_vault_retries: + total: 6 + backoff_factor: 0.9 + status_forcelist: [500, 502] + allowed_methods: + - GET + - PUT + debug: + msg: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/secret1', retry_action='warn') }}" +""" + +RETURN = """ +_raw: + description: + - secrets(s) requested + type: list + elements: dict +""" + +from ansible.errors import AnsibleError +from ansible.utils.display import Display + +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() + +HAS_HVAC = False +try: + import hvac + HAS_HVAC = True +except ImportError: + HAS_HVAC = False + + +class LookupModule(HashiVaultLookupBase): + def run(self, terms, variables=None, **kwargs): + if not HAS_HVAC: + raise AnsibleError("Please pip install hvac to use the hashi_vault lookup module.") + + ret = [] + + for term in terms: + opts = kwargs.copy() + opts.update(self.parse_kev_term(term, first_unqualified='secret', plugin_name='hashi_vault')) + self.set_options(direct=opts, var_options=variables) + # TODO: remove process_deprecations() if backported fix is available (see method definition) + self.process_deprecations() + self.process_options() + + client_args = self.connection_options.get_hvac_connection_options() + self.client = self.helper.get_vault_client(**client_args) + + try: + self.authenticator.authenticate(self.client) + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + ret.extend(self.get()) + + return ret + + def process_options(self): + '''performs deep validation and value loading for options''' + + # process connection options + self.connection_options.process_connection_options() + + try: + self.authenticator.validate() + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + # secret field splitter + self.field_ops() + + # begin options processing methods + + def field_ops(self): + # split secret and field + secret = self.get_option('secret') + + s_f = secret.rsplit(':', 1) + self.set_option('secret', s_f[0]) + if len(s_f) >= 2: + field = s_f[1] + else: + field = None + self.set_option('secret_field', field) + + def get(self): + '''gets a secret. should always return a list''' + + secret = self.get_option('secret') + field = self.get_option('secret_field') + return_as = self.get_option('return_format') + + try: + data = self.client.read(secret) + except hvac.exceptions.Forbidden: + raise AnsibleError("Forbidden: Permission Denied to secret '%s'." % secret) + + if data is None: + raise AnsibleError("The secret '%s' doesn't seem to exist." % secret) + + if return_as == 'raw': + return [data] + + # Check response for KV v2 fields and flatten nested secret data. + # https://vaultproject.io/api/secret/kv/kv-v2.html#sample-response-1 + try: + # sentinel field checks + check_dd = data['data']['data'] + check_md = data['data']['metadata'] + # unwrap nested data + data = data['data'] + except KeyError: + pass + + if return_as == 'values': + return list(data['data'].values()) + + # everything after here implements return_as == 'dict' + if not field: + return [data['data']] + + if field not in data['data']: + raise AnsibleError("The secret %s does not contain the field '%s'. for hashi_vault lookup" % (secret, field)) + + return [data['data'][field]] diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_ansible_settings.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_ansible_settings.py new file mode 100644 index 00000000..f3103a95 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_ansible_settings.py @@ -0,0 +1,337 @@ +# (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''' +name: vault_ansible_settings +version_added: 2.5.0 +author: + - Brian Scholer (@briantist) +short_description: Returns plugin settings (options) +description: + - Returns a dictionary of options and their values for a given plugin. + - This is most useful for using plugin settings in modules and C(module_defaults), + especially when common settings are set in C(ansible.cfg), in Ansible vars, or via environment variables on the controller. + - Options can be filtered by name, and can include or exclude defaults, unset options, and private options. +seealso: + - ref: Module defaults + description: Using the C(module_defaults) keyword. +notes: + - This collection supports some "low precedence" environment variables that get loaded after all other sources, such as C(VAULT_ADDR). + - These environment variables B(are not supported) with this plugin. + - If you wish to use them, use the R(ansible.builtin.env lookup,ansible_collections.ansible.builtin.env_lookup) to + load them directly when calling a module or setting C(module_defaults). + - Similarly, any options that rely on additional processing to fill in their values will not have that done. + - For example, tokens will not be loaded from the token sink file, auth methods will not have their C(validate) methods called. + - See the examples for workarounds, but consider using Ansible-specific ways of setting these values instead. +options: + _terms: + description: + - The names of the options to load. + - Supports C(fnmatch) L(style wildcards,https://docs.python.org/3/library/fnmatch.html). + - Prepend any name or pattern with C(!) to invert the match. + type: list + elements: str + required: false + default: ['*'] + plugin: + description: + - The name of the plugin whose options will be returned. + - Only lookups are supported. + - Short names (without a dot C(.)) will be fully qualified with C(community.hashi_vault). + type: str + default: community.hashi_vault.vault_login + include_private: + description: Include options that begin with underscore C(_). + type: bool + default: false + include_none: + description: Include options whose value is C(None) (this usually means they are unset). + type: bool + default: false + include_default: + description: Include options whose value comes from a default. + type: bool + default: false +''' + +EXAMPLES = r''' +### In these examples, we assume an ansible.cfg like this: +# [hashi_vault_collection] +# url = https://config-based-vault.example.com +# retries = 5 +### end ansible.cfg + +### We assume some environment variables set as well +# ANSIBLE_HASHI_VAULT_URL: https://env-based-vault.example.com +# ANSIBLE_HASHI_VAULT_TOKEN: s.123456789 +### end environment variables + +# playbook - ansible-core 2.12 and higher +## set defaults for the collection group +- hosts: all + vars: + ansible_hashi_vault_auth_method: token + module_defaults: + group/community.hashi_vault.vault: "{{ lookup('community.hashi_vault.vault_ansible_settings') }}" + tasks: + - name: Get a secret from the remote host with settings from the controller + community.hashi_vault.vault_kv2_get: + path: app/some/secret +###### + +# playbook - ansible any version +## set defaults for a specific module +- hosts: all + vars: + ansible_hashi_vault_auth_method: token + module_defaults: + community.hashi_vault.vault_kv2_get: "{{ lookup('community.hashi_vault.vault_ansible_settings') }}" + tasks: + - name: Get a secret from the remote host with settings from the controller + community.hashi_vault.vault_kv2_get: + path: app/some/secret +###### + +# playbook - ansible any version +## set defaults for several modules +## do not use controller's auth +- hosts: all + vars: + ansible_hashi_vault_auth_method: aws_iam + settings: "{{ lookup('community.hashi_vault.vault_ansible_settings', '*', '!*token*') }}" + module_defaults: + community.hashi_vault.vault_kv2_get: '{{ settings }}' + community.hashi_vault.vault_kv1_get: '{{ settings }}' + tasks: + - name: Get a secret from the remote host with some settings from the controller, auth from remote + community.hashi_vault.vault_kv2_get: + path: app/some/secret + + - name: Same with kv1 + community.hashi_vault.vault_kv1_get: + path: app/some/secret +###### + +# playbook - ansible any version +## set defaults for several modules +## do not use controller's auth +## override returned settings +- hosts: all + vars: + ansible_hashi_vault_auth_method: userpass + plugin_settings: "{{ lookup('community.hashi_vault.vault_ansible_settings', '*', '!*token*') }}" + overrides: + auth_method: aws_iam + retries: '{{ (plugin_settings.retries | int) + 2 }}' + settings: >- + {{ + plugin_settings + | combine(overrides) + }} + module_defaults: + community.hashi_vault.vault_kv2_get: '{{ settings }}' + community.hashi_vault.vault_kv1_get: '{{ settings }}' + tasks: + - name: Get a secret from the remote host with some settings from the controller, auth from remote + community.hashi_vault.vault_kv2_get: + path: app/some/secret + + - name: Same with kv1 + community.hashi_vault.vault_kv1_get: + path: app/some/secret +###### + +# using a block is similar +- name: Settings + vars: + ansible_hashi_vault_auth_method: aws_iam + settings: "{{ lookup('community.hashi_vault.vault_ansible_settings', '*', '!*token*') }}" + module_defaults: + community.hashi_vault.vault_kv2_get: '{{ settings }}' + community.hashi_vault.vault_kv1_get: '{{ settings }}' + block: + - name: Get a secret from the remote host with some settings from the controller, auth from remote + community.hashi_vault.vault_kv2_get: + path: app/some/secret + + - name: Same with kv1 + community.hashi_vault.vault_kv1_get: + path: app/some/secret +##### + +# use settings from a different plugin +## when you need settings that are not in the default plugin (vault_login) +- name: Settings + vars: + ansible_hashi_vault_engine_mount_point: dept-secrets + settings: "{{ lookup('community.hashi_vault.vault_ansible_settings', plugin='community.hashi_vault.vault_kv2_get') }}" + module_defaults: + community.hashi_vault.vault_kv2_get: '{{ settings }}' + block: + - name: Get a secret from the remote host with some settings from the controller, auth from remote + community.hashi_vault.vault_kv2_get: + path: app/some/secret +##### + +# use settings from a different plugin (on an indivdual call) +## short names assume community.hashi_vault +- name: Settings + vars: + ansible_hashi_vault_engine_mount_point: dept-secrets + settings: "{{ lookup('community.hashi_vault.vault_ansible_settings') }}" + module_defaults: + community.hashi_vault.vault_kv2_get: '{{ settings }}' + block: + - name: Get a secret from the remote host with some settings from the controller, auth from remote + community.hashi_vault.vault_kv2_get: + engine_mount_point: "{{ lookup('community.hashi_vault.vault_ansible_settings', plugin='vault_kv2_get') }}" + path: app/some/secret +##### + +# normally, options with default values are not returned, but can be +- name: Settings + vars: + settings: "{{ lookup('community.hashi_vault.vault_ansible_settings') }}" + module_defaults: + # we usually want to use the remote host's IAM auth + community.hashi_vault.vault_kv2_get: >- + {{ + settings + | combine({'auth_method': aws_iam}) + }} + block: + - name: Use the plugin auth method instead, even if it is the default method + community.hashi_vault.vault_kv2_get: + auth_method: "{{ lookup('community.hashi_vault.vault_ansible_settings', 'auth_method', include_default=True) }}" + path: app/some/secret +##### + +# normally, options with None/null values are not returned, +# nor are private options (names begin with underscore _), +# but they can be returned too if desired +- name: Show all plugin settings + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_ansible_settings', include_none=True, include_private=True, include_default=True) }}" +##### + +# dealing with low-precedence env vars and token sink loading +## here, VAULT_ADDR is usually used with plugins, but that will not work with vault_ansible_settings. +## additionally, the CLI `vault login` is used before running Ansible, so the token sink is usually used, which also will not work. +- hosts: all + vars: + plugin_settings: "{{ lookup('community.hashi_vault.vault_ansible_settings', 'url', 'token*', include_default=True) }}" + overrides: + url: "{{ plugin_settings.url | default(lookup('ansible.builtin.env', 'VAULT_ADDR')) }}" + token: >- + {{ + plugin_settings.token + | default( + lookup( + 'ansible.builtin.file', + ( + plugin_settings.token_path | default(lookup('ansible.builtin.env', 'HOME')), + plugin_settings.token_file + ) | path_join + ) + ) + }} + auth_method: token + settings: >- + {{ + plugin_settings + | combine(overrides) + }} + module_defaults: + community.hashi_vault.vault_kv2_get: "{{ lookup('community.hashi_vault.vault_ansible_settings') }}" + tasks: + - name: Get a secret from the remote host with settings from the controller + community.hashi_vault.vault_kv2_get: + path: app/some/secret +##### +''' + +RETURN = r''' +_raw: + description: + - A dictionary of the options and their values. + - Only a single dictionary will be returned, even with multiple terms. + type: dict + sample: + retries: 5 + timeout: 20 + token: s.jRHAoqElnJDx6J5ExYelCDYR + url: https://vault.example.com +''' + +from fnmatch import fnmatchcase + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible import constants as C +from ansible.plugins.loader import lookup_loader +from ansible.utils.display import Display + + +display = Display() + + +class LookupModule(LookupBase): + def run(self, terms, variables=None, **kwargs): + self.set_options(direct=kwargs, var_options=variables) + + include_private = self.get_option('include_private') + include_none = self.get_option('include_none') + include_default = self.get_option('include_default') + + plugin = self.get_option('plugin') + if '.' not in plugin: + plugin = 'community.hashi_vault.' + plugin + + if not terms: + terms = ['*'] + + opts = {} + + try: + # ansible-core 2.10 or later + p = lookup_loader.find_plugin_with_context(plugin) + loadname = p.plugin_resolved_name + resolved = p.resolved + except AttributeError: + # ansible 2.9 + p = lookup_loader.find_plugin_with_name(plugin) + loadname = p[0] + resolved = loadname is not None + + if not resolved: + raise AnsibleError("'%s' plugin not found." % plugin) + + # Loading ensures that the options are initialized in ConfigManager + lookup_loader.get(plugin, class_only=True) + + pluginget = C.config.get_configuration_definitions('lookup', loadname) + + for option in pluginget.keys(): + if not include_private and option.startswith('_'): + continue + + keep = False + for pattern in terms: + if pattern.startswith('!'): + if keep and fnmatchcase(option, pattern[1:]): + keep = False + else: + keep = keep or fnmatchcase(option, pattern) + + if not keep: + continue + + value, origin = C.config.get_config_value_and_origin(option, None, 'lookup', loadname, None, variables=variables) + if (include_none or value is not None) and (include_default or origin != 'default'): + opts[option] = value + + return [opts] diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_kv1_get.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_kv1_get.py new file mode 100644 index 00000000..053150a8 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_kv1_get.py @@ -0,0 +1,220 @@ +# (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''' +name: 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: + - module: community.hashi_vault.vault_kv1_get + - ref: 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_kv2_get + - ref: community.hashi_vault Lookup Guide + description: Guidance on using lookups in C(community.hashi_vault). + - 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.connection + - community.hashi_vault.connection.plugins + - community.hashi_vault.auth + - community.hashi_vault.auth.plugins + - community.hashi_vault.engine_mount + - community.hashi_vault.engine_mount.plugins +options: + _terms: + description: + - Vault KV path(s) to be read. + - These are relative to the I(engine_mount_point), so the mount path should not be included. + type: str + required: True + engine_mount_point: + default: kv +''' + +EXAMPLES = r''' +- name: Read a kv1 secret with the default mount point + ansible.builtin.set_fact: + response: "{{ lookup('community.hashi_vault.vault_kv1_get', 'hello', url='https://vault:8201') }}" + # 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 kv1 secret with a different mount point + ansible.builtin.set_fact: + response: "{{ lookup('community.hashi_vault.vault_kv1_get', 'hello', engine_mount_point='custom/kv1/mount', url='https://vault:8201') }}" + # 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 }}" + - "Value of key 'password' in the secret: {{ response.secret.password }}" + +- name: Perform multiple kv1 reads with a single Vault login, showing the secrets + vars: + paths: + - hello + - my-secret/one + - my-secret/two + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_kv1_get', *paths, auth_method='userpass', username=user, password=pwd)['secret'] }}" + +- name: Perform multiple kv1 reads with a single Vault login in a loop + vars: + paths: + - hello + - my-secret/one + - my-secret/two + ansible.builtin.debug: + msg: '{{ item }}' + loop: "{{ query('community.hashi_vault.vault_kv1_get', *paths, auth_method='userpass', username=user, password=pwd) }}" + +- name: Perform multiple kv1 reads with a single Vault login in a loop (via with_), display values only + vars: + ansible_hashi_vault_auth_method: userpass + ansible_hashi_vault_username: '{{ user }}' + ansible_hashi_vault_password: '{{ pwd }}' + ansible.builtin.debug: + msg: '{{ item.values() | list }}' + with_community.hashi_vault.vault_kv1_get: + - hello + - my-secret/one + - my-secret/two +''' + +RETURN = r''' +_raw: + description: + - The result of the read(s) against the given path(s). + type: list + elements: dict + contains: + 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 +''' + +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) + + engine_mount_point = self._options_adapter.get_option('engine_mount_point') + + try: + self.authenticator.validate() + self.authenticator.authenticate(client) + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + for term in terms: + try: + raw = client.secrets.kv.v1.read_secret(path=term, mount_point=engine_mount_point) + except hvac.exceptions.Forbidden as e: + raise_from(AnsibleError("Forbidden: Permission Denied to path ['%s']." % term), e) + except hvac.exceptions.InvalidPath as e: + if 'Invalid path for a versioned K/V secrets engine' in str(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']." + + raise_from(AnsibleError(msg % (term,)), e) + + metadata = raw.copy() + data = metadata.pop('data') + + ret.append(dict(raw=raw, data=data, secret=data, metadata=metadata)) + + return ret diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_kv2_get.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_kv2_get.py new file mode 100644 index 00000000..bbd54ef8 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_kv2_get.py @@ -0,0 +1,233 @@ +# (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''' +name: 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: + - module: community.hashi_vault.vault_kv2_get + - ref: 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_kv1_get + - ref: community.hashi_vault Lookup Guide + description: Guidance on using lookups in C(community.hashi_vault). + - name: KV2 Secrets Engine + description: Documentation for the Vault KV secrets engine, version 2. + link: https://www.vaultproject.io/docs/secrets/kv/kv-v2 +extends_documentation_fragment: + - community.hashi_vault.connection + - community.hashi_vault.connection.plugins + - community.hashi_vault.auth + - community.hashi_vault.auth.plugins + - community.hashi_vault.engine_mount + - community.hashi_vault.engine_mount.plugins +options: + _terms: + description: + - Vault KV path(s) to be read. + - These are relative to the I(engine_mount_point), so the mount path should not be included. + type: str + required: True + engine_mount_point: + default: secret + version: + description: Specifies the version to return. If not set the latest version is returned. + type: int +''' + +EXAMPLES = r''' +- name: Read a kv2 secret with the default mount point + ansible.builtin.set_fact: + response: "{{ lookup('community.hashi_vault.vault_kv2_get', 'hello', url='https://vault:8201') }}" + # equivalent API path in 3.x.x is kv/data/hello + # equivalent API path in 4.0.0+ 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 kv2 secret with a different mount point + ansible.builtin.set_fact: + response: "{{ lookup('community.hashi_vault.vault_kv2_get', 'hello', version=5, engine_mount_point='custom/kv2/mount', url='https://vault:8201') }}" + # 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 + +- name: Perform multiple kv2 reads with a single Vault login, showing the secrets + vars: + paths: + - hello + - my-secret/one + - my-secret/two + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_kv2_get', *paths, auth_method='userpass', username=user, password=pwd)['secret'] }}" + +- name: Perform multiple kv2 reads with a single Vault login in a loop + vars: + paths: + - hello + - my-secret/one + - my-secret/two + ansible.builtin.debug: + msg: '{{ item }}' + loop: "{{ query('community.hashi_vault.vault_kv2_get', *paths, auth_method='userpass', username=user, password=pwd) }}" + +- name: Perform multiple kv2 reads with a single Vault login in a loop (via with_), display values only + vars: + ansible_hashi_vault_auth_method: userpass + ansible_hashi_vault_username: '{{ user }}' + ansible_hashi_vault_password: '{{ pwd }}' + ansible_hashi_vault_engine_mount_point: special/kv2 + ansible.builtin.debug: + msg: '{{ item.values() | list }}' + with_community.hashi_vault.vault_kv2_get: + - hello + - my-secret/one + - my-secret/two +''' + +RETURN = r''' +_raw: + description: + - The result of the read(s) against the given path(s). + type: list + elements: dict + contains: + 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 +''' + +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) + + version = self._options_adapter.get_option_default('version') + engine_mount_point = self._options_adapter.get_option('engine_mount_point') + + try: + self.authenticator.validate() + self.authenticator.authenticate(client) + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + for term in terms: + try: + raw = client.secrets.kv.v2.read_secret_version(path=term, version=version, mount_point=engine_mount_point) + 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("Invalid or missing path ['%s'] with secret version '%s'. Check the path or secret version." % (term, version or 'latest')), + e + ) + + data = raw['data'] + metadata = data['metadata'] + secret = data['data'] + + ret.append(dict(raw=raw, data=data, secret=secret, metadata=metadata)) + + return ret diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_list.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_list.py new file mode 100644 index 00000000..56521c79 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_list.py @@ -0,0 +1,183 @@ +# (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 = """ + name: 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: + - module: community.hashi_vault.vault_list + extends_documentation_fragment: + - community.hashi_vault.connection + - community.hashi_vault.connection.plugins + - community.hashi_vault.auth + - community.hashi_vault.auth.plugins + options: + _terms: + description: Vault path(s) to be listed. + type: str + required: true +""" + +EXAMPLES = """ +- name: List all secrets at a path + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_list', 'secret/metadata', url='https://vault:8201') }}" + # For kv2, the path needs to follow the pattern 'mount_point/metadata' or 'mount_point/metadata/path' to list all secrets in that path + +- name: List access policies + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_list', 'sys/policies/acl', url='https://vault:8201') }}" + +- name: Perform multiple list operations with a single Vault login + vars: + paths: + - secret/metadata + - sys/policies/acl + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_list', *paths, auth_method='userpass', username=user, password=pwd) }}" + +- name: Perform multiple list operations with a single Vault login in a loop + vars: + paths: + - secret/metadata + - sys/policies/acl + ansible.builtin.debug: + msg: '{{ item }}' + loop: "{{ query('community.hashi_vault.vault_list', *paths, auth_method='userpass', username=user, password=pwd) }}" + +- name: Perform list operations with a single Vault login in a loop (via with_) + vars: + ansible_hashi_vault_auth_method: userpass + ansible_hashi_vault_username: '{{ user }}' + ansible_hashi_vault_password: '{{ pwd }}' + ansible.builtin.debug: + msg: '{{ item }}' + with_community.hashi_vault.vault_list: + - secret/metadata + - sys/policies/acl + +- name: Create fact consisting of list of dictionaries each with secret name (e.g. username) and value of a key (e.g. 'password') within that secret + ansible.builtin.set_fact: + credentials: >- + {{ + credentials + | default([]) + [ + { + 'username': item, + 'password': lookup('community.hashi_vault.vault_kv2_get', item, engine_mount_point='vpn-users').secret.password + } + ] + }} + loop: "{{ query('community.hashi_vault.vault_list', 'vpn-users/metadata')[0].data['keys'] }}" + no_log: true + +- ansible.builtin.debug: + msg: "{{ credentials }}" + +- name: Create the same as above without looping, and only 2 logins + vars: + secret_names: >- + {{ + query('community.hashi_vault.vault_list', 'vpn-users/metadata') + | map(attribute='data') + | map(attribute='keys') + | flatten + }} + secret_values: >- + {{ + lookup('community.hashi_vault.vault_kv2_get', *secret_names, engine_mount_point='vpn-users') + | map(attribute='secret') + | map(attribute='password') + | flatten + }} + credentials_dict: "{{ dict(secret_names | zip(secret_values)) }}" + ansible.builtin.set_fact: + credentials_dict: "{{ credentials_dict }}" + credentials_list: "{{ credentials_dict | dict2items(key_name='username', value_name='password') }}" + no_log: true + +- ansible.builtin.debug: + msg: + - "Dictionary: {{ credentials_dict }}" + - "List: {{ credentials_list }}" + +- name: List all userpass users and output the token policies for each user + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_read', 'auth/userpass/users/' + item).data.token_policies }}" + loop: "{{ query('community.hashi_vault.vault_list', 'auth/userpass/users')[0].data['keys'] }}" +""" + +RETURN = """ +_raw: + description: + - The raw result of the read 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) + + try: + self.authenticator.validate() + self.authenticator.authenticate(client) + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + for term in terms: + try: + data = client.list(term) + except hvac.exceptions.Forbidden: + raise AnsibleError("Forbidden: Permission Denied to path '%s'." % term) + + if data is None: + raise AnsibleError("The path '%s' doesn't seem to exist." % term) + + ret.append(data) + + return ret diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_login.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_login.py new file mode 100644 index 00000000..ebb49e4a --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_login.py @@ -0,0 +1,138 @@ +# (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 = """ + name: 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: + - module: community.hashi_vault.vault_login + - ref: community.hashi_vault.vault_login_token filter + description: The official documentation for the C(community.hashi_vault.vault_login_token) filter plugin. + notes: + - This lookup does not use the term string and will not work correctly in loops. Only a single response will be returned. + - "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 plugin 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." + extends_documentation_fragment: + - community.hashi_vault.connection + - community.hashi_vault.connection.plugins + - community.hashi_vault.auth + - community.hashi_vault.auth.plugins + options: + _terms: + description: This is unused and any terms supplied will be ignored. + type: str + required: false + token_validate: + default: true +""" + +EXAMPLES = """ +- name: Set a fact with a lookup result + set_fact: + login_data: "{{ lookup('community.hashi_vault.vault_login', url='https://vault', auth_method='userpass', username=user, password=pwd) }}" + +- 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.auth.client_token }}' + path: auth/approle/role/role-name/role-id + register: approle_id +""" + +RETURN = """ +_raw: + description: + - The result of the login with the given auth method. + type: list + elements: 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 +""" + +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 + ) + + self.set_options(direct=kwargs, var_options=variables) + # TODO: remove process_deprecations() if backported fix is available (see method definition) + self.process_deprecations() + + if self.get_option('auth_method') == 'none': + raise AnsibleError("The 'none' auth method is not valid for this lookup.") + + self.connection_options.process_connection_options() + client_args = self.connection_options.get_hvac_connection_options() + client = self.helper.get_vault_client(**client_args) + + if len(terms) != 0: + display.warning("Supplied term strings will be ignored. This lookup does not use term strings.") + + try: + self.authenticator.validate() + response = self.authenticator.authenticate(client) + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + return [response] diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_read.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_read.py new file mode 100644 index 00000000..794262ed --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_read.py @@ -0,0 +1,137 @@ +# (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 = """ + name: 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: + - module: community.hashi_vault.vault_read + - ref: 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.connection + - community.hashi_vault.connection.plugins + - community.hashi_vault.auth + - community.hashi_vault.auth.plugins + options: + _terms: + description: Vault path(s) to be read. + type: str + required: True +""" + +EXAMPLES = """ +- name: Read a kv2 secret + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_read', 'secret/data/hello', url='https://vault:8201') }}" + +- name: Retrieve an approle role ID + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_read', 'auth/approle/role/role-name/role-id', url='https://vault:8201') }}" + +- name: Perform multiple reads with a single Vault login + vars: + paths: + - secret/data/hello + - auth/approle/role/role-one/role-id + - auth/approle/role/role-two/role-id + ansible.builtin.debug: + msg: "{{ lookup('community.hashi_vault.vault_read', *paths, auth_method='userpass', username=user, password=pwd) }}" + +- name: Perform multiple reads with a single Vault login in a loop + vars: + paths: + - secret/data/hello + - auth/approle/role/role-one/role-id + - auth/approle/role/role-two/role-id + ansible.builtin.debug: + msg: '{{ item }}' + loop: "{{ query('community.hashi_vault.vault_read', *paths, auth_method='userpass', username=user, password=pwd) }}" + +- name: Perform multiple reads with a single Vault login in a loop (via with_) + vars: + ansible_hashi_vault_auth_method: userpass + ansible_hashi_vault_username: '{{ user }}' + ansible_hashi_vault_password: '{{ pwd }}' + ansible.builtin.debug: + msg: '{{ item }}' + with_community.hashi_vault.vault_read: + - secret/data/hello + - auth/approle/role/role-one/role-id + - auth/approle/role/role-two/role-id +""" + +RETURN = """ +_raw: + description: + - The raw result of the read 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) + + try: + self.authenticator.validate() + self.authenticator.authenticate(client) + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + for term in terms: + try: + data = client.read(term) + except hvac.exceptions.Forbidden: + raise AnsibleError("Forbidden: Permission Denied to path '%s'." % term) + + if data is None: + raise AnsibleError("The path '%s' doesn't seem to exist." % term) + + ret.append(data) + + return ret diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_token_create.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_token_create.py new file mode 100644 index 00000000..9b19ae29 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_token_create.py @@ -0,0 +1,195 @@ +# (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 = """ + name: 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: + - module: community.hashi_vault.vault_token_create + - ref: community.hashi_vault.vault_login lookup + description: The official documentation for the C(community.hashi_vault.vault_login) lookup plugin. + - module: community.hashi_vault.vault_login + - ref: community.hashi_vault.vault_login_token filter + description: The official documentation for the C(community.hashi_vault.vault_login_token) filter plugin. + 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. + - 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=no) in order to have a valid token that can be used. + extends_documentation_fragment: + - community.hashi_vault.connection + - community.hashi_vault.connection.plugins + - community.hashi_vault.auth + - community.hashi_vault.auth.plugins + - community.hashi_vault.token_create + - community.hashi_vault.wrapping + - community.hashi_vault.wrapping.plugins + options: + _terms: + description: This is unused and any terms supplied will be ignored. + type: str + required: false +""" + +EXAMPLES = """ +- name: Login via userpass and create a child token + ansible.builtin.set_fact: + token_data: "{{ lookup('community.hashi_vault.vault_token_create', url='https://vault', auth_method='userpass', username=user, password=passwd) }}" + +- 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 (token via direct dict access) + community.hashi_vault.vault_read: + url: https://vault:8201 + auth_method: token + token: '{{ token_data.auth.client_token }}' + path: auth/approle/role/role-name/role-id + register: approle_id + +# implicitly uses url & token auth with a token from the environment +- name: Create an orphaned token with a short TTL and display the full response + ansible.builtin.debug: + var: lookup('community.hashi_vault.vault_token_create', orphan=True, ttl='60s') +""" + +RETURN = """ +_raw: + 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 +""" + +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): + 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(self, terms, variables=None, **kwargs): + if HVAC_IMPORT_ERROR: + raise_from( + AnsibleError("This plugin requires the 'hvac' Python library"), + HVAC_IMPORT_ERROR + ) + + 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) + + if len(terms) != 0: + display.warning("Supplied term strings will be ignored. This lookup does not use term strings.") + + try: + self.authenticator.validate() + self.authenticator.authenticate(client) + except (NotImplementedError, HashiVaultValueError) as e: + raise AnsibleError(e) + + pass_thru_options = self._options_adapter.get_filled_options(*self.PASS_THRU_OPTION_NAMES) + + orphan_options = pass_thru_options.copy() + + for key in pass_thru_options.keys(): + if key in self.ORPHAN_OPTION_TRANSLATION: + orphan_options[self.ORPHAN_OPTION_TRANSLATION[key]] = orphan_options.pop(key) + + response = None + + if self.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: + raise AnsibleError(e) + else: + try: + response = client.auth.token.create(**pass_thru_options) + except Exception as e: + raise AnsibleError(e) + + return [response] diff --git a/ansible_collections/community/hashi_vault/plugins/lookup/vault_write.py b/ansible_collections/community/hashi_vault/plugins/lookup/vault_write.py new file mode 100644 index 00000000..97ff0c09 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/lookup/vault_write.py @@ -0,0 +1,191 @@ +# (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 = """ + 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 + 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 + 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 diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_approle.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_approle.py new file mode 100644 index 00000000..0c261d3a --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_approle.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultAuthMethodBase + + +class HashiVaultAuthMethodApprole(HashiVaultAuthMethodBase): + '''HashiVault option group class for auth: approle''' + + NAME = 'approle' + OPTIONS = ['role_id', 'secret_id', 'mount_point'] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodApprole, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def validate(self): + self.validate_by_required_fields('role_id') + + def authenticate(self, client, use_token=True): + params = self._options.get_filled_options(*self.OPTIONS) + try: + response = client.auth.approle.login(use_token=use_token, **params) + except (NotImplementedError, AttributeError): + self.warn("HVAC should be updated to version 0.10.6 or higher. Deprecated method 'auth_approle' will be used.") + response = client.auth_approle(use_token=use_token, **params) + + return response diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_aws_iam.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_aws_iam.py new file mode 100644 index 00000000..e3bb004b --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_aws_iam.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import ( + HashiVaultAuthMethodBase, + HashiVaultValueError, +) + + +class HashiVaultAuthMethodAwsIam(HashiVaultAuthMethodBase): + '''HashiVault option group class for auth: userpass''' + + NAME = 'aws_iam' + OPTIONS = [ + 'aws_profile', + 'aws_access_key', + 'aws_secret_key', + 'aws_security_token', + 'region', + 'aws_iam_server_id', + 'role_id', + ] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodAwsIam, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def validate(self): + params = { + 'access_key': self._options.get_option_default('aws_access_key'), + 'secret_key': self._options.get_option_default('aws_secret_key'), + } + + session_token = self._options.get_option_default('aws_security_token') + if session_token: + params['session_token'] = session_token + + mount_point = self._options.get_option_default('mount_point') + if mount_point: + params['mount_point'] = mount_point + + role = self._options.get_option_default('role_id') + if role: + params['role'] = role + + region = self._options.get_option_default('region') + if region: + params['region'] = region + + header_value = self._options.get_option_default('aws_iam_server_id') + if header_value: + params['header_value'] = header_value + + if not (params['access_key'] and params['secret_key']): + try: + import boto3 + import botocore + except ImportError: + raise HashiVaultValueError("boto3 is required for loading a profile or IAM role credentials.") + + profile = self._options.get_option_default('aws_profile') + try: + session_credentials = boto3.session.Session(profile_name=profile).get_credentials() + except botocore.exceptions.ProfileNotFound: + raise HashiVaultValueError("The AWS profile '%s' was not found." % profile) + + if not session_credentials: + raise HashiVaultValueError("No AWS credentials supplied or available.") + + params['access_key'] = session_credentials.access_key + params['secret_key'] = session_credentials.secret_key + if session_credentials.token: + params['session_token'] = session_credentials.token + + self._auth_aws_iam_login_params = params + + def authenticate(self, client, use_token=True): + params = self._auth_aws_iam_login_params + try: + response = client.auth.aws.iam_login(use_token=use_token, **params) + except (NotImplementedError, AttributeError): + self.warn("HVAC should be updated to version 0.9.3 or higher. Deprecated method 'auth_aws_iam' will be used.") + client.auth_aws_iam(use_token=use_token, **params) + + return response diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_azure.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_azure.py new file mode 100644 index 00000000..36f44e07 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_azure.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022 Junrui Chen (@jchenship) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import ( + HashiVaultAuthMethodBase, + HashiVaultValueError, +) + + +class HashiVaultAuthMethodAzure(HashiVaultAuthMethodBase): + '''HashiVault auth method for Azure''' + + NAME = 'azure' + OPTIONS = [ + 'role_id', + 'jwt', + 'mount_point', + 'azure_tenant_id', + 'azure_client_id', + 'azure_client_secret', + 'azure_resource', + ] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodAzure, self).__init__( + option_adapter, warning_callback, deprecate_callback + ) + + def validate(self): + params = { + 'role': self._options.get_option_default('role_id'), + 'jwt': self._options.get_option_default('jwt'), + } + if not params['role']: + raise HashiVaultValueError( + 'role_id is required for azure authentication.' + ) + + # if mount_point is not provided, it will use the default value defined + # in hvac library (e.g. `azure`) + mount_point = self._options.get_option_default('mount_point') + if mount_point: + params['mount_point'] = mount_point + + # if jwt exists, use provided jwt directly, otherwise trying to get jwt + # from azure service principal or managed identity + if not params['jwt']: + azure_tenant_id = self._options.get_option_default('azure_tenant_id') + azure_client_id = self._options.get_option_default('azure_client_id') + azure_client_secret = self._options.get_option_default('azure_client_secret') + + # the logic of getting azure scope is from this function + # https://github.com/Azure/azure-cli/blob/azure-cli-2.39.0/src/azure-cli-core/azure/cli/core/auth/util.py#L72 + # the reason we expose resource instead of scope is resource is + # more aligned with the vault azure auth config here + # https://www.vaultproject.io/api-docs/auth/azure#resource + azure_resource = self._options.get_option('azure_resource') + azure_scope = azure_resource + "/.default" + + try: + import azure.identity + except ImportError: + raise HashiVaultValueError( + "azure-identity is required for getting access token from azure service principal or managed identity." + ) + + if azure_client_id and azure_client_secret: + # service principal + if not azure_tenant_id: + raise HashiVaultValueError( + 'azure_tenant_id is required when using azure service principal.' + ) + azure_credentials = azure.identity.ClientSecretCredential( + azure_tenant_id, azure_client_id, azure_client_secret + ) + elif azure_client_id: + # user assigned managed identity + azure_credentials = azure.identity.ManagedIdentityCredential( + client_id=azure_client_id + ) + else: + # system assigned managed identity + azure_credentials = azure.identity.ManagedIdentityCredential() + + params['jwt'] = azure_credentials.get_token(azure_scope).token + + self._auth_azure_login_params = params + + def authenticate(self, client, use_token=True): + params = self._auth_azure_login_params + response = client.auth.azure.login(use_token=use_token, **params) + return response diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_cert.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_cert.py new file mode 100644 index 00000000..af5d3bb1 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_cert.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Devon Mar (@devon-mar) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultAuthMethodBase + + +class HashiVaultAuthMethodCert(HashiVaultAuthMethodBase): + """HashiVault option group class for auth: cert""" + + NAME = "cert" + OPTIONS = ["cert_auth_public_key", "cert_auth_private_key", "mount_point", "role_id"] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodCert, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def validate(self): + self.validate_by_required_fields("cert_auth_public_key", "cert_auth_private_key") + + def authenticate(self, client, use_token=True): + options = self._options.get_filled_options(*self.OPTIONS) + + params = { + "cert_pem": options["cert_auth_public_key"], + "key_pem": options["cert_auth_private_key"], + } + + if "mount_point" in options: + params["mount_point"] = options["mount_point"] + if "role_id" in options: + params["name"] = options["role_id"] + + try: + response = client.auth.cert.login(use_token=use_token, **params) + except NotImplementedError: + raise NotImplementedError("cert authentication requires HVAC version 0.10.12 or higher.") + + return response diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_jwt.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_jwt.py new file mode 100644 index 00000000..da291942 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_jwt.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultAuthMethodBase + + +class HashiVaultAuthMethodJwt(HashiVaultAuthMethodBase): + '''HashiVault option group class for auth: jwt''' + + NAME = 'jwt' + OPTIONS = ['jwt', 'role_id', 'mount_point'] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodJwt, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def validate(self): + self.validate_by_required_fields('role_id', 'jwt') + + def authenticate(self, client, use_token=True): + params = self._options.get_filled_options(*self.OPTIONS) + params['role'] = params.pop('role_id') + + if 'mount_point' in params: + params['path'] = params.pop('mount_point') + + try: + response = client.auth.jwt.jwt_login(**params) + except (NotImplementedError, AttributeError): + raise NotImplementedError("JWT authentication requires HVAC version 0.10.5 or higher.") + + # must manually set the client token with JWT login + # see https://github.com/hvac/hvac/issues/644 + # fixed in https://github.com/hvac/hvac/pull/746 + # but we do it manually to maintain compatibilty with older hvac versions. + if use_token: + client.token = response['auth']['client_token'] + + return response diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_ldap.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_ldap.py new file mode 100644 index 00000000..7fcb6b38 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_ldap.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultAuthMethodBase + + +class HashiVaultAuthMethodLdap(HashiVaultAuthMethodBase): + '''HashiVault option group class for auth: ldap''' + + NAME = 'ldap' + OPTIONS = ['username', 'password', 'mount_point'] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodLdap, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def validate(self): + self.validate_by_required_fields('username', 'password') + + def authenticate(self, client, use_token=True): + params = self._options.get_filled_options(*self.OPTIONS) + try: + response = client.auth.ldap.login(use_token=use_token, **params) + except (NotImplementedError, AttributeError): + self.warn("HVAC should be updated to version 0.7.0 or higher. Deprecated method 'auth_ldap' will be used.") + response = client.auth_ldap(use_token=use_token, **params) + + return response diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_none.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_none.py new file mode 100644 index 00000000..22c3e28f --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_none.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultAuthMethodBase + + +class HashiVaultAuthMethodNone(HashiVaultAuthMethodBase): + '''HashiVault option group class for auth: none''' + + NAME = 'none' + OPTIONS = [] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodNone, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def validate(self): + pass + + def authenticate(self, client, use_token=False): + return None diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_token.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_token.py new file mode 100644 index 00000000..3b66b193 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_token.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import ( + HashiVaultAuthMethodBase, + HashiVaultValueError, +) + + +class HashiVaultAuthMethodToken(HashiVaultAuthMethodBase): + '''HashiVault option group class for auth: userpass''' + + NAME = 'token' + OPTIONS = ['token', 'token_path', 'token_file', 'token_validate'] + + _LATE_BINDING_ENV_VAR_OPTIONS = { + 'token': dict(env=['VAULT_TOKEN']), + 'token_path': dict(env=['HOME']), + } + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodToken, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def _simulate_login_response(self, token, lookup_response=None): + '''returns a similar structure to a login method's return, optionally incorporating a lookup-self response''' + + response = { + 'auth': { + 'client_token': token + } + } + + if lookup_response is None: + return response + + # first merge in the entire response at the top level + # but, rather than being missing, the auth field is going to be None, + # so we explicitly overwrite that with our original value. + response.update(lookup_response, auth=response['auth']) + + # then we'll merge the data field right into the auth field + response['auth'].update(lookup_response['data']) + + # and meta->metadata needs a name change + metadata = response['auth'].pop('meta', None) + if metadata: + response['auth']['metadata'] = metadata + + return response + + def validate(self): + self.process_late_binding_env_vars(self._LATE_BINDING_ENV_VAR_OPTIONS) + + if self._options.get_option_default('token') is None and self._options.get_option_default('token_path') is not None: + token_filename = os.path.join( + self._options.get_option('token_path'), + self._options.get_option('token_file') + ) + if os.path.exists(token_filename): + if not os.path.isfile(token_filename): + raise HashiVaultValueError("The Vault token file '%s' was found but is not a file." % token_filename) + with open(token_filename) as token_file: + self._options.set_option('token', token_file.read().strip()) + + if self._options.get_option_default('token') is None: + raise HashiVaultValueError("No Vault Token specified or discovered.") + + def authenticate(self, client, use_token=True, lookup_self=False): + token = self._stringify(self._options.get_option('token')) + validate = self._options.get_option_default('token_validate') + + response = None + + if use_token: + client.token = token + + if lookup_self or validate: + from hvac import exceptions + + try: + try: + response = client.auth.token.lookup_self() + except (NotImplementedError, AttributeError): + # usually we would warn here, but the v1 method doesn't seem to be deprecated (yet?) + response = client.lookup_token() # when token=None on this method, it calls lookup-self + except (exceptions.Forbidden, exceptions.InvalidPath, exceptions.InvalidRequest): + if validate: + raise HashiVaultValueError("Invalid Vault Token Specified.") + + return self._simulate_login_response(token, response) diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_userpass.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_userpass.py new file mode 100644 index 00000000..f9ba58f6 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_auth_method_userpass.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultAuthMethodBase + + +class HashiVaultAuthMethodUserpass(HashiVaultAuthMethodBase): + '''HashiVault option group class for auth: userpass''' + + NAME = 'userpass' + OPTIONS = ['username', 'password', 'mount_point'] + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodUserpass, self).__init__(option_adapter, warning_callback, deprecate_callback) + + def validate(self): + self.validate_by_required_fields('username', 'password') + + def authenticate(self, client, use_token=True): + params = self._options.get_filled_options(*self.OPTIONS) + try: + response = client.auth.userpass.login(**params) + except (NotImplementedError, AttributeError): + self.warn("HVAC should be updated to version 0.9.6 or higher. Deprecated method 'auth_userpass' will be used.") + response = client.auth_userpass(**params) + + # must manually set the client token with userpass login + # see https://github.com/hvac/hvac/issues/644 + # fixed in 0.11.0 (https://github.com/hvac/hvac/pull/733) + # but we keep the old behavior to maintain compatibility with older hvac + if use_token: + client.token = response['auth']['client_token'] + + return response diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_authenticator.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_authenticator.py new file mode 100644 index 00000000..acf574bf --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_authenticator.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +# please keep this list in alphabetical order of auth method name +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_approle import HashiVaultAuthMethodApprole +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_aws_iam import HashiVaultAuthMethodAwsIam +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_azure import HashiVaultAuthMethodAzure +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_cert import HashiVaultAuthMethodCert +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_jwt import HashiVaultAuthMethodJwt +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_ldap import HashiVaultAuthMethodLdap +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_none import HashiVaultAuthMethodNone +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_token import HashiVaultAuthMethodToken +from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_userpass import HashiVaultAuthMethodUserpass + + +class HashiVaultAuthenticator(): + ARGSPEC = dict( + auth_method=dict(type='str', default='token', choices=[ + 'token', + 'userpass', + 'ldap', + 'approle', + 'aws_iam', + 'azure', + 'jwt', + 'cert', + 'none', + ]), + mount_point=dict(type='str'), + token=dict(type='str', no_log=True, default=None), + token_path=dict(type='str', default=None, no_log=False), + token_file=dict(type='str', default='.vault-token'), + token_validate=dict(type='bool', default=False), + username=dict(type='str'), + password=dict(type='str', no_log=True), + role_id=dict(type='str'), + secret_id=dict(type='str', no_log=True), + jwt=dict(type='str', no_log=True), + aws_profile=dict(type='str', aliases=['boto_profile']), + aws_access_key=dict(type='str', aliases=['aws_access_key_id'], no_log=False), + aws_secret_key=dict(type='str', aliases=['aws_secret_access_key'], no_log=True), + aws_security_token=dict(type='str', no_log=False), + region=dict(type='str'), + aws_iam_server_id=dict(type='str'), + azure_tenant_id=dict(type='str'), + azure_client_id=dict(type='str'), + azure_client_secret=dict(type='str', no_log=True), + azure_resource=dict(type='str', default='https://management.azure.com/'), + cert_auth_private_key=dict(type='path', no_log=False), + cert_auth_public_key=dict(type='path'), + ) + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + self._options = option_adapter + self._selector = { + # please keep this list in alphabetical order of auth method name + # so that it's easier to scan and see at a glance that a given auth method is present or absent + 'approle': HashiVaultAuthMethodApprole(option_adapter, warning_callback, deprecate_callback), + 'aws_iam': HashiVaultAuthMethodAwsIam(option_adapter, warning_callback, deprecate_callback), + 'azure': HashiVaultAuthMethodAzure(option_adapter, warning_callback, deprecate_callback), + 'cert': HashiVaultAuthMethodCert(option_adapter, warning_callback, deprecate_callback), + 'jwt': HashiVaultAuthMethodJwt(option_adapter, warning_callback, deprecate_callback), + 'ldap': HashiVaultAuthMethodLdap(option_adapter, warning_callback, deprecate_callback), + 'none': HashiVaultAuthMethodNone(option_adapter, warning_callback, deprecate_callback), + 'token': HashiVaultAuthMethodToken(option_adapter, warning_callback, deprecate_callback), + 'userpass': HashiVaultAuthMethodUserpass(option_adapter, warning_callback, deprecate_callback), + } + + self.warn = warning_callback + self.deprecate = deprecate_callback + + def _get_method_object(self, method=None): + if method is None: + method = self._options.get_option('auth_method') + + try: + o_method = self._selector[method] + except KeyError: + raise NotImplementedError("auth method '%s' is not implemented in HashiVaultAuthenticator" % method) + + return o_method + + def validate(self, *args, **kwargs): + method = self._get_method_object(kwargs.pop('method', None)) + method.validate(*args, **kwargs) + + def authenticate(self, *args, **kwargs): + method = self._get_method_object(kwargs.pop('method', None)) + return method.authenticate(*args, **kwargs) diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_connection_options.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_connection_options.py new file mode 100644 index 00000000..f570479d --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_connection_options.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os + +from ansible.module_utils.common.text.converters import to_text + +from ansible.module_utils.common.validation import ( + check_type_dict, + check_type_str, + check_type_bool, + check_type_int, +) + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultOptionGroupBase + +# we implement retries via the urllib3 Retry class +# https://github.com/ansible-collections/community.hashi_vault/issues/58 +HAS_RETRIES = False +try: + from requests import Session + from requests.adapters import HTTPAdapter + try: + # try for a standalone urllib3 + import urllib3 + HAS_RETRIES = True + except ImportError: + try: + # failing that try for a vendored version within requests + from requests.packages import urllib3 + HAS_RETRIES = True + except ImportError: + pass +except ImportError: + pass + + +class HashiVaultConnectionOptions(HashiVaultOptionGroupBase): + '''HashiVault option group class for connection options''' + + OPTIONS = ['url', 'proxies', 'ca_cert', 'validate_certs', 'namespace', 'timeout', 'retries', 'retry_action'] + + ARGSPEC = dict( + url=dict(type='str', default=None), + proxies=dict(type='raw'), + ca_cert=dict(type='str', aliases=['cacert'], default=None), + validate_certs=dict(type='bool'), + namespace=dict(type='str', default=None), + timeout=dict(type='int'), + retries=dict(type='raw'), + retry_action=dict(type='str', choices=['ignore', 'warn'], default='warn'), + ) + + _LATE_BINDING_ENV_VAR_OPTIONS = { + 'url': dict(env=['VAULT_ADDR'], required=True), + 'ca_cert': dict(env=['VAULT_CACERT']), + 'namespace': dict(env=['VAULT_NAMESPACE']), + } + + _RETRIES_DEFAULT_PARAMS = { + 'status_forcelist': [ + # https://www.vaultproject.io/api#http-status-codes + # 429 is usually a "too many requests" status, but in Vault it's the default health status response for standby nodes. + 412, # Precondition failed. Returned on Enterprise when a request can't be processed yet due to some missing eventually consistent data. + # Should be retried, perhaps with a little backoff. + 500, # Internal server error. An internal error has occurred, try again later. If the error persists, report a bug. + 502, # A request to Vault required Vault making a request to a third party; the third party responded with an error of some kind. + 503, # Vault is down for maintenance or is currently sealed. Try again later. + ], + ( + # this field name changed in 1.26.0, and in the interest of supporting a wider range of urllib3 versions + # we'll use the new name whenever possible, but fall back seamlessly when needed. + # See also: + # - https://github.com/urllib3/urllib3/issues/2092 + # - https://github.com/urllib3/urllib3/blob/main/CHANGES.rst#1260-2020-11-10 + "allowed_methods" if HAS_RETRIES and hasattr(urllib3.util.Retry.DEFAULT, "allowed_methods") else "method_whitelist" + ): None, # None allows retries on all methods, including those which may not be considered idempotent, like POST + 'backoff_factor': 0.3, + } + + def __init__(self, option_adapter, retry_callback_generator=None): + super(HashiVaultConnectionOptions, self).__init__(option_adapter) + self._retry_callback_generator = retry_callback_generator + + def get_hvac_connection_options(self): + '''returns kwargs to be used for constructing an hvac.Client''' + + # validate_certs is only used to optionally change the value of ca_cert + def _filter(k, v): + return v is not None and k != 'validate_certs' + + # our transformed ca_cert value will become the verify parameter for the hvac client + hvopts = self._options.get_filtered_options(_filter, *self.OPTIONS) + hvopts['verify'] = hvopts.pop('ca_cert') + + retry_action = hvopts.pop('retry_action') + if 'retries' in hvopts: + hvopts['session'] = self._get_custom_requests_session(new_callback=self._retry_callback_generator(retry_action), **hvopts.pop('retries')) + + return hvopts + + def process_connection_options(self): + '''executes special processing required for certain options''' + self.process_late_binding_env_vars(self._LATE_BINDING_ENV_VAR_OPTIONS) + + self._boolean_or_cacert() + self._process_option_proxies() + self._process_option_retries() + + def _get_custom_requests_session(self, **retry_kwargs): + '''returns a requests.Session to pass to hvac (or None)''' + + if not HAS_RETRIES: + # because hvac requires requests which requires urllib3 it's unlikely we'll ever reach this condition. + raise NotImplementedError("Retries are unavailable. This may indicate very old versions of one or more of the following: hvac, requests, urllib3.") + + # This is defined here because Retry may not be defined if its import failed. + # As mentioned above, that's very unlikely, but it'll fail sanity tests nonetheless if defined with other classes. + class CallbackRetry(urllib3.util.Retry): + def __init__(self, *args, **kwargs): + self._newcb = kwargs.pop('new_callback') + super(CallbackRetry, self).__init__(*args, **kwargs) + + def new(self, **kwargs): + if self._newcb is not None: + self._newcb(self) + + kwargs['new_callback'] = self._newcb + return super(CallbackRetry, self).new(**kwargs) + + # We don't want the Retry class raising its own exceptions because that will prevent + # hvac from raising its own on various response codes. + # We set this here, rather than in the defaults, because if the caller sets their own + # dict for retries, we use it directly, but we don't want them to have to remember to always + # set raise_on_status=False themselves to get proper error handling. + # On the off chance someone does set it, we leave it alone, even though it's probably a mistake. + # That will be mentioned in the parameter docs. + if 'raise_on_status' not in retry_kwargs: + retry_kwargs['raise_on_status'] = False + # needs urllib 1.15+ https://github.com/urllib3/urllib3/blob/main/CHANGES.rst#115-2016-04-06 + # but we should always have newer ones via requests, via hvac + + retry = CallbackRetry(**retry_kwargs) + + adapter = HTTPAdapter(max_retries=retry) + sess = Session() + sess.mount("https://", adapter) + sess.mount("http://", adapter) + + return sess + + def _process_option_retries(self): + '''check if retries option is int or dict and interpret it appropriately''' + # this method focuses on validating the option, and setting a valid Retry object construction dict + # it intentionally does not build the Session object, which will be done elsewhere + + retries_opt = self._options.get_option('retries') + + if retries_opt is None: + return + + # we'll start with a copy of our defaults + retries = self._RETRIES_DEFAULT_PARAMS.copy() + + try: + # try int + # on int, retry the specified number of times, and use the defaults for everything else + # on zero, disable retries + retries_int = check_type_int(retries_opt) + + if retries_int < 0: + raise ValueError("Number of retries must be >= 0 (got %i)" % retries_int) + elif retries_int == 0: + retries = None + else: + retries['total'] = retries_int + + except TypeError: + try: + # try dict + # on dict, use the value directly (will be used as the kwargs to initialize the Retry instance) + retries = check_type_dict(retries_opt) + except TypeError: + raise TypeError("retries option must be interpretable as int or dict. Got: %r" % retries_opt) + + self._options.set_option('retries', retries) + + def _process_option_proxies(self): + '''check if 'proxies' option is dict or str and set it appropriately''' + + proxies_opt = self._options.get_option('proxies') + + if proxies_opt is None: + return + + try: + # if it can be interpreted as dict + # do it + proxies = check_type_dict(proxies_opt) + except TypeError: + # if it can't be interpreted as dict + proxy = check_type_str(proxies_opt) + # but can be interpreted as str + # use this str as http and https proxy + proxies = { + 'http': proxy, + 'https': proxy, + } + + # record the new/interpreted value for 'proxies' option + self._options.set_option('proxies', proxies) + + def _boolean_or_cacert(self): + # This is needed because of this (https://hvac.readthedocs.io/en/stable/source/hvac_v1.html): + # + # # verify (Union[bool,str]) - Either a boolean to indicate whether TLS verification should + # # be performed when sending requests to Vault, or a string pointing at the CA bundle to use for verification. + # + '''return a bool or cacert''' + ca_cert = self._options.get_option('ca_cert') + + validate_certs = self._options.get_option('validate_certs') + + if validate_certs is None: + # Validate certs option was not explicitly set + + # Check if VAULT_SKIP_VERIFY is set + vault_skip_verify = os.environ.get('VAULT_SKIP_VERIFY') + + if vault_skip_verify is not None: + # VAULT_SKIP_VERIFY is set + try: + # Check that we have a boolean value + vault_skip_verify = check_type_bool(vault_skip_verify) + except TypeError: + # Not a boolean value fallback to default value (True) + validate_certs = True + else: + # Use the inverse of VAULT_SKIP_VERIFY + validate_certs = not vault_skip_verify + else: + validate_certs = True + + if not (validate_certs and ca_cert): + self._options.set_option('ca_cert', validate_certs) + else: + self._options.set_option('ca_cert', to_text(ca_cert, errors='surrogate_or_strict')) diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_common.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_common.py new file mode 100644 index 00000000..b39431c0 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_common.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +'''Python versions supported: >=3.6''' + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os + + +HAS_HVAC = False +try: + import hvac + HAS_HVAC = True +except ImportError: + HAS_HVAC = False + + +def _stringify(input): + ''' + This method is primarily used to Un-Unsafe values that come from Ansible. + We want to remove the Unsafe context so that libraries don't get confused + by the values. + ''' + + # Since this is a module_util, and will be used by both plugins and modules, + # we cannot import the AnsibleUnsafe* types, because they are controller-only. + # However, they subclass the native types, so we can check for that. + + # bytes is the only consistent type to check against in both py2 and py3 + if isinstance(input, bytes): + # seems redundant, but this will give us a regular bytes object even + # when the input is AnsibleUnsafeBytes + return bytes(input) + else: + # instead of checking for py2 vs. py3 to cast to str or unicode, + # let's get the type from the literal. + return type(u'')(input) + + +class HashiVaultValueError(ValueError): + '''Use in common code to raise an Exception that can be turned into AnsibleError or used to fail_json()''' + + +class HashiVaultHelper(): + + STRINGIFY_CANDIDATES = set([ + 'token', # Token will end up in a header, requests requires headers to be str or bytes, + # and newer versions of requests stopped converting automatically. Because our + # token could have been passed in from a previous lookup call, it could be one + # of the AnsibleUnsafe types instead, causing a failure. Tokens should always + # be strings, so we will convert them. + 'namespace', # namespace is also set in a header + ]) + + def __init__(self): + # TODO move hvac checking here? + pass + + @staticmethod + def _stringify(input): + return _stringify(input) + + def get_vault_client( + self, + hashi_vault_logout_inferred_token=True, hashi_vault_revoke_on_logout=False, hashi_vault_stringify_args=True, + **kwargs + ): + ''' + creates a Vault client with the given kwargs + + :param hashi_vault_logout_inferred_token: if True performs "logout" after creation to remove any token that + the hvac library itself may have read in. Only used if "token" is not included in kwargs. + :type hashi_vault_logout_implied_token: bool + + :param hashi_vault_revoke_on_logout: if True revokes any current token on logout. Only used if a logout is performed. Not recommended. + :type hashi_vault_revoke_on_logout: bool + + :param hashi_vault_stringify_args: if True converts a specific set of defined kwargs to a string type. + :type hashi_vault_stringify_args: bool + ''' + + if hashi_vault_stringify_args: + for key in kwargs.keys(): + if key in self.STRINGIFY_CANDIDATES: + kwargs[key] = self._stringify(kwargs[key]) + + client = hvac.Client(**kwargs) + + # logout to prevent accidental use of inferred tokens + # https://github.com/ansible-collections/community.hashi_vault/issues/13 + if hashi_vault_logout_inferred_token and 'token' not in kwargs: + client.logout(revoke_token=hashi_vault_revoke_on_logout) + + return client + + +class HashiVaultOptionAdapter(object): + ''' + The purpose of this class is to provide a standard interface for option getting/setting + within module_utils code, since the processes are so different in plugins and modules. + + Attention is paid to ensuring that in plugins we use the methods provided by Config Manager, + but to allow flexibility to create an adapter to work with other sources, hence the design + of defining specific methods exposed, and having them call provided function references. + ''' + # More context on the need to call Config Manager methods: + # + # Some issues raised around deprecations in plugins not being processed led to comments + # from core maintainers around the need to use Config Manager and also to ensure any + # option needed is always retrieved using AnsiblePlugin.get_option(). At the time of this + # writing, based on the way Config Manager is implemented, that's not actually necessary, + # and calling AnsiblePlugin.set_options() to initialize them is enough. But that's not + # guaranteed to stay that way, if get_option() is used to "trigger" internal events. + # + # More reading: + # - https://github.com/ansible-collections/community.hashi_vault/issues/35 + # - https://github.com/ansible/ansible/issues/73051 + # - https://github.com/ansible/ansible/pull/73058 + # - https://github.com/ansible/ansible/pull/73239 + # - https://github.com/ansible/ansible/pull/73240 + + @classmethod + def from_dict(cls, dict): + return cls( + getter=dict.__getitem__, + setter=dict.__setitem__, + haver=lambda key: key in dict, + updater=dict.update, + defaultsetter=dict.setdefault, + defaultgetter=dict.get, + ) + + @classmethod + def from_ansible_plugin(cls, plugin): + return cls( + getter=plugin.get_option, + setter=plugin.set_option, + haver=plugin.has_option if hasattr(plugin, 'has_option') else None, + # AnsiblePlugin.has_option was added in 2.10, see https://github.com/ansible/ansible/pull/61078 + ) + + @classmethod + def from_ansible_module(cls, module): + return cls.from_dict(module.params) + + def __init__( + self, + getter, setter, + haver=None, updater=None, getitems=None, getfiltereditems=None, getfilleditems=None, defaultsetter=None, defaultgetter=None): + + def _default_default_setter(key, default=None): + try: + value = self.get_option(key) + return value + except KeyError: + self.set_option(key, default) + return default + + def _default_updater(**kwargs): + for key, value in kwargs.items(): + self.set_option(key, value) + + def _default_haver(key): + try: + self.get_option(key) + return True + except KeyError: + return False + + def _default_getitems(*args): + return dict((key, self.get_option(key)) for key in args) + + def _default_getfiltereditems(filter, *args): + return dict((key, value) for key, value in self.get_options(*args).items() if filter(key, value)) + + def _default_getfilleditems(*args): + return self.get_filtered_options(lambda k, v: v is not None, *args) + + def _default_default_getter(key, default): + try: + return self.get_option(key) + except KeyError: + return default + + self._getter = getter + self._setter = setter + + self._haver = haver or _default_haver + self._updater = updater or _default_updater + self._getitems = getitems or _default_getitems + self._getfiltereditems = getfiltereditems or _default_getfiltereditems + self._getfilleditems = getfilleditems or _default_getfilleditems + self._defaultsetter = defaultsetter or _default_default_setter + self._defaultgetter = defaultgetter or _default_default_getter + + def get_option(self, key): + return self._getter(key) + + def get_option_default(self, key, default=None): + return self._defaultgetter(key, default) + + def set_option(self, key, value): + return self._setter(key, value) + + def set_option_default(self, key, default=None): + return self._defaultsetter(key, default) + + def has_option(self, key): + return self._haver(key) + + def set_options(self, **kwargs): + return self._updater(**kwargs) + + def get_options(self, *args): + return self._getitems(*args) + + def get_filtered_options(self, filter, *args): + return self._getfiltereditems(filter, *args) + + def get_filled_options(self, *args): + return self._getfilleditems(*args) + + +class HashiVaultOptionGroupBase: + '''A base class for class option group classes''' + + def __init__(self, option_adapter): + self._options = option_adapter + + def process_late_binding_env_vars(self, option_vars): + '''looks through a set of options, and if empty/None, looks for a value in specified env vars, or sets an optional default''' + # see https://github.com/ansible-collections/community.hashi_vault/issues/10 + # + # Options which seek to use environment vars that are not Ansible-specific + # should load those as values of last resort, so that INI values can override them. + # For default processing, list such options and vars here. + # Alternatively, process them in another appropriate place like an auth method's + # validate_ method. + # + # key = option_name + # value = dict with "env" key which is a list of env vars (in order of those checked first; process stops when value is found), + # and an optional "default" key whose value will be set if none of the env vars are found. + # An optional boolean "required" key can be used to specify that a value is required, so raise if one is not found. + + for opt, config in option_vars.items(): + for env in config['env']: + # we use has_option + get_option rather than get_option_default + # because we will only override if the option exists and + # is None, not if it's missing. For plugins, that is the usual, + # but for modules, they may have to set the default to None + # in the argspec if it has late binding env vars. + if self._options.has_option(opt) and self._options.get_option(opt) is None: + self._options.set_option(opt, os.environ.get(env)) + + if 'default' in config and self._options.has_option(opt) and self._options.get_option(opt) is None: + self._options.set_option(opt, config['default']) + + if 'required' in config and self._options.get_option_default(opt) is None: + raise HashiVaultValueError("Required option %s was not set." % opt) + + +class HashiVaultAuthMethodBase(HashiVaultOptionGroupBase): + '''Base class for individual auth method implementations''' + + def __init__(self, option_adapter, warning_callback, deprecate_callback): + super(HashiVaultAuthMethodBase, self).__init__(option_adapter) + self._warner = warning_callback + self._deprecator = deprecate_callback + + def validate(self): + '''Validates the given auth method as much as possible without calling Vault.''' + raise NotImplementedError('validate must be implemented') + + def authenticate(self, client, use_token=True): + '''Authenticates against Vault, returns a token.''' + raise NotImplementedError('authenticate must be implemented') + + def validate_by_required_fields(self, *field_names): + missing = [field for field in field_names if self._options.get_option_default(field) is None] + + if missing: + raise HashiVaultValueError("Authentication method %s requires options %r to be set, but these are missing: %r" % (self.NAME, field_names, missing)) + + def warn(self, message): + self._warner(message) + + def deprecate(self, message, version=None, date=None, collection_name=None): + self._deprecator(message, version=version, date=date, collection_name=collection_name) + + @staticmethod + def _stringify(input): + return _stringify(input) diff --git a/ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_module.py b/ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_module.py new file mode 100644 index 00000000..5d2dfafc --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/module_utils/_hashi_vault_module.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import ( + HashiVaultHelper, + HashiVaultOptionAdapter, +) +from ansible_collections.community.hashi_vault.plugins.module_utils._connection_options import HashiVaultConnectionOptions +from ansible_collections.community.hashi_vault.plugins.module_utils._authenticator import HashiVaultAuthenticator + + +class HashiVaultModule(AnsibleModule): + def __init__(self, *args, **kwargs): + if 'hashi_vault_custom_retry_callback' in kwargs: + callback = kwargs.pop('hashi_vault_custom_retry_callback') + else: + callback = self._generate_retry_callback + + super(HashiVaultModule, self).__init__(*args, **kwargs) + + self.helper = HashiVaultHelper() + self.adapter = HashiVaultOptionAdapter.from_dict(self.params) + self.connection_options = HashiVaultConnectionOptions(option_adapter=self.adapter, retry_callback_generator=callback) + self.authenticator = HashiVaultAuthenticator(option_adapter=self.adapter, warning_callback=self.warn, deprecate_callback=self.deprecate) + + @classmethod + def generate_argspec(cls, **kwargs): + spec = HashiVaultConnectionOptions.ARGSPEC.copy() + spec.update(HashiVaultAuthenticator.ARGSPEC.copy()) + spec.update(**kwargs) + + return spec + + def _generate_retry_callback(self, retry_action): + '''returns a Retry callback function for modules''' + def _on_retry(retry_obj): + if retry_obj.total > 0: + if retry_action == 'warn': + self.warn('community.hashi_vault: %i %s remaining.' % (retry_obj.total, 'retry' if retry_obj.total == 1 else 'retries')) + else: + pass + + return _on_retry 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 00000000..e21f4a81 --- /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 + 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 00000000..ac4d5925 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_delete.py @@ -0,0 +1,179 @@ +#!/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 + - 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 00000000..5ff5903f --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/modules/vault_kv2_get.py @@ -0,0 +1,212 @@ +#!/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 + description: The official documentation for the C(community.hashi_vault.vault_kv2_get) lookup plugin. + - module: community.hashi_vault.vault_kv1_get + - name: KV2 Secrets Engine + description: Documentation for the Vault KV secrets engine, version 2. + link: https://www.vaultproject.io/docs/secrets/kv/kv-v2 +extends_documentation_fragment: + - community.hashi_vault.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_list.py b/ansible_collections/community/hashi_vault/plugins/modules/vault_list.py new file mode 100644 index 00000000..a0823dc2 --- /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 + 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 00000000..c52e969e --- /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 + description: The official documentation for the C(community.hashi_vault.vault_login) lookup plugin. + - ref: 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 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 + +# 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 +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 00000000..66b9190b --- /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(;:) 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 00000000..6b6b209d --- /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 + description: The official documentation for the C(community.hashi_vault.vault_read) lookup plugin. + - ref: 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 00000000..c2d19422 --- /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 + 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 + description: The official documentation for the C(community.hashi_vault.vault_login) lookup plugin. + - ref: 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 00000000..35c7fcb6 --- /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 + 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 + 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() diff --git a/ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_lookup_base.py b/ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_lookup_base.py new file mode 100644 index 00000000..5feb6fcf --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_lookup_base.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright (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 + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible.utils.display import Display + +from ansible_collections.community.hashi_vault.plugins.plugin_utils._hashi_vault_plugin import HashiVaultPlugin + +display = Display() + + +class HashiVaultLookupBase(HashiVaultPlugin, LookupBase): + + def __init__(self, loader=None, templar=None, **kwargs): + HashiVaultPlugin.__init__(self) + LookupBase.__init__(self, loader=loader, templar=templar, **kwargs) + + def parse_kev_term(self, term, plugin_name, first_unqualified=None): + '''parses a term string into a dictionary''' + param_dict = {} + + for i, param in enumerate(term.split()): + try: + key, value = param.split('=', 1) + except ValueError: + if i == 0 and first_unqualified is not None: + # allow first item to be specified as value only and assign to assumed option name + key = first_unqualified + value = param + else: + raise AnsibleError("%s lookup plugin needs key=value pairs, but received %s" % (plugin_name, term)) + + param_dict[key] = value + + return param_dict diff --git a/ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_plugin.py b/ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_plugin.py new file mode 100644 index 00000000..65c17c27 --- /dev/null +++ b/ansible_collections/community/hashi_vault/plugins/plugin_utils/_hashi_vault_plugin.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Copyright (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 + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within the community.hashi_vault collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.plugins import AnsiblePlugin +from ansible import constants as C +from ansible.utils.display import Display + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import ( + HashiVaultHelper, + HashiVaultOptionAdapter, +) + +from ansible_collections.community.hashi_vault.plugins.module_utils._connection_options import HashiVaultConnectionOptions +from ansible_collections.community.hashi_vault.plugins.module_utils._authenticator import HashiVaultAuthenticator + + +display = Display() + + +class HashiVaultPlugin(AnsiblePlugin): + def __init__(self): + super(HashiVaultPlugin, self).__init__() + + self.helper = HashiVaultHelper() + self._options_adapter = HashiVaultOptionAdapter.from_ansible_plugin(self) + self.connection_options = HashiVaultConnectionOptions(self._options_adapter, self._generate_retry_callback) + self.authenticator = HashiVaultAuthenticator(self._options_adapter, display.warning, display.deprecated) + + def _generate_retry_callback(self, retry_action): + '''returns a Retry callback function for plugins''' + def _on_retry(retry_obj): + if retry_obj.total > 0: + if retry_action == 'warn': + display.warning('community.hashi_vault: %i %s remaining.' % (retry_obj.total, 'retry' if retry_obj.total == 1 else 'retries')) + else: + pass + + return _on_retry + + def process_deprecations(self, collection_name='community.hashi_vault'): + '''processes deprecations related to the collection''' + + # TODO: this is a workaround for deprecations not being shown in lookups + # See: + # - https://github.com/ansible/ansible/issues/73051 + # - https://github.com/ansible/ansible/pull/73058 + # - https://github.com/ansible/ansible/pull/73239 + # - https://github.com/ansible/ansible/pull/73240 + # + # If a fix is backported to 2.9, this should be removed. + # Otherwise, we'll have to test with fixes that are available and see how we + # can determine whether to execute this conditionally. + + # nicked from cli/__init__.py + # with slight customizations to help filter out relevant messages + # (relying on the collection name since it's a valid attrib and we only have 1 plugin at this time) + + # warn about deprecated config options + + for deprecated in list(C.config.DEPRECATED): + name = deprecated[0] + why = deprecated[1]['why'] + if deprecated[1].get('collection_name') != collection_name: + continue + + if 'alternatives' in deprecated[1]: + alt = ', use %s instead' % deprecated[1]['alternatives'] + else: + alt = '' + ver = deprecated[1].get('version') + date = deprecated[1].get('date') + collection_name = deprecated[1].get('collection_name') + display.deprecated("%s option, %s%s" % (name, why, alt), version=ver, date=date, collection_name=collection_name) + + # remove this item from the list so it won't get processed again by something else + C.config.DEPRECATED.remove(deprecated) -- cgit v1.2.3