Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions integration/keeper_secrets_manager_ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ For more information see our official documentation page https://docs.keeper.io/

# Changes

## 1.4.0
* KSM-827: Fixed Tower Execution Environment Docker image missing system packages required by AAP
- Added `openssh-clients`, `sshpass`, `rsync`, and `git` to `additional_build_packages` in `execution-environment.yml`
- Resolves `[dumb-init] ssh agent: No such file or directory` error in Ansible Automation Platform
- The `redhat/ubi9` base image (introduced Oct 2025) does not include these packages that the previous `ansible-runner` base provided
- `openssh-clients`: provides `ssh-agent` required by AAP at container startup
- `sshpass`: required for password-based SSH connections (`ansible_ssh_pass`)
- `rsync`: required by `ansible.builtin.synchronize` module
- `git`: required by `ansible.builtin.git` module
- Added regression test to prevent recurrence
* KSM-816: Fixed `keeper_create` failing when the target shared folder contains no records
- The plugin now uses the `get_folders` endpoint to resolve the folder encryption key,
which returns all accessible folders regardless of whether they contain records
- Previously, the plugin used `get_secrets` which only returns folder keys alongside
records — empty shared folders were invisible, causing creation to fail
- Closes [GitHub issue #934](https://github.com/Keeper-Security/secrets-manager/issues/934)
* KSM-811: Raised minimum Python version from 3.7 to 3.9
- Aligns with the Python 3.9+ requirement of keeper-secrets-manager-core >= 17.2.0
- Added classifiers for Python 3.12 and 3.13
* **Dependency Update**: Updated keeper-secrets-manager-core to >= 17.2.0 and keeper-secrets-manager-helper to >= 1.1.0

## 1.3.0
* KSM-781: Fixed Jinja2 templating for `keeper_config_file` and `keeper_cache_dir` variables
- Variables like `{{ playbook_dir }}/keeper-config.yml` are now resolved before use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ configuration file or even a playbook.

# Changes

## 1.4.0
* KSM-827: Fixed Tower Execution Environment Docker image missing system packages required by AAP
- Added `openssh-clients`, `sshpass`, `rsync`, and `git` to the EE image
- Resolves `[dumb-init] ssh agent: No such file or directory` error in Ansible Automation Platform
* KSM-816: Fixed `keeper_create` failing when the target shared folder contains no records
- Closes [GitHub issue #934](https://github.com/Keeper-Security/secrets-manager/issues/934)
* KSM-811: Raised minimum Python version from 3.7 to 3.9
- Replaced `importlib_metadata` backport with stdlib `importlib.metadata` (available since Python 3.8)
* **Dependency Update**: Updated keeper-secrets-manager-core to >= 17.2.0 and keeper-secrets-manager-helper to >= 1.1.0

## 1.3.0
* KSM-781: Fixed Jinja2 templating for `keeper_config_file` and `keeper_cache_dir` variables
* KSM-714: Added notes field update support to `keeper_set`
* KSM-768: Added notes field retrieval support to `keeper_get`
* KSM-770: Fixed `keeper_get` error when `notes: yes` is used with an empty notes field
* KSM-771: Fixed `keeper_copy` error when `notes: yes` parameter is present
* KSM-772: Fixed `keeper_set` notes field being set to `None` instead of provided value
* KSM-773: Standardized `notes` parameter name across `keeper_create`, `keeper_set`, `keeper_copy`
* KSM-780: Added backward-compatible `note` alias (deprecated, will be removed in 2.0.0)
* **Dependency Update**: Updated Python SDK requirement to v17.1.0

## 1.2.6
* KSM-672: KSMCache class initializes cache file path before env vars are set. Closes ([issue #675](https://github.com/Keeper-Security/secrets-manager/issues/675))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ dependencies:
ansible_runner:
package_pip: ansible-runner
galaxy: requirements.yml
python: requirements.txt
python: requirements.txt

additional_build_packages:
- openssh-clients
- sshpass
- rsync
- git
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
importlib_metadata
keeper-secrets-manager-core>=17.0.0
keeper-secrets-manager-helper>=1.0.5
keeper-secrets-manager-core>=17.2.0
keeper-secrets-manager-helper>=1.1.0
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
KSM_SDK_ERR = traceback.format_exc()
else:
from keeper_secrets_manager_core import SecretsManager
from keeper_secrets_manager_core.core import KSMCache
from keeper_secrets_manager_core.core import KSMCache, CreateOptions
from keeper_secrets_manager_core.storage import FileKeyValueStorage, InMemoryKeyValueStorage
from keeper_secrets_manager_core.utils import generate_password as sdk_generate_password, strtobool

Expand Down Expand Up @@ -478,8 +478,14 @@ def get_record(self, uids=None, titles=None, cache=None):
return records[0]

def create_record(self, new_record, shared_folder_uid):
# KSM-816: use create_secret_with_options() instead of create_secret() so
# that folder keys are fetched via the get_folders endpoint, which returns
# all folders including empty ones. create_secret() uses get_secrets() which
# only returns folder keys when the folder already contains records.
try:
record_uid = self.client.create_secret(shared_folder_uid, new_record)
record_uid = self.client.create_secret_with_options(
CreateOptions(shared_folder_uid, None), new_record
)
except Exception as err:
raise Exception("Cannot get create record: {}".format(err))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import sys
import os
import platform
import importlib_metadata
from importlib.metadata import version as pkg_version, PackageNotFoundError
import keeper_secrets_manager_core
import logging
import ansible
Expand Down Expand Up @@ -55,8 +55,8 @@ def _version():
}
for module in versions:
try:
versions[module] = importlib_metadata.version(module)
except importlib_metadata.PackageNotFoundError:
versions[module] = pkg_version(module)
except PackageNotFoundError:
pass

print()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
options:
shared_folder_uid:
description:
- The UID of shared folder in your Keeper application.
- The UID of the top-level shared folder in your Keeper application.
- Must be a shared folder UID, not a subfolder UID.
type: str
required: yes
record_type:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from keeper_secrets_manager_helper.record_type import RecordType
from keeper_secrets_manager_helper.field_type import FieldType
from ansible.utils.display import Display
import importlib_metadata
from importlib.metadata import version as pkg_version, PackageNotFoundError
import json

display = Display()
Expand Down Expand Up @@ -66,8 +66,8 @@ def get_versions():

for module in versions:
try:
versions[module] = importlib_metadata.version(module)
except importlib_metadata.PackageNotFoundError:
versions[module] = pkg_version(module)
except PackageNotFoundError:
pass

return versions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
options:
shared_folder_uid:
description:
- The UID of shared folder in your Keeper application.
- The UID of the top-level shared folder in your Keeper application.
- Must be a shared folder UID, not a subfolder UID.
type: str
required: yes
record_type:
Expand Down
5 changes: 2 additions & 3 deletions integration/keeper_secrets_manager_ansible/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
ansible-core>=2.12.0
importlib_metadata
keeper-secrets-manager-core>=17.0.0
keeper-secrets-manager-helper>=1.0.5
keeper-secrets-manager-core>=17.2.0
keeper-secrets-manager-helper>=1.1.0
13 changes: 6 additions & 7 deletions integration/keeper_secrets_manager_ansible/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
long_description = fp.read()

install_requires = [
'keeper-secrets-manager-core>=17.1.0',
'keeper-secrets-manager-helper>=1.0.5',
'importlib_metadata',
'keeper-secrets-manager-core>=17.2.0',
'keeper-secrets-manager-helper>=1.1.0',
'ansible-core>=2.12.0' # Use ansible-core instead of ansible to avoid community collections
]

setup(
name="keeper-secrets-manager-ansible",
version='1.3.0',
version='1.4.0',
description="Keeper Secrets Manager plugins for Ansible.",
long_description=long_description,
long_description_content_type="text/markdown",
Expand All @@ -29,7 +28,7 @@
packages=find_packages(exclude=["tests", "tests.*"]),
zip_safe=False,
install_requires=install_requires,
python_requires='>=3.7',
python_requires='>=3.9',
project_urls={
"Bug Tracker": "https://github.com/Keeper-Security/secrets-manager/issues",
"Documentation": "https://app.gitbook.com/@keeper-security/s/secrets-manager/secrets-manager/"
Expand All @@ -44,11 +43,11 @@
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Security",
"Topic :: System :: Installation/Setup",
"Topic :: System :: Systems Administration"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest
import pytest
from keeper_secrets_manager_core import SecretsManager, mock
from keeper_secrets_manager_core.storage import InMemoryKeyValueStorage
from keeper_secrets_manager_helper.record import Record


class KeeperCreateEmptyFolderTest(unittest.TestCase):
"""
Regression test for KSM-816 / GitHub issue #934.

keeper_create fails when the target shared folder contains no records.
create_secret() uses get_secrets(full_response=True) to look up the folder
encryption key, but that endpoint only returns folders bundled with records.
Empty folders are invisible to it, so the key is never found.
"""

def test_create_secret_fails_on_empty_folder(self):
"""Reproduce: create_secret raises when the target folder has no records."""

secrets_manager = SecretsManager(
config=InMemoryKeyValueStorage(config=mock.MockConfig.make_base64())
)

# Empty response — no records, no folders.
# Simulates what the backend returns when a KSM app has access to a
# shared folder that contains zero records.
empty_response = mock.Response()
queue = mock.ResponseQueue(client=secrets_manager)
queue.add_response(empty_response)
secrets_manager.custom_post_function = queue.post_method

record = Record(version="v3").create_from_field_list(
record_type="login",
title="Test Record",
notes=None,
fields=[],
password_generate=False,
password_complexity=None
)
record_create = record[0].get_record_create_obj()

with pytest.raises(Exception) as exc_info:
secrets_manager.create_secret("EMPTY_FOLDER_UID", record_create)

assert "was not retrieved" in str(exc_info.value)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# This is tied to the test. If additional tests are added, they will need their own create_secret mock method.
def mocked_create_secret(*args):

# First one in the shared folder uid
# args[0] is a CreateOptions object (folder_uid, subfolder_uid)
_ = args[0]
record_create = args[1]

Expand Down Expand Up @@ -48,7 +48,7 @@ def mocked_create_secret(*args):

class KeeperCreateTest(unittest.TestCase):

@patch("keeper_secrets_manager_core.core.SecretsManager.create_secret", side_effect=mocked_create_secret)
@patch("keeper_secrets_manager_core.core.SecretsManager.create_secret_with_options", side_effect=mocked_create_secret)
def test_keeper_create(self, mock_create):
with tempfile.TemporaryDirectory() as _:
a = AnsibleTestFramework(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
import unittest
import yaml


EE_SPEC_PATH = os.path.join(
os.path.dirname(__file__),
"..",
"ansible_galaxy",
"tower_execution_environment",
"execution-environment.yml",
)


# Packages required in the EE that are absent from the redhat/ubi9 base image.
# ansible-runner (the previous base) included these; ubi9 does not.
REQUIRED_PACKAGES = {
"openssh-clients": "provides ssh-agent required by AAP at container startup",
"sshpass": "required for password-based SSH (ansible_ssh_pass)",
"rsync": "required by ansible.builtin.synchronize module",
"git": "required by ansible.builtin.git module",
}


class TowerExecutionEnvironmentTest(unittest.TestCase):
"""KSM-827: Verify tower EE spec includes required system packages.

The keeper-secrets-manager-tower-ee image uses redhat/ubi9 as its base.
UBI9 is a minimal OS image — it does not include the packages that
ansible-runner (the previous base) provided. Missing packages cause
runtime failures in AAP that are not caught by the Ansible plugin unit
tests because those tests never build or run the Docker image.
"""

def setUp(self):
with open(EE_SPEC_PATH, "r") as f:
self.spec = yaml.safe_load(f)

def test_required_packages_in_additional_build_packages(self):
packages = self.spec.get("additional_build_packages", [])
for package, reason in REQUIRED_PACKAGES.items():
with self.subTest(package=package):
self.assertIn(
package,
packages,
f"execution-environment.yml must include '{package}' in "
f"additional_build_packages — {reason} (KSM-827)",
)
Loading