From feb697b06fbcbc4d00225d7258a59b56e63c8846 Mon Sep 17 00:00:00 2001 From: John Walstra <98108730+jwalstra-keeper@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:05:35 -0600 Subject: [PATCH] KSM-472 Add keeper_get_record action (#568) Added a new action to return all the fields in a record as a dictionary. The key in the dictionary is the field label, or field type if the label is missing. The key is normalized to remove any non-alphanumeric characters. Non-alphanumeric characters replaced with underscores. To use this action must be used with the 'register' attribute in order to store the results in memory. The results can be accessed the same way other stored structures are stored in Ansbile. --- .../keeper_secrets_manager_ansible/README.md | 6 + .../keeper_secrets_manager/README.md | 12 +- .../keeper_secrets_manager/meta/runtime.yml | 1 + .../requirements.txt | 2 +- .../__init__.py | 119 +++++++++++----- .../plugins/action_plugins/keeper_get.py | 6 +- .../action_plugins/keeper_get_record.py | 132 ++++++++++++++++++ .../requirements.txt | 2 +- .../keeper_secrets_manager_ansible/setup.py | 5 +- .../playbooks/keeper_get_record.yml | 58 ++++++++ .../playbooks/keeper_get_record_cache.yml | 46 ++++++ .../tests/keeper_get_record_test.py | 67 +++++++++ 12 files changed, 412 insertions(+), 44 deletions(-) create mode 100644 integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get_record.py create mode 100644 integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record.yml create mode 100644 integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record_cache.yml create mode 100644 integration/keeper_secrets_manager_ansible/tests/keeper_get_record_test.py diff --git a/integration/keeper_secrets_manager_ansible/README.md b/integration/keeper_secrets_manager_ansible/README.md index f8f8ac87..92e9ba01 100644 --- a/integration/keeper_secrets_manager_ansible/README.md +++ b/integration/keeper_secrets_manager_ansible/README.md @@ -5,6 +5,7 @@ This module contains plugins that allow your Ansible automations to use Keeper S * `keeper_cache_records` - Generate a cache to use with other actions. * `keeper_copy` - Similar to `ansible.builtin.copy`. Uses the KSM vault for the source/content. * `keeper_get` - Retrieve secrets from a record. +* `keeper_get_record` - Retrieve records as a dictionary. * `keeper_set` - Update an existing record from Ansible information. * `keeper_init` - Initialize a KSM configuration from a one-time access token. * `keeper_cleanup` - Remove the cache file, if being used. @@ -18,6 +19,11 @@ For more information see our official documentation page https://docs.keeper.io/ # Changes +## 1.2.2 +* Add action `keeper_get_record` to return record as a dictionary. +* Clean up comments. +* Update pinned KSM SDK version to 16.6.3. + ## 1.2.1 * Add action `keeper_remove` to remove secrets from the Keeper Vault * Update pinned KSM SDK version to 16.6.2. diff --git a/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/README.md b/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/README.md index fe57c142..4f9cce81 100644 --- a/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/README.md +++ b/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/README.md @@ -64,9 +64,10 @@ If you omit the `collections` , you will need to use the full plugin name. * `keepersecurity.keeper_secrets_manager.keeper_cache_records` - Generate a cache to use with other actions. * `keepersecurity.keeper_secrets_manager.keeper_copy` - Copy file, or value, from your vault to a remote server. -* `keepersecurity.keeper_secrets_manager.keeper_get` - Get a value from your vault. +* `keepersecurity.keeper_secrets_manager.keeper_get` - Get a value from a record. +* `keepersecurity.keeper_secrets_manager.keeper_get_record` - Get record as a dictionary. * `keepersecurity.keeper_secrets_manager.keeper_set` - Set a value of an existing record in your vault. -* `keepersecurity.keeper_secrets_manager.keeper_create` - Create a new record in your vault. +* `keepersecurity.keeper_secrets_manager.keeper_create` - Create a new record. * `keepersecurity.keeper_secrets_manager.keeper_remove` - Remove a record from your vault. * `keepersecurity.keeper_secrets_manager.keeper_password` - Generate a random password. * `keepersecurity.keeper_secrets_manager.keeper_cleanup` - Clean up Keeper related files. @@ -118,8 +119,13 @@ configuration file or even a playbook. # Changes +## 1.2.2 +* Add action `keeper_get_record` to return entire record as dictionary. +* Clean up comments in code. +* Update pinned KSM SDK version to 16.6.3. + ## 1.2.1 -* Add action `keeper_remove` to remove secrets from the Keeper Vault +* Add action `keeper_remove` to remove secrets from the Keeper Vault. * Update pinned KSM SDK version to 16.6.2. ## 1.2.0 diff --git a/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/meta/runtime.yml b/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/meta/runtime.yml index 7c4307a7..e7ed932b 100644 --- a/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/meta/runtime.yml +++ b/integration/keeper_secrets_manager_ansible/ansible_galaxy/keepersecurity/keeper_secrets_manager/meta/runtime.yml @@ -5,6 +5,7 @@ action_groups: - keeper_cache_records - keeper_copy - keeper_get + - keeper_get_record - keeper_set - keeper_cleanup - keeper_create diff --git a/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/requirements.txt b/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/requirements.txt index 1228d4f8..0addcc35 100644 --- a/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/requirements.txt +++ b/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/requirements.txt @@ -1,3 +1,3 @@ importlib_metadata -keeper-secrets-manager-core>=16.6.2 +keeper-secrets-manager-core>=16.6.3 keeper-secrets-manager-helper \ No newline at end of file diff --git a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__init__.py b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__init__.py index d4d3be4e..f0de6399 100644 --- a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__init__.py +++ b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__init__.py @@ -20,7 +20,6 @@ import re import json import random -from re import sub from enum import Enum import traceback import pickle @@ -63,7 +62,7 @@ def get_enum(value): class KeeperAnsible: - """ A class containing common method used by the Ansible plugin and also talked to Keeper Python SDK + """ A class containing a common method used by the Ansible plugin and also talked to Keeper Python SDK """ KEY_PREFIX = "keeper" @@ -100,7 +99,8 @@ def fail_json(msg, **kwargs): def __init__(self, task_vars, action_module=None, task_attributes=None, force_in_memory=False): - """ Build the config used by the Keeper Python SDK + """ + Build the config used by the Keeper Python SDK The configuration is mainly read from a JSON file. @@ -138,7 +138,7 @@ def __init__(self, task_vars, action_module=None, task_attributes=None, force_in self.secret_values = [] def camel_case(text): - text = sub(r"([_\-])+", " ", text).title().replace(" ", "") + text = re.sub(r"([_\-])+", " ", text).title().replace(" ", "") return text[0].lower() + text[1:] try: @@ -200,8 +200,9 @@ def camel_case(text): # not to leave config files lying around. in_memory_storage = True - # If we have parameter with a Base64 config, use it for the config_option and force + # If we have a parameter with a Base64 config, use it for the config_option and force # the config to be in memory. + base64_key = KeeperAnsible.keeper_key(KeeperAnsible.KEY_CONFIG_BASE64) if base64_key in task_vars: config_option = task_vars.get(base64_key) @@ -219,8 +220,9 @@ def camel_case(text): if keeper_key in task_vars: config_option[camel_key] = task_vars[keeper_key] - # Token is the odd ball. we need it to be client key in the SDK config. SDK will remove it - # when it is done. + # The token is the odd ball. + # We need it to be client key in the SDK config. + # SDK will remove it when it is done. token_key = KeeperAnsible.keeper_key(KeeperAnsible.TOKEN_KEY) if token_key in task_vars: config_option[KeeperAnsible.CONFIG_CLIENT_KEY] = task_vars[token_key] @@ -231,7 +233,7 @@ def camel_case(text): elif token_key in task_vars: config_option[KeeperAnsible.CONFIG_CLIENT_KEY] = task_vars[token_key] - # If no variables were passed in throw an error. + # If no variables were passed in, throw an error. if len(config_option) == 0: raise AnsibleError("There is no config file and the Ansible variable contain no config keys." " Will not be able to connect to the Keeper server.") @@ -256,7 +258,8 @@ def camel_case(text): elif os.path.isfile(self.config_file) is False: self.config_created = True - # Write the variables we have to a JSON file. If we are in here config_option is a dictionary, + # Write the variables we have to a JSON file. + # If we are in here, config_option is a dictionary, # not a Base64 string. with open(self.config_file, "w") as fh: json.dump(config_option, fh, indent=4) @@ -408,7 +411,7 @@ def get_records_from_vault(self, uids=None, titles=None, encrypt=False): if titles is not None: records = self.client.get_secrets() - # If we are getting uid we need only select amount. + # If we are getting uid, we need only select amount. else: records = self.client.get_secrets(uids) except Exception as err: @@ -486,8 +489,10 @@ def remove_record(self, uids=None, titles=None, cache=None): @staticmethod def _gather_secrets(obj): - """ Walk the secret structure and get values. These should just be str, list, and dict. Warn if the SDK - return something different. + """ + Walk the secret structure and get values. + These should just be str, list, and dict. + Warn if the SDK returns something different. """ result = [] if type(obj) is str: @@ -504,7 +509,8 @@ def _gather_secrets(obj): return result def stash_secret_value(self, value): - """ Parse the result of the secret retrieval and add values to list of secret values. + """ + Parse the result of the secret retrieval and add values to a list of secret values. """ for secret_value in self._gather_secrets(value): if secret_value not in self.secret_values: @@ -549,7 +555,7 @@ def get_value(self, field_type, key, uid=None, title=None, allow_array=False, ar self.stash_secret_value(values) - # If we want the entire array, then just return what we got from the field. + # If we want the entire array, then return what we got from the field. if allow_array is True: return values @@ -571,6 +577,45 @@ def get_value(self, field_type, key, uid=None, title=None, allow_array=False, ar return value + def get_dict(self, uid=None, title=None, cache=None, allow=None): + + record = self.get_record(uids=uid, titles=title, cache=cache) + + record_dict = {} + for field_section in ["fields", "custom"]: + for field in record.dict.get(field_section, []): + label = field.get("label") + type = field.get("type") + value = field.get("value", []) + key = label if label is not None else type + + if key is None or key == "": + display.vvvvv("record contains a field without a label or type.") + continue + + # Scrub the label to make it a clean key. + # Only allow alphanumerics, remove runs of _, remove _ at the start and end of the key. + key = re.sub('[^0-9a-zA-Z]+', '_', key) + key = re.sub('_+', '_', key) + key = re.sub('^_+', '', key) + key = re.sub('_+$', '', key) + + if key == "": + display.vvvvv("record contains a field that has no alphanumeric characters. cannot use this field.") + continue + + if key in record_dict: + count = len([x for x in record_dict if x.startswith(key)]) + key = key + "_" + str(count) + + if allow is not None and key not in allow: + continue + + record_dict[key] = value + self.stash_secret_value(value) + + return record_dict + def set_value(self, field_type, key, value, uid=None, title=None, cache=None): record = self.get_record(uids=uid, titles=title, cache=cache) @@ -589,13 +634,14 @@ def set_value(self, field_type, key, value, uid=None, title=None, cache=None): @staticmethod def get_field_type_enum_and_key(args): - """ Get the field type enum and field key in the Ansible args for a task. + """ + Get the field type enum and field key in the Ansible args for a task. - For a task that, only allowed one of the allowed field, this method will find the type of field and + For a task that only allowed one of the allowed fields, this method will find the type of field and the key/label for that field. - If multiple fields types are specified, an error will be thrown. If no fields are found, an error will be - thrown. + If multiple fields types are specified, an error will be thrown. + If no fields are found, an error will be thrown. The method will return the KeeperFieldType enum for the field type and the name of the field in Keeper that the task requires. @@ -617,12 +663,14 @@ def get_field_type_enum_and_key(args): return KeeperFieldType.get_enum(field_type[0]), field_key def add_secret_values_to_results(self, results): - """ If the 'redact' stdout callback is being used, add the secrets to the results dictionary. The redact - stdout callback will remove it from the results. It will use value to remove values from stdout. + """ + If the 'redact' stdout callback is being used, add the secrets to the result dictionary. + The redacted stdout callback will remove it from the results. + It will use value to remove values from stdout. """ - # If we are using the redact stdout callback, add the secrets we retrieve to the special key. The redact - # stdout callback will make sure the value is not in the stdout. + # If we are using the redacted stdout callback, add the secrets we retrieve to the special key. + # The redacted stdout callback will make sure the value is not in the stdout. if self.has_redact is True: results["_secrets"] = self.secret_values return results @@ -630,24 +678,27 @@ def add_secret_values_to_results(self, results): @staticmethod def password_complexity_translation(**kwargs): """ - Generate a password complexity dictionary + Generate a password complexity dictionary. - Password complexity differ from place to place :( + Password complexity differs from place to place. - This is in more tune with the Vault UI since most service just want a specific set of characters, but not - a quantity. And some characters are illegal for specific services. Neither the SDK and Vault UI address this. + This is in more tune with the Vault UI since most services just want a specific set of characters, but not + a quantity. + And some characters are illegal for specific services. + Neither the SDK and Vault UI address this. So this is the third standard. - kwargs + Kwargs * length - Length of the password * allow_lowercase - Allow lowercase letters. Default is True. * allow_uppercase - Allow uppercase letters. Default is True. * allow_digits - Allow digits. Default is True. * allow_symbols - Allow symbols. Default is True - * filter_characters - An array of characters not to use. Some servies don't like some characters. + * filter_characters - An array of characters not to use. Some services don't like some characters. - The length is divided by the allowed characters. So with a length of 64, each would get 16 of each character. + The length is divided by the allowed characters. + So with a length of 64, each would get 16 of all characters. If the length cannot be unevenly divided, additional will be added to the first allowed character in the above list. @@ -679,7 +730,7 @@ def password_complexity_translation(**kwargs): complexity = { "length": length, - # This is not part of the standard, however it's important because some service will not accept certain + # This is not part of the standard, however, it's important because some service will not accept certain # characters. "filter_characters": filter_characters } @@ -713,7 +764,7 @@ def replacement_char(**kwargs): attempt = 0 while True: - # If allow everything, then just get a lowercase letter + # If allow everything, then get a lowercase letter if all_true is True: new_char = "abcdefghijklmnopqrstuvwxyz"[random.randint(0, 25)] @@ -736,7 +787,8 @@ def replacement_char(**kwargs): if new_char not in kwargs.get("filter_characters"): break - # Ok, some user might go crazy and filter out every letter, digit, and symbol and cause an infinite loop. + # Ok, some user might go overboard and filter out every letter, digit, and symbol + # and cause an infinite loop. # If we can't find a good character after 25 attempts, error out. attempt += 1 if attempt > 25: @@ -759,7 +811,8 @@ def generate_password(**kwargs): # The SDK generate_password doesn't know what the filter_characters is, remove it for now. filter_characters = kwargs.pop("filter_characters", None) - # The SDK uses these a params, record complexity use the ones on the right. Translate them. + # The SDK uses these a params, record complexity uses the ones on the right. + # Translate them. kwargs["uppercase"] = kwargs.pop("caps", None) kwargs["special_characters"] = kwargs.pop("special", None) diff --git a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get.py b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get.py index 3a7ec7c0..95494842 100644 --- a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get.py +++ b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get.py @@ -103,16 +103,16 @@ EXAMPLES = r''' - name: Get login name - keeper_copy: + keeper_get: uid: XXX field: login register: my_login_value - name: Get login name via notation - keeper_copy: + keeper_get: notation: XXX/field/login register: my_login_value - name: Get custom field - keeper_copy: + keeper_get: uid: XXX custom_field: Custom Label register: my_custom_value diff --git a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get_record.py b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get_record.py new file mode 100644 index 00000000..ef4b8ab9 --- /dev/null +++ b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action_plugins/keeper_get_record.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# _ __ +# | |/ /___ ___ _ __ ___ _ _ (R) +# | ' =16.6.2 +keeper-secrets-manager-core>=16.6.3 keeper-secrets-manager-helper diff --git a/integration/keeper_secrets_manager_ansible/setup.py b/integration/keeper_secrets_manager_ansible/setup.py index bd67108a..8a708175 100644 --- a/integration/keeper_secrets_manager_ansible/setup.py +++ b/integration/keeper_secrets_manager_ansible/setup.py @@ -17,7 +17,7 @@ setup( name="keeper-secrets-manager-ansible", - version='1.2.1', + version='1.2.2', description="Keeper Secrets Manager plugins for Ansible.", long_description=long_description, long_description_content_type="text/markdown", @@ -29,7 +29,7 @@ packages=find_packages(exclude=["tests", "tests.*"]), zip_safe=False, install_requires=install_requires, - python_requires='>=3.6', + python_requires='>=3.7', project_urls={ "Bug Tracker": "https://github.com/Keeper-Security/secrets-manager/issues", "Documentation": "https://app.gitbook.com/@keeper-security/s/secrets-manager/secrets-manager/" @@ -44,7 +44,6 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record.yml b/integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record.yml new file mode 100644 index 00000000..7a75eb72 --- /dev/null +++ b/integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record.yml @@ -0,0 +1,58 @@ +# vim: set shiftwidth=2 tabstop=2 softtabstop=-1 expandtab: +--- +- name: Keeper Get Record + hosts: "my_systems" + gather_facts: no + + tasks: + - name: "Get Record By UID" + keeper_get_record: + uid: "{{ uid }}" + allow: + - login + - password + - phone + - D1 + - D1_1 + register: "my_record" + + - name: "The Record" + debug: + msg: "{{ my_record.record }}" + verbosity: 0 + + - name: Check for login + assert: + that: + - my_record.record.login[0] == "MYLOGIN" + fail_msg: "did not contain MYLOGIN" + + - name: Check for password + assert: + that: + - my_record.record.password[0] == "MYPASSWORD" + fail_msg: "did not contain MYPASSWORD" + + - name: Check for D1 + assert: + that: + - my_record.record.D1[0] == "DUP 1" + fail_msg: "did not contain DUP 1" + + - name: Check for D1_1 + assert: + that: + - my_record.record.D1_1[0] == "DUP 2" + fail_msg: "did not contain DUP 2" + + - name: Check for phone, first number + assert: + that: + - my_record.record.phone[0].number == "15551234" + fail_msg: "did not contain 15551234" + + - name: Check for phone, second number + assert: + that: + - my_record.record.phone[1].number == "15557890" + fail_msg: "did not contain 15557890" \ No newline at end of file diff --git a/integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record_cache.yml b/integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record_cache.yml new file mode 100644 index 00000000..14686832 --- /dev/null +++ b/integration/keeper_secrets_manager_ansible/tests/ansible_example/playbooks/keeper_get_record_cache.yml @@ -0,0 +1,46 @@ +# vim: set shiftwidth=2 tabstop=2 softtabstop=-1 expandtab: +--- +- name: Keeper Get Record Cache + hosts: "my_systems" + gather_facts: no + + tasks: + - name: Generate a Keeper Record Cache secret + keeper_password: + length: 64 + register: keeper_record_cache_secret + # no_log: True + + - name: Store the Keeper Record Cache secret into variables. + set_fact: + keeper_record_cache_secret: "{{ keeper_record_cache_secret.password }}" + # no_log: True + + - name: Cache records. Will use keeper_record_cache_secret from above. + keeper_cache_records: + titles: + - "{{ title }}" + register: my_records + # no_log: True + + - name: "Get Record By Title" + keeper_get_record: + title: "{{ title }}" + register: "my_record" + + - name: "The Record" + debug: + msg: "{{ my_record.record }}" + verbosity: 0 + + - name: Check for bad label 1 + assert: + that: + - my_record.record.This_I_A_Bad_Label[0] == "BAD" + fail_msg: "did not contain BAD" + + - name: Check for bad label 2 + assert: + that: + - my_record.record.This_I_A_Bad_Label_1[0] == "BAD 2" + fail_msg: "did not contain BAD 2" \ No newline at end of file diff --git a/integration/keeper_secrets_manager_ansible/tests/keeper_get_record_test.py b/integration/keeper_secrets_manager_ansible/tests/keeper_get_record_test.py new file mode 100644 index 00000000..7cdc8111 --- /dev/null +++ b/integration/keeper_secrets_manager_ansible/tests/keeper_get_record_test.py @@ -0,0 +1,67 @@ +import unittest +from keeper_secrets_manager_core.mock import Record, Response +from .ansible_test_framework import AnsibleTestFramework +import tempfile + + +mock_response = Response() +mock_record = Record(title="Record 1", record_type="login") +mock_record.field("login", "MYLOGIN") +mock_record.field("password", "MYPASSWORD") +mock_record.field("text", "TEXT", "Text Label") +mock_record.field("phone", [ + { + "number": "15551234", + "type": "Home" + }, + { + "number": "15557890", + "type": "Work" + } +]) +mock_record.field("fileRef", ["XXXXX", "YYYYY"]) +mock_record.custom_field("C1","CUSTOM 1", field_type="text") +mock_record.custom_field("D1", "DUP 1", field_type="text") +mock_record.custom_field("D1", "DUP 2", field_type="text") +mock_record.custom_field("This! **I$** A Bad Label...", "BAD", field_type="text") +mock_record.custom_field(" This! **I$** A Bad Label...", "BAD 2", field_type="text") +mock_response.add_record(record=mock_record) + +class KeeperGetRecordTest(unittest.TestCase): + + def test_keeper_get_record(self): + + with tempfile.TemporaryDirectory() as temp_dir: + a = AnsibleTestFramework( + playbook="keeper_get_record.yml", + vars={ + "tmp_dir": temp_dir, + "uid": mock_record.uid, + "title": mock_record.title + }, + mock_responses=[mock_response] + ) + result, out, err = a.run() + self.assertEqual(result["ok"], 8, "8 things didn't happen") + self.assertEqual(result["failed"], 0, "failed was not 0") + self.assertEqual(result["changed"], 0, "0 things didn't change") + + + + def test_keeper_get_record_cache(self): + + with tempfile.TemporaryDirectory() as temp_dir: + a = AnsibleTestFramework( + playbook="keeper_get_record_cache.yml", + vars={ + "tmp_dir": temp_dir, + "uid": mock_record.uid, + "title": mock_record.title + }, + mock_responses=[mock_response] + ) + result, out, err = a.run() + self.assertEqual(result["ok"], 7, "7 things didn't happen") + self.assertEqual(result["failed"], 0, "failed was not 0") + self.assertEqual(result["changed"], 0, "0 things didn't change") + self.assertRegex(out, r'MYPASSWORD', "Did not find the password in the stdout")