diff --git a/CHANGELOG.md b/CHANGELOG.md index 495b3dd1..96904ce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the Zowe Client Python SDK will be documented in this fil ### Bug Fixes +- Fixed secure `user`/`password` properties not being loaded for team-config profiles nested more than one level deep which caused 401 errors. [#411](https://github.com/zowe/zowe-client-python-sdk/pull/411) - Redacted request headers and restricted log directory/file to owner-only access. [#404](https://github.com/zowe/zowe-client-python-sdk/pull/404) - Fixed `Jobs.get_job_output_as_files` writing to a directory it never created, and made job output paths stay within the target directory. [#403](https://github.com/zowe/zowe-client-python-sdk/pull/403) - Updated the `pyo3` dependency of the Secrets SDK for technical currency. [#399](https://github.com/zowe/zowe-client-python-sdk/pull/399) diff --git a/src/core/zowe/core_for_zowe_sdk/config_file.py b/src/core/zowe/core_for_zowe_sdk/config_file.py index 82bc14d4..ef462127 100644 --- a/src/core/zowe/core_for_zowe_sdk/config_file.py +++ b/src/core/zowe/core_for_zowe_sdk/config_file.py @@ -383,7 +383,7 @@ def find_profile(self, path: str, profiles: dict[str, Any]) -> Optional[dict[str for k, v in profiles.items(): if not isinstance(v, dict): # Ensure v is a dictionary if not self.__suppress_config_file_warnings: - self.__logger.warning("Invalid profile passed when schame validation is off") + self.__logger.warning("Invalid profile passed when schema validation is off") continue # Skip invalid entries if segments[0] == k: @@ -443,20 +443,10 @@ def __load_secure_properties(self) -> None: secure_props = CredentialManager.secure_props.get(self.filepath or "", {}) for key, value in secure_props.items(): segments = [name for i, name in enumerate(key.split(".")) if i % 2 == 1] - profiles_obj = self.profiles property_name = segments.pop() - for i, profile_name in enumerate(segments): - if profiles_obj is None or not isinstance(profiles_obj, dict): - break - if profile_name in profiles_obj: - profiles_obj = profiles_obj[profile_name] - if not isinstance(profiles_obj, dict): - break - if i == len(segments) - 1: - profiles_obj.setdefault("properties", {}) - profiles_obj["properties"][property_name] = value - else: - break + profile = self.find_profile(".".join(segments), self.profiles) + if profile is not None: + profile.setdefault("properties", {})[property_name] = value def __extract_secure_properties( self, profiles_obj: dict[str, Any], json_path: Optional[str] = "profiles" diff --git a/tests/unit/core/test_profile_manager.py b/tests/unit/core/test_profile_manager.py index 1e797a43..9c763ce1 100644 --- a/tests/unit/core/test_profile_manager.py +++ b/tests/unit/core/test_profile_manager.py @@ -182,6 +182,58 @@ def test_custom_file_and_custom_profile_loading_with_nested_profile(self, get_pa } self.assertEqual(props, expected_props) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_nested_profile_with_secure_properties_on_child(self, get_pass_func): + """ + Test that secure properties declared on a child profile nested under a + parent (e.g. mainframe.zosmf) are correctly loaded from the vault. + + Regression test: __load_secure_properties previously looked for the + child profile name as a direct key of the parent profile dict, but + nested profiles actually live one level deeper under the parent's + own "profiles" key, so secure user/password values were silently + never injected for any profile nested more than one level deep. + """ + custom_file_path = os.path.join(self.custom_dir, self.custom_filename) + config_contents = { + "$schema": "./zowe.schema.json", + "profiles": { + "mainframe": { + "properties": {"host": "example.com"}, + "profiles": { + "zosmf": { + "type": "zosmf", + "properties": {"port": 1443}, + "secure": ["user", "password"], + } + }, + } + }, + "defaults": {"zosmf": "mainframe.zosmf"}, + } + with open(custom_file_path, "w") as f: + json.dump(config_contents, f) + + self.setUpCreds( + custom_file_path, + { + "profiles.mainframe.profiles.zosmf.properties.user": "admin", + "profiles.mainframe.profiles.zosmf.properties.password": "secret", + }, + ) + + prof_manager = ProfileManager(appname=self.custom_appname) + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load(profile_name="mainframe.zosmf", validate_schema=False) + + expected_props = { + "host": "example.com", + "port": 1443, + "user": "admin", + "password": "secret", + } + self.assertEqual(props, expected_props) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_profile_loading_with_user_overridden_properties(self, get_pass_func): """