diff --git a/integration/keeper_secrets_manager_ansible/README.md b/integration/keeper_secrets_manager_ansible/README.md index a5dfc5bc..ee548fb0 100644 --- a/integration/keeper_secrets_manager_ansible/README.md +++ b/integration/keeper_secrets_manager_ansible/README.md @@ -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 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 5a0daa7e..42e63e1b 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 @@ -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)) diff --git a/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/execution-environment.yml b/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/execution-environment.yml index bd4443fc..bd532fae 100644 --- a/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/execution-environment.yml +++ b/integration/keeper_secrets_manager_ansible/ansible_galaxy/tower_execution_environment/execution-environment.yml @@ -10,4 +10,10 @@ dependencies: ansible_runner: package_pip: ansible-runner galaxy: requirements.yml - python: requirements.txt \ No newline at end of file + python: requirements.txt + +additional_build_packages: + - openssh-clients + - sshpass + - rsync + - git \ No newline at end of file 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 2e3e428e..4d3f03d9 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,2 @@ -importlib_metadata -keeper-secrets-manager-core>=17.0.0 -keeper-secrets-manager-helper>=1.0.5 \ No newline at end of file +keeper-secrets-manager-core>=17.2.0 +keeper-secrets-manager-helper>=1.1.0 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 d081f7f3..eb41bb7c 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 @@ -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 @@ -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)) diff --git a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__main__.py b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__main__.py index 8822c6dc..ca26c2fe 100644 --- a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__main__.py +++ b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/__main__.py @@ -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 @@ -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() diff --git a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_create.py b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_create.py index a0cdbaad..1444c89b 100644 --- a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_create.py +++ b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_create.py @@ -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: diff --git a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_info.py b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_info.py index ee459f5b..e7682b00 100644 --- a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_info.py +++ b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/action/keeper_info.py @@ -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() @@ -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 diff --git a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/modules/keeper_create.py b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/modules/keeper_create.py index 633cc6fe..96720995 100644 --- a/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/modules/keeper_create.py +++ b/integration/keeper_secrets_manager_ansible/keeper_secrets_manager_ansible/plugins/modules/keeper_create.py @@ -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: diff --git a/integration/keeper_secrets_manager_ansible/requirements.txt b/integration/keeper_secrets_manager_ansible/requirements.txt index 74febd6d..fee550f0 100644 --- a/integration/keeper_secrets_manager_ansible/requirements.txt +++ b/integration/keeper_secrets_manager_ansible/requirements.txt @@ -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 diff --git a/integration/keeper_secrets_manager_ansible/setup.py b/integration/keeper_secrets_manager_ansible/setup.py index 930875f8..6c5f6da9 100644 --- a/integration/keeper_secrets_manager_ansible/setup.py +++ b/integration/keeper_secrets_manager_ansible/setup.py @@ -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", @@ -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/" @@ -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" diff --git a/integration/keeper_secrets_manager_ansible/tests/keeper_create_empty_folder_test.py b/integration/keeper_secrets_manager_ansible/tests/keeper_create_empty_folder_test.py new file mode 100644 index 00000000..f09cd359 --- /dev/null +++ b/integration/keeper_secrets_manager_ansible/tests/keeper_create_empty_folder_test.py @@ -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) diff --git a/integration/keeper_secrets_manager_ansible/tests/keeper_create_test.py b/integration/keeper_secrets_manager_ansible/tests/keeper_create_test.py index da4685d5..c1e9ac00 100644 --- a/integration/keeper_secrets_manager_ansible/tests/keeper_create_test.py +++ b/integration/keeper_secrets_manager_ansible/tests/keeper_create_test.py @@ -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] @@ -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( diff --git a/integration/keeper_secrets_manager_ansible/tests/keeper_tower_ee_test.py b/integration/keeper_secrets_manager_ansible/tests/keeper_tower_ee_test.py new file mode 100644 index 00000000..1565283d --- /dev/null +++ b/integration/keeper_secrets_manager_ansible/tests/keeper_tower_ee_test.py @@ -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)", + )