From 39acd69690a392682ade3c8b36edc396c6f6cc24 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Fri, 13 Jan 2023 12:32:26 +0000 Subject: [PATCH 01/22] read -> list --- plugins/lookup/vault_list.py | 137 ++++++++++++++++++++++++++++++++++ plugins/modules/vault_list.py | 135 +++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 plugins/lookup/vault_list.py create mode 100644 plugins/modules/vault_list.py diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py new file mode 100644 index 000000000..2bb892057 --- /dev/null +++ b/plugins/lookup/vault_list.py @@ -0,0 +1,137 @@ +# (c) 2021, 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 + - 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 listed. + 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.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/plugins/modules/vault_list.py b/plugins/modules/vault_list.py new file mode 100644 index 000000000..9558eb369 --- /dev/null +++ b/plugins/modules/vault_list.py @@ -0,0 +1,135 @@ +#!/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. + - 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 listed. + 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 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() From 41bf98d75d1374ad5f2207e871fecfdf35e95dec Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Fri, 13 Jan 2023 17:03:05 +0000 Subject: [PATCH 02/22] lookup and module for vault_list - initial tests --- plugins/modules/vault_list.py | 18 ++-- .../targets/lookup_vault_list/aliases | 1 + .../targets/lookup_vault_list/meta/main.yml | 4 + .../tasks/lookup_vault_list_setup.yml | 9 ++ .../tasks/lookup_vault_list_test.yml | 98 +++++++++++++++++++ .../targets/lookup_vault_list/tasks/main.yml | 3 + .../targets/module_vault_list/aliases | 1 + .../targets/module_vault_list/meta/main.yml | 4 + .../targets/module_vault_list/tasks/main.yml | 3 + .../tasks/module_vault_list_setup.yml | 9 ++ .../tasks/module_vault_list_test.yml | 62 ++++++++++++ .../setup_vault_configure/tasks/configure.yml | 16 +++ .../setup_vault_configure/vars/main.yml | 16 +++ 13 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 tests/integration/targets/lookup_vault_list/aliases create mode 100644 tests/integration/targets/lookup_vault_list/meta/main.yml create mode 100644 tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_setup.yml create mode 100644 tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml create mode 100644 tests/integration/targets/lookup_vault_list/tasks/main.yml create mode 100644 tests/integration/targets/module_vault_list/aliases create mode 100644 tests/integration/targets/module_vault_list/meta/main.yml create mode 100644 tests/integration/targets/module_vault_list/tasks/main.yml create mode 100644 tests/integration/targets/module_vault_list/tasks/module_vault_list_setup.yml create mode 100644 tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml diff --git a/plugins/modules/vault_list.py b/plugins/modules/vault_list.py index 9558eb369..e7332b474 100644 --- a/plugins/modules/vault_list.py +++ b/plugins/modules/vault_list.py @@ -39,10 +39,10 @@ """ EXAMPLES = """ -- name: Read a kv2 secret from Vault via the remote host with userpass auth - community.hashi_vault.vault_read: +- name: List kv2 secrets from Vault via the remote host with userpass auth + community.hashi_vault.vault_list: url: https://vault:8201 - path: secret/data/hello + path: secret auth_method: userpass username: user password: '{{ passwd }}' @@ -50,17 +50,17 @@ - name: Display the secret data ansible.builtin.debug: - msg: "{{ secret.data.data.data }}" + msg: "{{ secret.data }}" -- name: Retrieve an approle role ID from Vault via the remote host - community.hashi_vault.vault_read: +- name: List role IDs from Vault via the remote host + community.hashi_vault.vault_list: url: https://vault:8201 - path: auth/approle/role/role-name/role-id + path: auth/approle register: approle_id -- name: Display the role ID +- name: Display the role IDs ansible.builtin.debug: - msg: "{{ approle_id.data.data.role_id }}" + msg: "{{ approle_id.data }}" """ RETURN = """ diff --git a/tests/integration/targets/lookup_vault_list/aliases b/tests/integration/targets/lookup_vault_list/aliases new file mode 100644 index 000000000..1bb8bf6d7 --- /dev/null +++ b/tests/integration/targets/lookup_vault_list/aliases @@ -0,0 +1 @@ +# empty diff --git a/tests/integration/targets/lookup_vault_list/meta/main.yml b/tests/integration/targets/lookup_vault_list/meta/main.yml new file mode 100644 index 000000000..d3acb69e9 --- /dev/null +++ b/tests/integration/targets/lookup_vault_list/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_vault_test_plugins + - setup_vault_configure diff --git a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_setup.yml b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_setup.yml new file mode 100644 index 000000000..193d6fa5e --- /dev/null +++ b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_setup.yml @@ -0,0 +1,9 @@ +--- +- name: Configuration tasks + module_defaults: + vault_ci_token_create: '{{ vault_plugins_module_defaults_common }}' + block: + - name: 'Create a test non-root token' + vault_ci_token_create: + policies: test-policy + register: user_token_cmd diff --git a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml new file mode 100644 index 000000000..7efcaf3a0 --- /dev/null +++ b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml @@ -0,0 +1,98 @@ +--- +- name: Var block + vars: + ansible_hashi_vault_token_validate: true + user_token: '{{ user_token_cmd.result.auth.client_token }}' + kwargs: + url: '{{ vault_test_server_http }}' + auth_method: token + token: '{{ user_token }}' + block: + - name: 'Check kv2 secret list' + vars: + kv2_secret2: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_path, **kwargs) }}" + assert: + that: + - "'data' in kv2_secret2" + - "'keys' in kv2_secret2['data']" + fail_msg: 'Return value did not contain expected fields.' + + - name: "Check multiple path list as array" + vars: + paths: + - '{{ vault_kv2_api_list_path }}' + - '{{ vault_policy_api_list_path }}' + list_results: "{{ lookup('community.hashi_vault.vault_list', *paths, **kwargs) }}" + assert: + that: + - list_results | type_debug == 'list' + - item | type_debug == 'dict' + - "'data' in item" + - "'keys' in item['data']" + # - "'value' in item['data']['keys']" + fail_msg: 'Return value was not correct type or items do not match.' + loop: '{{ list_results }}' + + + ### failure tests + + - name: 'Failure expected when erroneous credentials are used' + vars: + secret_wrong_cred: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_path, token='wrong_token', url=kwargs.url) }}" + debug: + msg: 'Failure is expected ({{ secret_wrong_cred }})' + register: test_wrong_cred + ignore_errors: true + + - assert: + that: + - test_wrong_cred is failed + - test_wrong_cred.msg is search('Invalid Vault Token') + fail_msg: "Expected failure but got success or wrong failure message." + + - name: 'Failure expected when unauthorized path is provided' + vars: + secret_unauthorized: "{{ lookup('community.hashi_vault.vault_list', unauthorized_vault_kv2_mount_point, **kwargs) }}" + debug: + msg: 'Failure is expected ({{ secret_unauthorized }})' + register: test_unauthorized + ignore_errors: true + + - assert: + that: + - test_unauthorized is failed + - test_unauthorized.msg is search('Permission Denied') + fail_msg: "Expected failure but got success or wrong failure message." + + - name: 'Failure expected when inexistent path is listed' + vars: + path_inexistent: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_inexistent_path, **kwargs) }}" + debug: + msg: 'Failure is expected ({{ path_inexistent }})' + register: test_inexistent + ignore_errors: true + + - assert: + that: + - test_inexistent is failed + - test_inexistent.msg is search("Permission Denied") # This is the error received when listing a non-existent path + fail_msg: "Expected failure but got success or wrong failure message." + + # do this last so our set_fact doesn't affect any other tests + - name: Set the vars that will configure the lookup settings we can't set via with_ + set_fact: + ansible_hashi_vault_url: '{{ kwargs.url }}' + ansible_hashi_vault_token: '{{ kwargs.token }}' + ansible_hashi_vault_auth_method: '{{ kwargs.auth_method }}' + + - name: Check multiple path list via with_ + assert: + that: + - item | type_debug == 'dict' + - "'data' in item" + - "'keys' in item['data']" + # - "'value' in item['data']['data']" + fail_msg: 'Return value was not correct type or items do not match.' + with_community.hashi_vault.vault_list: + - '{{ vault_kv2_api_list_path }}' + - '{{ vault_policy_api_list_path }}' diff --git a/tests/integration/targets/lookup_vault_list/tasks/main.yml b/tests/integration/targets/lookup_vault_list/tasks/main.yml new file mode 100644 index 000000000..e0caae6bd --- /dev/null +++ b/tests/integration/targets/lookup_vault_list/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- import_tasks: lookup_vault_list_setup.yml +- import_tasks: lookup_vault_list_test.yml diff --git a/tests/integration/targets/module_vault_list/aliases b/tests/integration/targets/module_vault_list/aliases new file mode 100644 index 000000000..7636a9a65 --- /dev/null +++ b/tests/integration/targets/module_vault_list/aliases @@ -0,0 +1 @@ +context/target diff --git a/tests/integration/targets/module_vault_list/meta/main.yml b/tests/integration/targets/module_vault_list/meta/main.yml new file mode 100644 index 000000000..d3acb69e9 --- /dev/null +++ b/tests/integration/targets/module_vault_list/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_vault_test_plugins + - setup_vault_configure diff --git a/tests/integration/targets/module_vault_list/tasks/main.yml b/tests/integration/targets/module_vault_list/tasks/main.yml new file mode 100644 index 000000000..cd7bd5d5d --- /dev/null +++ b/tests/integration/targets/module_vault_list/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- import_tasks: module_vault_list_setup.yml +- import_tasks: module_vault_list_test.yml diff --git a/tests/integration/targets/module_vault_list/tasks/module_vault_list_setup.yml b/tests/integration/targets/module_vault_list/tasks/module_vault_list_setup.yml new file mode 100644 index 000000000..193d6fa5e --- /dev/null +++ b/tests/integration/targets/module_vault_list/tasks/module_vault_list_setup.yml @@ -0,0 +1,9 @@ +--- +- name: Configuration tasks + module_defaults: + vault_ci_token_create: '{{ vault_plugins_module_defaults_common }}' + block: + - name: 'Create a test non-root token' + vault_ci_token_create: + policies: test-policy + register: user_token_cmd diff --git a/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml new file mode 100644 index 000000000..0cc7e276b --- /dev/null +++ b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml @@ -0,0 +1,62 @@ +--- +- name: Var block + vars: + user_token: '{{ user_token_cmd.result.auth.client_token }}' + module_defaults: + community.hashi_vault.vault_list: + url: '{{ vault_test_server_http }}' + auth_method: token + token: '{{ user_token }}' + token_validate: true + timeout: 5 + block: + - name: 'Check kv2 secret list' + register: kv2_path + community.hashi_vault.vault_list: + path: "{{ vault_kv2_api_list_path }}" + + - assert: + that: + - "'data' in kv2_path" + - "'data' in kv2_path['data']" + - "'keys' in kv2_path['data']['data']" + fail_msg: 'Return value did not contain expected fields.' + + ### failure tests + + - name: 'Failure expected when erroneous credentials are used' + register: test_wrong_cred + community.hashi_vault.vault_list: + path: "{{ vault_kv2_api_list_path }}" + token: wrong_token + ignore_errors: true + + - assert: + that: + - test_wrong_cred is failed + - test_wrong_cred.msg is search('Invalid Vault Token') + fail_msg: "Expected failure but got success or wrong failure message." + + - name: 'Failure expected when unauthorized path is listed' + register: test_unauthorized + community.hashi_vault.vault_list: + path: "{{ unauthorized_vault_kv2_mount_point }}" + ignore_errors: true + + - assert: + that: + - test_unauthorized is failed + - test_unauthorized.msg is search('Permission Denied') + fail_msg: "Expected failure but got success or wrong failure message." + + - name: 'Failure expected when inexistent path is listed' + register: test_inexistent + community.hashi_vault.vault_list: + path: "{{ vault_kv2_api_list_inexistent_path }}" + ignore_errors: true + + - assert: + that: + - test_inexistent is failed + - test_inexistent.msg is search("Permission Denied") # This is the error received when listing a non-existent path + fail_msg: "Expected failure but got success or wrong failure message." diff --git a/tests/integration/targets/setup_vault_configure/tasks/configure.yml b/tests/integration/targets/setup_vault_configure/tasks/configure.yml index 3d7bc5f61..5fdafe937 100644 --- a/tests/integration/targets/setup_vault_configure/tasks/configure.yml +++ b/tests/integration/targets/setup_vault_configure/tasks/configure.yml @@ -13,6 +13,13 @@ options: version: 2 +- name: 'Create KV v2 secrets engine to test unauthorized access' + vault_ci_enable_engine: + backend_type: kv + path: '{{ unauthorized_vault_kv2_mount_point }}' + options: + version: 2 + - name: Create a test policy vault_ci_policy_put: name: test-policy @@ -56,6 +63,15 @@ secret: value: 'foo{{ item }}' +- name: 'Create KV v2 secrets in unauthorized path' + loop: [1, 2, 3, 4, 5] + vault_ci_kv_put: + path: "{{ vault_kv2_path }}/secret{{ item }}" + version: 2 + mount_point: '{{ unauthorized_vault_kv2_mount_point }}' + secret: + value: 'foo{{ item }}' + - name: 'Update KV v2 secret4 with new value to create version' vault_ci_kv_put: path: "{{ vault_kv2_path }}/secret4" diff --git a/tests/integration/targets/setup_vault_configure/vars/main.yml b/tests/integration/targets/setup_vault_configure/vars/main.yml index 2d76ea951..324fcbd0b 100644 --- a/tests/integration/targets/setup_vault_configure/vars/main.yml +++ b/tests/integration/targets/setup_vault_configure/vars/main.yml @@ -8,6 +8,7 @@ vault_kv1_path: testproject vault_kv1_api_path: '{{ vault_kv1_mount_point }}/{{ vault_kv1_path }}' vault_kv2_mount_point: kv2 +unauthorized_vault_kv2_mount_point: kv2_noauth vault_kv2_path: testproject vault_kv2_multi_path: testmulti @@ -18,6 +19,12 @@ vault_kv2_multi_api_path: '{{ vault_kv2_mount_point }}/data/{{ vault_kv2_multi_p vault_kv2_versioned_api_path: '{{ vault_kv2_mount_point }}/data/{{ vault_kv2_versioned_path }}' vault_kv2_delete_api_path: '{{ vault_kv2_mount_point }}/delete/{{ vault_kv2_versioned_path }}' vault_kv2_metadata_api_path: '{{ vault_kv2_mount_point }}/metadata/{{ vault_kv2_versioned_path }}' +vault_kv2_api_list_path: '{{ vault_kv2_mount_point }}/metadata' + +vault_policy_api_list_path: 'sys/policies/acl' + +vault_kv2_api_list_inexistent_path: 'non_existent_{{ vault_kv2_mount_point }}/metadata' +vault_kv2_api_list_unauthorized_path: '{{ unauthorized_vault_kv2_mount_point }}/metadata' vault_base_policy: | path "{{ vault_kv1_api_path }}/secret1" { @@ -69,6 +76,15 @@ vault_base_policy: | path "{{ vault_kv2_metadata_api_path }}/secret6" { capabilities = ["read"] } + path "{{ vault_kv2_api_list_path }}" { + capabilities = ["list"] + } + path "{{ vault_policy_api_list_path }}" { + capabilities = ["list"] + } + path "{{ vault_kv2_api_list_unauthorized_path }}" { + capabilities = ["deny"] + } vault_token_creator_policy: | path "auth/token/create" { From 3831878060974afb3a3f87b521b9ba0ba05f00e9 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Fri, 13 Jan 2023 17:32:58 +0000 Subject: [PATCH 03/22] unit tests for list lookup/module --- tests/unit/plugins/lookup/test_vault_list.py | 87 ++++++++++ tests/unit/plugins/modules/test_vault_list.py | 157 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 tests/unit/plugins/lookup/test_vault_list.py create mode 100644 tests/unit/plugins/modules/test_vault_list.py diff --git a/tests/unit/plugins/lookup/test_vault_list.py b/tests/unit/plugins/lookup/test_vault_list.py new file mode 100644 index 000000000..ae99f44e1 --- /dev/null +++ b/tests/unit/plugins/lookup/test_vault_list.py @@ -0,0 +1,87 @@ +# -*- 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 + +import pytest + +from ansible.plugins.loader import lookup_loader +from ansible.errors import AnsibleError + +from ...compat import mock + +from .....plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + +from .....plugins.lookup import vault_list + + +hvac = pytest.importorskip('hvac') + + +pytestmark = pytest.mark.usefixtures( + 'patch_authenticator', + 'patch_get_vault_client', +) + + +@pytest.fixture +def vault_list_lookup(): + return lookup_loader.get('community.hashi_vault.vault_list') + + +@pytest.fixture +def kv1_get_response(fixture_loader): + return fixture_loader('kv1_get_response.json') + + +class TestVaultListLookup(object): + + def test_vault_list_is_lookup_base(self, vault_list_lookup): + assert issubclass(type(vault_list_lookup), HashiVaultLookupBase) + + def test_vault_list_no_hvac(self, vault_list_lookup, minimal_vars): + with mock.patch.object(vault_list, 'HVAC_IMPORT_ERROR', new=ImportError()): + with pytest.raises(AnsibleError, match=r"This plugin requires the 'hvac' Python library"): + vault_list_lookup.run(terms='fake', variables=minimal_vars) + + @pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')]) + def test_vault_list_authentication_error(self, vault_list_lookup, minimal_vars, authenticator, exc): + authenticator.authenticate.side_effect = exc + + with pytest.raises(AnsibleError, match=r'throwaway msg'): + vault_list_lookup.run(terms='fake', variables=minimal_vars) + + @pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')]) + def test_vault_list_auth_validation_error(self, vault_list_lookup, minimal_vars, authenticator, exc): + authenticator.validate.side_effect = exc + + with pytest.raises(AnsibleError, match=r'throwaway msg'): + vault_list_lookup.run(terms='fake', variables=minimal_vars) + + @pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']]) + def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, kv1_get_response, vault_client, paths): + client = vault_client + + expected_calls = [mock.call(p) for p in paths] + + def _fake_kv1_get(path): + r = kv1_get_response.copy() + r.update({'_path': path}) + return r + + client.list = mock.Mock(wraps=_fake_kv1_get) + + response = vault_list_lookup.run(terms=paths, variables=minimal_vars) + + client.list.assert_has_calls(expected_calls) + + assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response)) + + for p in paths: + r = response.pop(0) + ins_p = r.pop('_path') + assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p) diff --git a/tests/unit/plugins/modules/test_vault_list.py b/tests/unit/plugins/modules/test_vault_list.py new file mode 100644 index 000000000..ab9e3f397 --- /dev/null +++ b/tests/unit/plugins/modules/test_vault_list.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Copyright (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 + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_list +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip('hvac') + + +pytestmark = pytest.mark.usefixtures( + 'patch_ansible_module', + 'patch_authenticator', + 'patch_get_vault_client', +) + + +def _connection_options(): + return { + 'auth_method': 'token', + 'url': 'http://myvault', + 'token': 'beep-boop', + } + + +def _sample_options(): + return { + 'path': 'endpoint', + } + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def kv1_get_response(fixture_loader): + return fixture_loader('kv1_get_response.json') + + +class TestModuleVaultList(): + + @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) + @pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')]) + def test_vault_list_authentication_error(self, authenticator, exc, capfd): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result['msg'] == 'throwaway msg', "result: %r" % result + + @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) + @pytest.mark.parametrize('exc', [HashiVaultValueError('throwaway msg'), NotImplementedError('throwaway msg')]) + def test_vault_list_auth_validation_error(self, authenticator, exc, capfd): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result['msg'] == 'throwaway msg' + + @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) + def test_vault_list_return_data(self, patch_ansible_module, kv1_get_response, vault_client, capfd): + client = vault_client + client.list.return_value = kv1_get_response.copy() + + with pytest.raises(SystemExit) as e: + vault_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.list.assert_called_once_with(patch_ansible_module['path']) + + assert result['data'] == kv1_get_response, "module result did not match expected result:\nexpected: %r\ngot: %r" % (kv1_get_response, result) + + @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) + def test_vault_list_no_data(self, patch_ansible_module, vault_client, capfd): + client = vault_client + client.list.return_value = None + + with pytest.raises(SystemExit) as e: + vault_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + + client.list.assert_called_once_with(patch_ansible_module['path']) + + match = re.search(r"The path '[^']+' doesn't seem to exist", result['msg']) + + assert match is not None, "Unexpected msg: %s" % result['msg'] + + @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) + def test_vault_list_no_hvac(self, capfd): + with mock.patch.multiple(vault_list, HAS_HVAC=False, HVAC_IMPORT_ERROR=None, create=True): + with pytest.raises(SystemExit) as e: + vault_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result['msg'] == missing_required_lib('hvac') + + @pytest.mark.parametrize( + 'exc', + [ + (hvac.exceptions.Forbidden, "", r"^Forbidden: Permission Denied to path '([^']+)'"), + ] + ) + @pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'path']], indirect=True) + @pytest.mark.parametrize('opt_path', ['path/1', 'second/path']) + def test_vault_list_vault_exception(self, vault_client, exc, opt_path, capfd): + + client = vault_client + client.list.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result['msg']) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_path == match.group(1) From 27ddf78b904c5390cd066e41bbde78c04eeb7176 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Fri, 13 Jan 2023 17:35:15 +0000 Subject: [PATCH 04/22] copyright - not sure if done correctly --- plugins/lookup/vault_list.py | 2 +- tests/unit/plugins/lookup/test_vault_list.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py index 2bb892057..e9d0b258e 100644 --- a/plugins/lookup/vault_list.py +++ b/plugins/lookup/vault_list.py @@ -1,4 +1,4 @@ -# (c) 2021, Tom Kivlin (@tomkivlin) +# (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 diff --git a/tests/unit/plugins/lookup/test_vault_list.py b/tests/unit/plugins/lookup/test_vault_list.py index ae99f44e1..c35b09708 100644 --- a/tests/unit/plugins/lookup/test_vault_list.py +++ b/tests/unit/plugins/lookup/test_vault_list.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022 Brian Scholer (@briantist) +# Copyright (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 80c74924ad186577a3138f9fcbd013125652642a Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Fri, 13 Jan 2023 17:35:28 +0000 Subject: [PATCH 05/22] add new plugins to codecov.yml --- codecov.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/codecov.yml b/codecov.yml index 8dafd8ba1..03b0eec8b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -20,6 +20,10 @@ flags: paths: - plugins/modules/vault_kv2_get.py + target_module_vault_list: + paths: + - plugins/modules/vault_list.py + target_module_vault_login: paths: - plugins/modules/vault_login.py @@ -44,6 +48,10 @@ flags: paths: - plugins/lookup/vault_kv2_get.py + target_lookup_vault_list: + paths: + - plugins/lookup/vault_list.py + target_lookup_vault_login: paths: - plugins/lookup/vault_login.py From 859cc23d38e9c314b7710033aa80fd8f37375ee3 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Fri, 13 Jan 2023 17:44:22 +0000 Subject: [PATCH 06/22] update documentation block for both plugins --- plugins/lookup/vault_list.py | 33 +++++++++++++++------------------ plugins/modules/vault_list.py | 8 ++++---- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py index e9d0b258e..510ee662e 100644 --- a/plugins/lookup/vault_list.py +++ b/plugins/lookup/vault_list.py @@ -33,32 +33,30 @@ """ EXAMPLES = """ -- name: Read a kv2 secret +- name: List all secrets at a path ansible.builtin.debug: - msg: "{{ lookup('community.hashi_vault.vault_read', 'secret/data/hello', url='https://vault:8201') }}" + msg: "{{ lookup('community.hashi_vault.vault_list', 'secret', url='https://vault:8201') }}" -- name: Retrieve an approle role ID +- name: List access policies ansible.builtin.debug: - msg: "{{ lookup('community.hashi_vault.vault_read', 'auth/approle/role/role-name/role-id', url='https://vault:8201') }}" + msg: "{{ lookup('community.hashi_vault.vault_list', 'sys/policies/acl', url='https://vault:8201') }}" -- name: Perform multiple reads with a single Vault login +- name: Perform multiple list operations with a single Vault login vars: paths: - - secret/data/hello - - auth/approle/role/role-one/role-id - - auth/approle/role/role-two/role-id + - secret + - sys/policies/acl ansible.builtin.debug: - msg: "{{ lookup('community.hashi_vault.vault_read', *paths, auth_method='userpass', username=user, password=pwd) }}" + msg: "{{ lookup('community.hashi_vault.vault_list', *paths, auth_method='userpass', username=user, password=pwd) }}" -- name: Perform multiple reads with a single Vault login in a loop +- name: Perform multiple list operations 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 + - secret + - sys/policies/acl ansible.builtin.debug: msg: '{{ item }}' - loop: "{{ query('community.hashi_vault.vault_read', *paths, auth_method='userpass', username=user, password=pwd) }}" + loop: "{{ query('community.hashi_vault.vault_list', *paths, auth_method='userpass', username=user, password=pwd) }}" - name: Perform multiple reads with a single Vault login in a loop (via with_) vars: @@ -67,10 +65,9 @@ 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 + with_community.hashi_vault.vault_list: + - secret + - sys/policies/acl """ RETURN = """ diff --git a/plugins/modules/vault_list.py b/plugins/modules/vault_list.py index e7332b474..b27b78a35 100644 --- a/plugins/modules/vault_list.py +++ b/plugins/modules/vault_list.py @@ -52,15 +52,15 @@ ansible.builtin.debug: msg: "{{ secret.data }}" -- name: List role IDs from Vault via the remote host +- name: List access policies from Vault via the remote host community.hashi_vault.vault_list: url: https://vault:8201 - path: auth/approle - register: approle_id + path: sys/policies/acl + register: policies - name: Display the role IDs ansible.builtin.debug: - msg: "{{ approle_id.data }}" + msg: "{{ policies.data }}" """ RETURN = """ From db6c609ff25d0b34515d66eaef83252cc0e2b128 Mon Sep 17 00:00:00 2001 From: Tom Kivlin <52716470+tomkivlin@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:48:26 +0000 Subject: [PATCH 07/22] Apply suggestions from @briantist code review Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com> --- plugins/lookup/vault_list.py | 6 ++---- plugins/modules/vault_list.py | 10 +++------- .../lookup_vault_list/tasks/lookup_vault_list_test.yml | 4 ++-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py index 510ee662e..2c663eb48 100644 --- a/plugins/lookup/vault_list.py +++ b/plugins/lookup/vault_list.py @@ -18,8 +18,6 @@ - Performs a generic list operation against a given path in HashiCorp Vault. seealso: - module: community.hashi_vault.vault_list - - 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 @@ -29,7 +27,7 @@ _terms: description: Vault path(s) to be listed. type: str - required: True + required: true """ EXAMPLES = """ @@ -58,7 +56,7 @@ msg: '{{ item }}' loop: "{{ query('community.hashi_vault.vault_list', *paths, auth_method='userpass', username=user, password=pwd) }}" -- name: Perform multiple reads with a single Vault login in a loop (via with_) +- 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 }}' diff --git a/plugins/modules/vault_list.py b/plugins/modules/vault_list.py index b27b78a35..fc70bb4cb 100644 --- a/plugins/modules/vault_list.py +++ b/plugins/modules/vault_list.py @@ -21,10 +21,6 @@ seealso: - ref: community.hashi_vault.vault_list lookup description: The official documentation for the C(community.hashi_vault.vault_list) lookup plugin. - - 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 @@ -35,7 +31,7 @@ path: description: Vault path to be listed. type: str - required: True + required: true """ EXAMPLES = """ @@ -58,9 +54,9 @@ path: sys/policies/acl register: policies -- name: Display the role IDs +- name: Display the policy names ansible.builtin.debug: - msg: "{{ policies.data }}" + msg: "{{ policies.data.keys }}" """ RETURN = """ diff --git a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml index 7efcaf3a0..e37f42aa8 100644 --- a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml +++ b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml @@ -29,7 +29,7 @@ - item | type_debug == 'dict' - "'data' in item" - "'keys' in item['data']" - # - "'value' in item['data']['keys']" + - item['data']['keys'] | type_debug == 'list' fail_msg: 'Return value was not correct type or items do not match.' loop: '{{ list_results }}' @@ -91,7 +91,7 @@ - item | type_debug == 'dict' - "'data' in item" - "'keys' in item['data']" - # - "'value' in item['data']['data']" + - item['data']['keys'] | type_debug == 'list' fail_msg: 'Return value was not correct type or items do not match.' with_community.hashi_vault.vault_list: - '{{ vault_kv2_api_list_path }}' From 5ffc8dcd0951c961c02e401085f718d5aa5ba7c0 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Mon, 16 Jan 2023 09:52:48 +0000 Subject: [PATCH 08/22] add vault_list to meta/runtime.yml --- meta/runtime.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/meta/runtime.yml b/meta/runtime.yml index fb5b0b8fb..4a4663dda 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -6,6 +6,7 @@ action_groups: - vault_kv1_get - vault_kv2_delete - vault_kv2_get + - vault_list - vault_login - vault_pki_generate_certificate - vault_read From cd583f9b8c8985a792deac010c295aa2a8cbb33a Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Mon, 16 Jan 2023 15:38:22 +0000 Subject: [PATCH 09/22] add extra examples as per suggestion --- plugins/lookup/vault_list.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py index 2c663eb48..f0c2fbb2e 100644 --- a/plugins/lookup/vault_list.py +++ b/plugins/lookup/vault_list.py @@ -66,6 +66,20 @@ with_community.hashi_vault.vault_list: - secret - sys/policies/acl + +- name: Create dictionary fact consisting of 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: 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 = """ From 148a3b1d6b60cb08be14c26d7c348df356b7993f Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Mon, 16 Jan 2023 16:01:16 +0000 Subject: [PATCH 10/22] new fixtures for unit tests --- tests/unit/fixtures/kv2_list_response.json | 15 +++++ tests/unit/fixtures/policy_list_response.json | 15 +++++ .../unit/fixtures/userpass_list_response.json | 15 +++++ tests/unit/plugins/lookup/test_vault_list.py | 66 +++++++++++++++++-- 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/unit/fixtures/kv2_list_response.json create mode 100644 tests/unit/fixtures/policy_list_response.json create mode 100644 tests/unit/fixtures/userpass_list_response.json diff --git a/tests/unit/fixtures/kv2_list_response.json b/tests/unit/fixtures/kv2_list_response.json new file mode 100644 index 000000000..6d3e5404a --- /dev/null +++ b/tests/unit/fixtures/kv2_list_response.json @@ -0,0 +1,15 @@ +{ + "auth": null, + "data": { + "keys": [ + "Secret1", + "Secret2" + ] + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "02e4b52a-23b1-9a1c-cf2b-3799edb17fed", + "warnings": null, + "wrap_info": null +} \ No newline at end of file diff --git a/tests/unit/fixtures/policy_list_response.json b/tests/unit/fixtures/policy_list_response.json new file mode 100644 index 000000000..ba31a51fe --- /dev/null +++ b/tests/unit/fixtures/policy_list_response.json @@ -0,0 +1,15 @@ +{ + "auth": null, + "data": { + "keys": [ + "Policy1", + "Policy2" + ] + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "96f2857e-5e33-1957-ea7e-be58f483faa3", + "warnings": null, + "wrap_info": null +} \ No newline at end of file diff --git a/tests/unit/fixtures/userpass_list_response.json b/tests/unit/fixtures/userpass_list_response.json new file mode 100644 index 000000000..c421d5507 --- /dev/null +++ b/tests/unit/fixtures/userpass_list_response.json @@ -0,0 +1,15 @@ +{ + "auth": null, + "data": { + "keys": [ + "User1", + "User2" + ] + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "8b18a5ca-9baf-eb7c-18a6-11be81ed95a6", + "warnings": null, + "wrap_info": null +} \ No newline at end of file diff --git a/tests/unit/plugins/lookup/test_vault_list.py b/tests/unit/plugins/lookup/test_vault_list.py index c35b09708..c4a15a41c 100644 --- a/tests/unit/plugins/lookup/test_vault_list.py +++ b/tests/unit/plugins/lookup/test_vault_list.py @@ -34,8 +34,18 @@ def vault_list_lookup(): @pytest.fixture -def kv1_get_response(fixture_loader): - return fixture_loader('kv1_get_response.json') +def kv2_list_response(fixture_loader): + return fixture_loader('kv2_list_response.json') + + +@pytest.fixture +def policy_list_response(fixture_loader): + return fixture_loader('policy_list_response.json') + + +@pytest.fixture +def userpass_list_response(fixture_loader): + return fixture_loader('userpass_list_response.json') class TestVaultListLookup(object): @@ -63,13 +73,61 @@ def test_vault_list_auth_validation_error(self, vault_list_lookup, minimal_vars, vault_list_lookup.run(terms='fake', variables=minimal_vars) @pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']]) - def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, kv1_get_response, vault_client, paths): + def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, kv2_list_response, vault_client, paths): + client = vault_client + + expected_calls = [mock.call(p) for p in paths] + + def _fake_kv1_get(path): + r = kv2_list_response.copy() + r.update({'_path': path}) + return r + + client.list = mock.Mock(wraps=_fake_kv1_get) + + response = vault_list_lookup.run(terms=paths, variables=minimal_vars) + + client.list.assert_has_calls(expected_calls) + + assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response)) + + for p in paths: + r = response.pop(0) + ins_p = r.pop('_path') + assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p) + + @pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']]) + def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, policy_list_response, vault_client, paths): + client = vault_client + + expected_calls = [mock.call(p) for p in paths] + + def _fake_kv1_get(path): + r = policy_list_response.copy() + r.update({'_path': path}) + return r + + client.list = mock.Mock(wraps=_fake_kv1_get) + + response = vault_list_lookup.run(terms=paths, variables=minimal_vars) + + client.list.assert_has_calls(expected_calls) + + assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response)) + + for p in paths: + r = response.pop(0) + ins_p = r.pop('_path') + assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p) + + @pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']]) + def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, userpass_list_response, vault_client, paths): client = vault_client expected_calls = [mock.call(p) for p in paths] def _fake_kv1_get(path): - r = kv1_get_response.copy() + r = userpass_list_response.copy() r.update({'_path': path}) return r From 2a6d76d8fc4f135101911f1737e39063d1a8f4e2 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Mon, 16 Jan 2023 13:34:46 -0500 Subject: [PATCH 11/22] dedup unit test for fixtures --- tests/unit/plugins/lookup/test_vault_list.py | 75 ++++---------------- 1 file changed, 12 insertions(+), 63 deletions(-) diff --git a/tests/unit/plugins/lookup/test_vault_list.py b/tests/unit/plugins/lookup/test_vault_list.py index c4a15a41c..dddb4a381 100644 --- a/tests/unit/plugins/lookup/test_vault_list.py +++ b/tests/unit/plugins/lookup/test_vault_list.py @@ -33,19 +33,16 @@ def vault_list_lookup(): return lookup_loader.get('community.hashi_vault.vault_list') -@pytest.fixture -def kv2_list_response(fixture_loader): - return fixture_loader('kv2_list_response.json') - - -@pytest.fixture -def policy_list_response(fixture_loader): - return fixture_loader('policy_list_response.json') +LIST_FIXTURES = [ + 'kv2_list_response.json', + 'policy_list_response.json', + 'userpass_list_response.json', +] -@pytest.fixture -def userpass_list_response(fixture_loader): - return fixture_loader('userpass_list_response.json') +@pytest.fixture(params=LIST_FIXTURES) +def list_response(request, fixture_loader): + return fixture_loader(request.param) class TestVaultListLookup(object): @@ -73,65 +70,17 @@ def test_vault_list_auth_validation_error(self, vault_list_lookup, minimal_vars, vault_list_lookup.run(terms='fake', variables=minimal_vars) @pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']]) - def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, kv2_list_response, vault_client, paths): - client = vault_client - - expected_calls = [mock.call(p) for p in paths] - - def _fake_kv1_get(path): - r = kv2_list_response.copy() - r.update({'_path': path}) - return r - - client.list = mock.Mock(wraps=_fake_kv1_get) - - response = vault_list_lookup.run(terms=paths, variables=minimal_vars) - - client.list.assert_has_calls(expected_calls) - - assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response)) - - for p in paths: - r = response.pop(0) - ins_p = r.pop('_path') - assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p) - - @pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']]) - def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, policy_list_response, vault_client, paths): - client = vault_client - - expected_calls = [mock.call(p) for p in paths] - - def _fake_kv1_get(path): - r = policy_list_response.copy() - r.update({'_path': path}) - return r - - client.list = mock.Mock(wraps=_fake_kv1_get) - - response = vault_list_lookup.run(terms=paths, variables=minimal_vars) - - client.list.assert_has_calls(expected_calls) - - assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response)) - - for p in paths: - r = response.pop(0) - ins_p = r.pop('_path') - assert p == ins_p, "expected '_path=%s' field was not found in response, got %r" % (p, ins_p) - - @pytest.mark.parametrize('paths', [['fake1'], ['fake2', 'fake3']]) - def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, userpass_list_response, vault_client, paths): + def test_vault_list_return_data(self, vault_list_lookup, minimal_vars, list_response, vault_client, paths): client = vault_client expected_calls = [mock.call(p) for p in paths] - def _fake_kv1_get(path): - r = userpass_list_response.copy() + def _fake_list_operation(path): + r = list_response.copy() r.update({'_path': path}) return r - client.list = mock.Mock(wraps=_fake_kv1_get) + client.list = mock.Mock(wraps=_fake_list_operation) response = vault_list_lookup.run(terms=paths, variables=minimal_vars) From 34c3d91d2b9e0f3d4c1f03713bb94779a178ccab Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Mon, 16 Jan 2023 13:38:48 -0500 Subject: [PATCH 12/22] update module units with new fixtures --- tests/unit/plugins/modules/test_vault_list.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/unit/plugins/modules/test_vault_list.py b/tests/unit/plugins/modules/test_vault_list.py index ab9e3f397..b6458a54b 100644 --- a/tests/unit/plugins/modules/test_vault_list.py +++ b/tests/unit/plugins/modules/test_vault_list.py @@ -48,9 +48,16 @@ def _combined_options(**kwargs): return opt -@pytest.fixture -def kv1_get_response(fixture_loader): - return fixture_loader('kv1_get_response.json') +LIST_FIXTURES = [ + 'kv2_list_response.json', + 'policy_list_response.json', + 'userpass_list_response.json', +] + + +@pytest.fixture(params=LIST_FIXTURES) +def list_response(request, fixture_loader): + return fixture_loader(request.param) class TestModuleVaultList(): @@ -84,9 +91,9 @@ def test_vault_list_auth_validation_error(self, authenticator, exc, capfd): assert result['msg'] == 'throwaway msg' @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) - def test_vault_list_return_data(self, patch_ansible_module, kv1_get_response, vault_client, capfd): + def test_vault_list_return_data(self, patch_ansible_module, list_response, vault_client, capfd): client = vault_client - client.list.return_value = kv1_get_response.copy() + client.list.return_value = list_response.copy() with pytest.raises(SystemExit) as e: vault_list.main() @@ -98,7 +105,7 @@ def test_vault_list_return_data(self, patch_ansible_module, kv1_get_response, va client.list.assert_called_once_with(patch_ansible_module['path']) - assert result['data'] == kv1_get_response, "module result did not match expected result:\nexpected: %r\ngot: %r" % (kv1_get_response, result) + assert result['data'] == list_response, "module result did not match expected result:\nexpected: %r\ngot: %r" % (kv1_get_response, result) @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) def test_vault_list_no_data(self, patch_ansible_module, vault_client, capfd): From 3d981bba008d23e7d9698a80511b364e6bdfc75d Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:09:38 -0500 Subject: [PATCH 13/22] more list lookup examples and formatting --- plugins/lookup/vault_list.py | 50 +++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py index f0c2fbb2e..8e5092764 100644 --- a/plugins/lookup/vault_list.py +++ b/plugins/lookup/vault_list.py @@ -33,7 +33,7 @@ EXAMPLES = """ - name: List all secrets at a path ansible.builtin.debug: - msg: "{{ lookup('community.hashi_vault.vault_list', 'secret', url='https://vault:8201') }}" + msg: "{{ lookup('community.hashi_vault.vault_list', 'secret/metadata/path', url='https://vault:8201') }}" - name: List access policies ansible.builtin.debug: @@ -42,7 +42,7 @@ - name: Perform multiple list operations with a single Vault login vars: paths: - - secret + - secret/metadata/path - sys/policies/acl ansible.builtin.debug: msg: "{{ lookup('community.hashi_vault.vault_list', *paths, auth_method='userpass', username=user, password=pwd) }}" @@ -50,7 +50,7 @@ - name: Perform multiple list operations with a single Vault login in a loop vars: paths: - - secret + - secret/metadata/path - sys/policies/acl ansible.builtin.debug: msg: '{{ item }}' @@ -64,22 +64,58 @@ ansible.builtin.debug: msg: '{{ item }}' with_community.hashi_vault.vault_list: - - secret + - secret/metadata/path - sys/policies/acl -- name: Create dictionary fact consisting of secret name (e.g. username) and value of a key (e.g. 'password') within that secret +- 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} ] }}" + 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 = """ From 9d12d57fdfb623c930806d2b9aec8e40d0aa8c73 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:11:27 -0500 Subject: [PATCH 14/22] update secret path in list module examples --- plugins/modules/vault_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/vault_list.py b/plugins/modules/vault_list.py index fc70bb4cb..e453be1ec 100644 --- a/plugins/modules/vault_list.py +++ b/plugins/modules/vault_list.py @@ -38,7 +38,7 @@ - name: List kv2 secrets from Vault via the remote host with userpass auth community.hashi_vault.vault_list: url: https://vault:8201 - path: secret + path: secret/metadata/path auth_method: userpass username: user password: '{{ passwd }}' From d6d586f0f80db035014ec6eb51ef301bfe727d48 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:26:22 -0500 Subject: [PATCH 15/22] fix policies to test inexistant path response --- .../integration/targets/setup_vault_configure/vars/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/targets/setup_vault_configure/vars/main.yml b/tests/integration/targets/setup_vault_configure/vars/main.yml index 324fcbd0b..434bc9802 100644 --- a/tests/integration/targets/setup_vault_configure/vars/main.yml +++ b/tests/integration/targets/setup_vault_configure/vars/main.yml @@ -23,7 +23,7 @@ vault_kv2_api_list_path: '{{ vault_kv2_mount_point }}/metadata' vault_policy_api_list_path: 'sys/policies/acl' -vault_kv2_api_list_inexistent_path: 'non_existent_{{ vault_kv2_mount_point }}/metadata' +vault_kv2_api_list_inexistent_path: '{{ vault_kv2_mount_point }}/metadata/__inexistant' vault_kv2_api_list_unauthorized_path: '{{ unauthorized_vault_kv2_mount_point }}/metadata' vault_base_policy: | @@ -76,6 +76,9 @@ vault_base_policy: | path "{{ vault_kv2_metadata_api_path }}/secret6" { capabilities = ["read"] } + path "{{ vault_kv2_api_list_inexistent_path }}" { + capabilities = ["list"] + } path "{{ vault_kv2_api_list_path }}" { capabilities = ["list"] } From 8f74aa1212be00d943d9d422fb4f862573aab6c8 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:29:06 -0500 Subject: [PATCH 16/22] fix inexistant path integration tests --- .../targets/lookup_vault_list/tasks/lookup_vault_list_test.yml | 2 +- .../targets/module_vault_list/tasks/module_vault_list_test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml index e37f42aa8..66962257f 100644 --- a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml +++ b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml @@ -75,7 +75,7 @@ - assert: that: - test_inexistent is failed - - test_inexistent.msg is search("Permission Denied") # This is the error received when listing a non-existent path + - test_inexistent.msg is search("doesn't seem to exist") fail_msg: "Expected failure but got success or wrong failure message." # do this last so our set_fact doesn't affect any other tests diff --git a/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml index 0cc7e276b..54a0c489b 100644 --- a/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml +++ b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml @@ -58,5 +58,5 @@ - assert: that: - test_inexistent is failed - - test_inexistent.msg is search("Permission Denied") # This is the error received when listing a non-existent path + - test_inexistent.msg is search("doesn't seem to exist") fail_msg: "Expected failure but got success or wrong failure message." From 86d1b86d0883c681ea9a0fc9f367b98a5cd17bed Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:58:35 -0500 Subject: [PATCH 17/22] missed variable substitution --- tests/unit/plugins/modules/test_vault_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/test_vault_list.py b/tests/unit/plugins/modules/test_vault_list.py index b6458a54b..6891547be 100644 --- a/tests/unit/plugins/modules/test_vault_list.py +++ b/tests/unit/plugins/modules/test_vault_list.py @@ -105,7 +105,7 @@ def test_vault_list_return_data(self, patch_ansible_module, list_response, vault client.list.assert_called_once_with(patch_ansible_module['path']) - assert result['data'] == list_response, "module result did not match expected result:\nexpected: %r\ngot: %r" % (kv1_get_response, result) + assert result['data'] == list_response, "module result did not match expected result:\nexpected: %r\ngot: %r" % (list_response, result) @pytest.mark.parametrize('patch_ansible_module', [_combined_options()], indirect=True) def test_vault_list_no_data(self, patch_ansible_module, vault_client, capfd): From 857568ab7e72c9826cf56b5bb3691a4d950433d3 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Tue, 17 Jan 2023 09:03:08 +0000 Subject: [PATCH 18/22] update paths and add comments explaining --- plugins/modules/vault_list.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/modules/vault_list.py b/plugins/modules/vault_list.py index e453be1ec..03a3098a6 100644 --- a/plugins/modules/vault_list.py +++ b/plugins/modules/vault_list.py @@ -38,15 +38,17 @@ - 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/path + path: secret/metadata + # For kv2, the path needs to follow the pattern 'mount_point/metadata' to list all secrets in that path auth_method: userpass username: user password: '{{ passwd }}' register: secret -- name: Display the secret data +- name: Display the secrets found at the path provided above ansible.builtin.debug: - msg: "{{ secret.data }}" + 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: @@ -56,7 +58,8 @@ - name: Display the policy names ansible.builtin.debug: - msg: "{{ policies.data.keys }}" + msg: "{{ policies.data.data['keys'] }}" + # Note that secret.data.data.keys won't work as 'keys' is a built-in method """ RETURN = """ From 4050a8e2375e5595bdda227e95c8f1bc15983ac5 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Tue, 17 Jan 2023 15:25:32 +0000 Subject: [PATCH 19/22] correct the path for lookup plugin --- plugins/lookup/vault_list.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py index 8e5092764..4dc9989f4 100644 --- a/plugins/lookup/vault_list.py +++ b/plugins/lookup/vault_list.py @@ -33,7 +33,8 @@ EXAMPLES = """ - name: List all secrets at a path ansible.builtin.debug: - msg: "{{ lookup('community.hashi_vault.vault_list', 'secret/metadata/path', url='https://vault:8201') }}" + 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' to list all secrets in that path - name: List access policies ansible.builtin.debug: @@ -42,7 +43,7 @@ - name: Perform multiple list operations with a single Vault login vars: paths: - - secret/metadata/path + - secret/metadata - sys/policies/acl ansible.builtin.debug: msg: "{{ lookup('community.hashi_vault.vault_list', *paths, auth_method='userpass', username=user, password=pwd) }}" @@ -50,7 +51,7 @@ - name: Perform multiple list operations with a single Vault login in a loop vars: paths: - - secret/metadata/path + - secret/metadata - sys/policies/acl ansible.builtin.debug: msg: '{{ item }}' @@ -64,7 +65,7 @@ ansible.builtin.debug: msg: '{{ item }}' with_community.hashi_vault.vault_list: - - secret/metadata/path + - 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 From 1f0ea0336ac6b389b8fcdc1db06512f468a6964b Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:16:56 -0500 Subject: [PATCH 20/22] Update tests/integration/targets/setup_vault_configure/vars/main.yml --- tests/integration/targets/setup_vault_configure/vars/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/setup_vault_configure/vars/main.yml b/tests/integration/targets/setup_vault_configure/vars/main.yml index 434bc9802..092c11654 100644 --- a/tests/integration/targets/setup_vault_configure/vars/main.yml +++ b/tests/integration/targets/setup_vault_configure/vars/main.yml @@ -23,7 +23,7 @@ vault_kv2_api_list_path: '{{ vault_kv2_mount_point }}/metadata' vault_policy_api_list_path: 'sys/policies/acl' -vault_kv2_api_list_inexistent_path: '{{ vault_kv2_mount_point }}/metadata/__inexistant' +vault_kv2_api_list_inexistent_path: '{{ vault_kv2_mount_point }}/metadata/__inexistent' vault_kv2_api_list_unauthorized_path: '{{ unauthorized_vault_kv2_mount_point }}/metadata' vault_base_policy: | From c294b877a9fe33ca872d5addfff1ef9ce89ef181 Mon Sep 17 00:00:00 2001 From: Tom Kivlin Date: Wed, 18 Jan 2023 13:40:33 +0000 Subject: [PATCH 21/22] add further tests and comments --- plugins/lookup/vault_list.py | 2 +- plugins/modules/vault_list.py | 2 +- .../tasks/lookup_vault_list_test.yml | 39 +++++++++++++++++++ .../tasks/module_vault_list_test.yml | 38 ++++++++++++++++++ .../setup_vault_configure/vars/main.yml | 12 ++++-- 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/plugins/lookup/vault_list.py b/plugins/lookup/vault_list.py index 4dc9989f4..56521c792 100644 --- a/plugins/lookup/vault_list.py +++ b/plugins/lookup/vault_list.py @@ -34,7 +34,7 @@ - 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' to list all secrets in that path + # 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: diff --git a/plugins/modules/vault_list.py b/plugins/modules/vault_list.py index 03a3098a6..a0823dc2d 100644 --- a/plugins/modules/vault_list.py +++ b/plugins/modules/vault_list.py @@ -39,7 +39,7 @@ community.hashi_vault.vault_list: url: https://vault:8201 path: secret/metadata - # For kv2, the path needs to follow the pattern 'mount_point/metadata' to list all secrets in that path + # 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 }}' diff --git a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml index 66962257f..95432be10 100644 --- a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml +++ b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml @@ -17,6 +17,15 @@ - "'keys' in kv2_secret2['data']" fail_msg: 'Return value did not contain expected fields.' + - name: 'Check kv2 mount point list' + vars: + kv2_mount_point: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_mount_point, **kwargs) }}" + assert: + that: + - "'data' in kv2_mount_point" + - "'keys' in kv2_mount_point['data']" + fail_msg: 'Return value did not contain expected fields.' + - name: "Check multiple path list as array" vars: paths: @@ -64,6 +73,21 @@ - test_unauthorized.msg is search('Permission Denied') fail_msg: "Expected failure but got success or wrong failure message." + # When an inexistent mount point is listed, the API returns a 403 error, not 404. + - name: 'Failure expected when inexistent mount point is listed' + vars: + mount_point_inexistent: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_inexistent_mount_point, **kwargs) }}" + debug: + msg: 'Failure is expected ({{ mount_point_inexistent }})' + register: test_inexistent_mount_point + ignore_errors: true + + - assert: + that: + - test_inexistent_mount_point is failed + - test_inexistent_mount_point.msg is search("Permission Denied") + fail_msg: "Expected failure but got success or wrong failure message." + - name: 'Failure expected when inexistent path is listed' vars: path_inexistent: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_inexistent_path, **kwargs) }}" @@ -78,6 +102,21 @@ - test_inexistent.msg is search("doesn't seem to exist") fail_msg: "Expected failure but got success or wrong failure message." + # If an inexistent path is listed by is included in a policy statement that denies access, the API returns a 403 error. + - name: 'Failure expected when inexistent path is listed but is explicitly mentioned in a policy statement' + vars: + path_inexistent_unauthorized: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_inexistent_unauthorized_path, **kwargs) }}" + debug: + msg: 'Failure is expected ({{ path_inexistent_unauthorized }})' + register: test_inexistent_unauthorized + ignore_errors: true + + - assert: + that: + - test_inexistent_unauthorized is failed + - test_inexistent_unauthorized.msg is search("Permission Denied") + fail_msg: "Expected failure but got success or wrong failure message." + # do this last so our set_fact doesn't affect any other tests - name: Set the vars that will configure the lookup settings we can't set via with_ set_fact: diff --git a/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml index 54a0c489b..59f85eb85 100644 --- a/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml +++ b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml @@ -22,6 +22,18 @@ - "'keys' in kv2_path['data']['data']" fail_msg: 'Return value did not contain expected fields.' + - name: 'Check kv2 mount point list' + register: kv2_mount_point + community.hashi_vault.vault_list: + path: "{{ vault_kv2_api_list_mount_point }}" + + - assert: + that: + - "'data' in kv2_mount_point" + - "'data' in kv2_mount_point['data']" + - "'keys' in kv2_mount_point['data']['data']" + fail_msg: 'Return value did not contain expected fields.' + ### failure tests - name: 'Failure expected when erroneous credentials are used' @@ -49,6 +61,19 @@ - test_unauthorized.msg is search('Permission Denied') fail_msg: "Expected failure but got success or wrong failure message." + # When an inexistent mount point is listed, the API returns a 403 error, not 404. + - name: 'Failure expected when inexistent mount point is listed' + register: test_inexistent_mount_point + community.hashi_vault.vault_list: + path: "{{ vault_kv2_api_list_inexistent_mount_point }}" + ignore_errors: true + + - assert: + that: + - test_inexistent_mount_point is failed + - test_inexistent_mount_point.msg is search("Permission Denied") + fail_msg: "Expected failure but got success or wrong failure message." + - name: 'Failure expected when inexistent path is listed' register: test_inexistent community.hashi_vault.vault_list: @@ -60,3 +85,16 @@ - test_inexistent is failed - test_inexistent.msg is search("doesn't seem to exist") fail_msg: "Expected failure but got success or wrong failure message." + + # If an inexistent path is listed by is included in a policy statement that denies access, the API returns a 403 error. + - name: 'Failure expected when inexistent path is listed but is explicitly mentioned in a policy statement' + register: test_inexistent_unauthorized + community.hashi_vault.vault_list: + path: "{{ vault_kv2_api_list_inexistent_unauthorized_path }}" + ignore_errors: true + + - assert: + that: + - test_inexistent_unauthorized is failed + - test_inexistent_unauthorized.msg is search("Permission Denied") + fail_msg: "Expected failure but got success or wrong failure message." \ No newline at end of file diff --git a/tests/integration/targets/setup_vault_configure/vars/main.yml b/tests/integration/targets/setup_vault_configure/vars/main.yml index 434bc9802..900185ed8 100644 --- a/tests/integration/targets/setup_vault_configure/vars/main.yml +++ b/tests/integration/targets/setup_vault_configure/vars/main.yml @@ -19,11 +19,14 @@ vault_kv2_multi_api_path: '{{ vault_kv2_mount_point }}/data/{{ vault_kv2_multi_p vault_kv2_versioned_api_path: '{{ vault_kv2_mount_point }}/data/{{ vault_kv2_versioned_path }}' vault_kv2_delete_api_path: '{{ vault_kv2_mount_point }}/delete/{{ vault_kv2_versioned_path }}' vault_kv2_metadata_api_path: '{{ vault_kv2_mount_point }}/metadata/{{ vault_kv2_versioned_path }}' -vault_kv2_api_list_path: '{{ vault_kv2_mount_point }}/metadata' +vault_kv2_api_list_mount_point: '{{ vault_kv2_mount_point }}/metadata' +vault_kv2_api_list_path: '{{ vault_kv2_mount_point }}/metadata/{{ vault_kv2_path }}' vault_policy_api_list_path: 'sys/policies/acl' -vault_kv2_api_list_inexistent_path: '{{ vault_kv2_mount_point }}/metadata/__inexistant' +vault_kv2_api_list_inexistent_path: '{{ vault_kv2_mount_point }}/metadata/__inexistent' +vault_kv2_api_list_inexistent_mount_point: '{{ vault_kv2_mount_point }}__inexistent/metadata' +vault_kv2_api_list_inexistent_unauthorized_path: '{{ vault_kv2_mount_point }}/metadata/__inexistent_no_auth' vault_kv2_api_list_unauthorized_path: '{{ unauthorized_vault_kv2_mount_point }}/metadata' vault_base_policy: | @@ -76,7 +79,7 @@ vault_base_policy: | path "{{ vault_kv2_metadata_api_path }}/secret6" { capabilities = ["read"] } - path "{{ vault_kv2_api_list_inexistent_path }}" { + path "{{ vault_kv2_api_list_mount_point }}/*" { capabilities = ["list"] } path "{{ vault_kv2_api_list_path }}" { @@ -85,6 +88,9 @@ vault_base_policy: | path "{{ vault_policy_api_list_path }}" { capabilities = ["list"] } + path "{{ vault_kv2_api_list_inexistent_unauthorized_path }}" { + capabilities = ["deny"] + } path "{{ vault_kv2_api_list_unauthorized_path }}" { capabilities = ["deny"] } From 2b43f4c1b7c7972a9ee15a47c4947a26df590418 Mon Sep 17 00:00:00 2001 From: Tom Kivlin <52716470+tomkivlin@users.noreply.github.com> Date: Wed, 18 Jan 2023 21:13:03 +0000 Subject: [PATCH 22/22] Apply suggestions from code review Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com> --- .../lookup_vault_list/tasks/lookup_vault_list_test.yml | 2 +- .../module_vault_list/tasks/module_vault_list_test.yml | 4 ++-- tests/integration/targets/setup_vault_configure/vars/main.yml | 2 +- tests/unit/fixtures/kv2_list_response.json | 2 +- tests/unit/fixtures/policy_list_response.json | 2 +- tests/unit/fixtures/userpass_list_response.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml index 95432be10..9c2190208 100644 --- a/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml +++ b/tests/integration/targets/lookup_vault_list/tasks/lookup_vault_list_test.yml @@ -102,7 +102,7 @@ - test_inexistent.msg is search("doesn't seem to exist") fail_msg: "Expected failure but got success or wrong failure message." - # If an inexistent path is listed by is included in a policy statement that denies access, the API returns a 403 error. + # If an inexistent path is included in a policy statement that denies access, the list API returns a 403 error. - name: 'Failure expected when inexistent path is listed but is explicitly mentioned in a policy statement' vars: path_inexistent_unauthorized: "{{ lookup('community.hashi_vault.vault_list', vault_kv2_api_list_inexistent_unauthorized_path, **kwargs) }}" diff --git a/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml index 59f85eb85..64f40d845 100644 --- a/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml +++ b/tests/integration/targets/module_vault_list/tasks/module_vault_list_test.yml @@ -86,7 +86,7 @@ - test_inexistent.msg is search("doesn't seem to exist") fail_msg: "Expected failure but got success or wrong failure message." - # If an inexistent path is listed by is included in a policy statement that denies access, the API returns a 403 error. + # If an inexistent path is included in a policy statement that denies access, the list API returns a 403 error. - name: 'Failure expected when inexistent path is listed but is explicitly mentioned in a policy statement' register: test_inexistent_unauthorized community.hashi_vault.vault_list: @@ -97,4 +97,4 @@ that: - test_inexistent_unauthorized is failed - test_inexistent_unauthorized.msg is search("Permission Denied") - fail_msg: "Expected failure but got success or wrong failure message." \ No newline at end of file + fail_msg: "Expected failure but got success or wrong failure message." diff --git a/tests/integration/targets/setup_vault_configure/vars/main.yml b/tests/integration/targets/setup_vault_configure/vars/main.yml index 5a4cb1db2..900185ed8 100644 --- a/tests/integration/targets/setup_vault_configure/vars/main.yml +++ b/tests/integration/targets/setup_vault_configure/vars/main.yml @@ -26,7 +26,7 @@ vault_policy_api_list_path: 'sys/policies/acl' vault_kv2_api_list_inexistent_path: '{{ vault_kv2_mount_point }}/metadata/__inexistent' vault_kv2_api_list_inexistent_mount_point: '{{ vault_kv2_mount_point }}__inexistent/metadata' -vault_kv2_api_list_inexistent_unauthorized_path: '{{ vault_kv2_mount_point }}/metadate/__inexistent_no_auth' +vault_kv2_api_list_inexistent_unauthorized_path: '{{ vault_kv2_mount_point }}/metadata/__inexistent_no_auth' vault_kv2_api_list_unauthorized_path: '{{ unauthorized_vault_kv2_mount_point }}/metadata' vault_base_policy: | diff --git a/tests/unit/fixtures/kv2_list_response.json b/tests/unit/fixtures/kv2_list_response.json index 6d3e5404a..2fe833b51 100644 --- a/tests/unit/fixtures/kv2_list_response.json +++ b/tests/unit/fixtures/kv2_list_response.json @@ -12,4 +12,4 @@ "request_id": "02e4b52a-23b1-9a1c-cf2b-3799edb17fed", "warnings": null, "wrap_info": null -} \ No newline at end of file +} diff --git a/tests/unit/fixtures/policy_list_response.json b/tests/unit/fixtures/policy_list_response.json index ba31a51fe..5a7dfdb8d 100644 --- a/tests/unit/fixtures/policy_list_response.json +++ b/tests/unit/fixtures/policy_list_response.json @@ -12,4 +12,4 @@ "request_id": "96f2857e-5e33-1957-ea7e-be58f483faa3", "warnings": null, "wrap_info": null -} \ No newline at end of file +} diff --git a/tests/unit/fixtures/userpass_list_response.json b/tests/unit/fixtures/userpass_list_response.json index c421d5507..84cabf3bb 100644 --- a/tests/unit/fixtures/userpass_list_response.json +++ b/tests/unit/fixtures/userpass_list_response.json @@ -12,4 +12,4 @@ "request_id": "8b18a5ca-9baf-eb7c-18a6-11be81ed95a6", "warnings": null, "wrap_info": null -} \ No newline at end of file +}