Skip to content

Commit

Permalink
Add keeper_remove action
Browse files Browse the repository at this point in the history
KSM-471

* Added an action plugin to delete record from the Vault.
* Update the pinned KSM SDK module
* Unpinned Ansible version so it is tested against latest version.
  • Loading branch information
jwalstra-keeper committed Oct 27, 2023
1 parent a8187c3 commit 453e778
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 16 deletions.
4 changes: 4 additions & 0 deletions integration/keeper_secrets_manager_ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ For more information see our official documentation page https://docs.keeper.io/

# Changes

## 1.2.1
* Add action `keeper_remove` to remove secrets from the Keeper Vault
* Update pinned KSM SDK version to 16.6.2.

## 1.2.0

* Added action `keeper_cache_records` to cache Keeper Vault records to reduce API calls.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ If you omit the `collections` , you will need to use the full plugin name.
* `keepersecurity.keeper_secrets_manager.keeper_get` - Get a value from your vault.
* `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_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.
* `keepersecurity.keeper_secrets_manager.keeper_info` - Display information about plugin, record and field types.
Expand Down Expand Up @@ -117,6 +118,10 @@ configuration file or even a playbook.

# Changes

## 1.2.1
* Add action `keeper_remove` to remove secrets from the Keeper Vault
* Update pinned KSM SDK version to 16.6.2.

## 1.2.0

* Added action `keeper_cache_records` to cache Keeper Vault records to reduce API calls.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
importlib_metadata
keeper-secrets-manager-core>=16.6.0
keeper-secrets-manager-helper>=1.0.4
keeper-secrets-manager-core>=16.6.2
keeper-secrets-manager-helper
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,10 @@ def camel_case(text):
display.vvv("Loading keeper config from Ansible vars.")

# Since we are getting our variables from Ansible, we want to default using the in memory storage so
# not to leave config files laying around.
# not to leave config files lying around.
in_memory_storage = True

# If be have parameter with a Base64 config, use it for the config_option and force
# If we have 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:
Expand Down Expand Up @@ -388,9 +388,9 @@ def _find_records(records, uids=None, titles=None):
uid_map.pop(uid, None)

if len(uid_map) > 0:
raise ValueError(f"The following record uid(s) could not be found: {list(uid_map.keys())}")
raise AnsibleError(f"The following record uid(s) could not be found: {list(uid_map.keys())}")
if len(title_map) > 0:
raise ValueError(f"The following record title(s) could not be found: {list(title_map.keys())}")
raise AnsibleError(f"The following record title(s) could not be found: {list(title_map.keys())}")

return [found_records[x] for x in found_records]

Expand Down Expand Up @@ -473,6 +473,17 @@ def create_record(self, new_record, shared_folder_uid):

return record_uid

def remove_record(self, uids=None, titles=None, cache=None):

records = self.get_records(cache=cache, uids=uids, titles=titles)
if len(records) > 1 and titles is not None:
raise AnsibleError("Found multiple records for the Title. To fix, make sure records "
"have a unique Title or use a UID.")

display.vvvvvv(f"removing record UID {records[0].uid}")

self.client.delete_secret([records[0].uid])

@staticmethod
def _gather_secrets(obj):
""" Walk the secret structure and get values. These should just be str, list, and dict. Warn if the SDK
Expand Down Expand Up @@ -636,13 +647,13 @@ def password_complexity_translation(**kwargs):
* allow_symbols - Allow symbols. Default is True
* filter_characters - An array of characters not to use. Some servies 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 characters.
The length is divided by the allowed characters. So with a length of 64, each would get 16 of each character.
If the length cannot be unevenly divided, additional will be added to the first allowed character in the above
list.
"""

# This maps nicer human readable keys to the ones used the records' complexity.
# This maps nicer human-readable keys to the ones used the records' complexity.
kwargs_map = [
{"param": "allow_lowercase", "key": "lowercase"},
{"param": "allow_uppercase", "key": "caps"},
Expand Down Expand Up @@ -725,7 +736,7 @@ 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 invite loop.
# Ok, some user might go crazy 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _config():
if os.fstat(0) == os.fstat(1):
print("\n# Below are the directory paths to action and lookup plugins.", file=sys.stderr)

# Ansible doesn't really work on Windows, however include this anyways for the cleaver DevOp.
# Ansible doesn't really work on Windows, however include this any ways for the cleaver DevOp.
if platform.system() == 'Windows':
is_power_shell = len(os.getenv('PSModulePath', '').split(os.pathsep)) >= 3
if is_power_shell is True:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# _ __
# | |/ /___ ___ _ __ ___ _ _ (R)
# | ' </ -_) -_) '_ \/ -_) '_|
# |_|\_\___\___| .__/\___|_|
# |_|
#
# Keeper Secrets Manager
# Copyright 2021 Keeper Security Inc.
# Contact: [email protected]
#

from ansible.plugins.action import ActionBase
from ansible.errors import AnsibleError
from ansible.utils.display import Display
from keeper_secrets_manager_ansible import KeeperAnsible

DOCUMENTATION = r'''
---
module: keeper_remove
short_description: Remove a secret from the vault.
version_added: "1.2.1"
description:
- Remove a secret from the vault.
author:
- John Walstra
options:
uid:
description:
- The UID of the Keeper Vault record.
type: str
required: no
title:
description:
- The Title of the Keeper Vault record.
type: str
required: no
version_added: '1.2.0'
cache:
description:
- The cache registered by keeper_get_records_cache.
- Used to lookup Keeper Vault record by title.
type: str
required: no
version_added: '1.2.0'
'''

EXAMPLES = r'''
- name: Remove secret using UID.
keeper_remove:
uid: XXX
- name: Remove secret using title.
keeper_remove:
title: XXXXXXXXX
'''

RETURN = r'''
existed:
description: Indicates that the record did exist in the Vault.
returned: success
sample: |
{
"existed": True
},
'''

display = Display()


class ActionModule(ActionBase):

def run(self, tmp=None, task_vars=None):
super(ActionModule, self).run(tmp, task_vars)

if task_vars is None:
task_vars = {}

keeper = KeeperAnsible(task_vars=task_vars, action_module=self)

cache = self._task.args.get("cache")

uid = self._task.args.get("uid")
title = self._task.args.pop("title", None)
if uid is None and title is None:
raise AnsibleError("The uid and title are blank. keeper_get requires one to be set.")
if uid is not None and title is not None:
raise AnsibleError("The uid and title are both set. keeper_get requires one to be set, but not both.")

keeper.remove_record(uids=uid, titles=title, cache=cache)

return {}
4 changes: 2 additions & 2 deletions integration/keeper_secrets_manager_ansible/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ansible
importlib_metadata
keeper-secrets-manager-core>=16.4.1
keeper-secrets-manager-helper>=1.0.4
keeper-secrets-manager-core>=16.6.2
keeper-secrets-manager-helper
8 changes: 4 additions & 4 deletions integration/keeper_secrets_manager_ansible/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
long_description = fp.read()

install_requires = [
'keeper-secrets-manager-core>=16.6.0',
'keeper-secrets-manager-helper>=1.0.4',
'keeper-secrets-manager-core>=16.6.2',
'keeper-secrets-manager-helper',
'importlib_metadata',
'ansible<8.0.0'
'ansible'
]

setup(
name="keeper-secrets-manager-ansible",
version='1.2.0',
version='1.2.1',
description="Keeper Secrets Manager plugins for Ansible.",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# vim: set shiftwidth=2 tabstop=2 softtabstop=-1 expandtab:
---
- name: Keeper Remove
hosts: "my_systems"
gather_facts: no

tasks:
- name: "Remove By UID"
keeper_remove:
uid: "{{ uid }}"

- name: "Remove By Title"
keeper_remove:
title: "{{ title }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# vim: set shiftwidth=2 tabstop=2 softtabstop=-1 expandtab:
---
- name: Keeper Remove 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:
uids:
- "{{ uid }}"
titles:
- "{{ title }}"
register: my_records
# no_log: True

- name: "Remove By UID"
keeper_remove:
cache: "{{ my_records.cache }}"
uid: "{{ uid }}"

- name: "Remove By Title"
keeper_remove:
cache: "{{ my_records.cache }}"
title: "{{ title }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import unittest
from unittest.mock import patch
from keeper_secrets_manager_core.mock import Record, Response
from .ansible_test_framework import AnsibleTestFramework
import tempfile



mock_record_1 = Record(title="Record 1", record_type="login")
mock_record_1.field("password", "PASS 1")
mock_record_2 = Record(title="Record 2", record_type="login")
mock_record_2.field("password", "PASS 2")

mock_response_1 = Response()
mock_response_1.add_record(record=mock_record_1)
mock_response_1.add_record(record=mock_record_2)
mock_response_1.add_record(record=mock_record_1)
mock_response_1.add_record(record=mock_record_2)


mock_response_2 = Response()
mock_response_2.add_record(record=mock_record_1)
mock_response_2.add_record(record=mock_record_2)
mock_response_2.add_record(record=mock_record_1)
mock_response_2.add_record(record=mock_record_2)


class KeeperRemoveTest(unittest.TestCase):

def test_keeper_remove(self):

with patch(f'keeper_secrets_manager_core.SecretsManager.delete_secret') as mock_delete:
mock_delete.return_value = None

with tempfile.TemporaryDirectory() as temp_dir:
a = AnsibleTestFramework(
playbook="keeper_remove.yml",
vars={
"tmp_dir": temp_dir,
"uid": mock_record_1.uid,
"title": mock_record_2.title
},
mock_responses=[mock_response_1]
)
result, out, err = a.run()
self.assertEqual(result["ok"], 2, "2 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_remove_cache(self):

with patch(f'keeper_secrets_manager_core.SecretsManager.delete_secret') as mock_delete:
mock_delete.return_value = None

with tempfile.TemporaryDirectory() as temp_dir:
a = AnsibleTestFramework(
playbook="keeper_remove_cache.yml",
vars={
"tmp_dir": temp_dir,
"uid": mock_record_1.uid,
"title": mock_record_2.title
},
mock_responses=[mock_response_2]
)
result, out, err = a.run()
self.assertEqual(result["ok"], 5, "5 things didn't happen")
self.assertEqual(result["failed"], 0, "failed was not 0")
self.assertEqual(result["changed"], 0, "0 things didn't change")

0 comments on commit 453e778

Please sign in to comment.