From 4c919808d5fc18b4ab5f29f5c918ab0939db4105 Mon Sep 17 00:00:00 2001 From: ATorrise Date: Fri, 7 Aug 2026 14:04:51 -0400 Subject: [PATCH 1/5] allowing secure creds for nested profiles Signed-off-by: ATorrise --- src/core/zowe/core_for_zowe_sdk/config_file.py | 2 ++ 1 file changed, 2 insertions(+) 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..a831bb06 100644 --- a/src/core/zowe/core_for_zowe_sdk/config_file.py +++ b/src/core/zowe/core_for_zowe_sdk/config_file.py @@ -455,6 +455,8 @@ def __load_secure_properties(self) -> None: if i == len(segments) - 1: profiles_obj.setdefault("properties", {}) profiles_obj["properties"][property_name] = value + else: + profiles_obj = profiles_obj.get("profiles", {}) else: break From 0238ee979befef1c5f39d8a2bf1c39a2a49f6f73 Mon Sep 17 00:00:00 2001 From: ATorrise Date: Fri, 7 Aug 2026 14:33:40 -0400 Subject: [PATCH 2/5] test Signed-off-by: ATorrise --- tests/unit/core/test_profile_manager.py | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) 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): """ From a411a6a41fa3d071e2768439a2a37d6dadd38dde Mon Sep 17 00:00:00 2001 From: ATorrise Date: Fri, 7 Aug 2026 14:40:37 -0400 Subject: [PATCH 3/5] changelog Signed-off-by: ATorrise --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d579cf23..6128c710 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) From c9f60d1d44cb3d13341713f07d956ea9f72203cf Mon Sep 17 00:00:00 2001 From: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:33:02 -0400 Subject: [PATCH 4/5] chore(feedback): Address PR feedback - Reduced function complexity, and reuse the `find_profile()` function Co-Authored-by: Claude w/ Model: Sonnet-5/high Modified AI generated code: YES Dev Name: Fernando Rijo Cedeno, Reviewed by: Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> --- .../zowe/core_for_zowe_sdk/config_file.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) 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 a831bb06..b891cdbe 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: @@ -441,24 +441,14 @@ def load_profile_properties(self, profile_name: str) -> dict[str, Any]: def __load_secure_properties(self) -> None: """Inject secure properties that have been loaded from the vault into the profiles object.""" secure_props = CredentialManager.secure_props.get(self.filepath or "", {}) + if self.profiles is None: + return 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: - profiles_obj = profiles_obj.get("profiles", {}) - 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" From 8c1bed45990a060db937e683afb279dcd4fac1ae Mon Sep 17 00:00:00 2001 From: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:49:18 -0400 Subject: [PATCH 5/5] chore: remove redundant check Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> --- src/core/zowe/core_for_zowe_sdk/config_file.py | 2 -- 1 file changed, 2 deletions(-) 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 b891cdbe..ef462127 100644 --- a/src/core/zowe/core_for_zowe_sdk/config_file.py +++ b/src/core/zowe/core_for_zowe_sdk/config_file.py @@ -441,8 +441,6 @@ def load_profile_properties(self, profile_name: str) -> dict[str, Any]: def __load_secure_properties(self) -> None: """Inject secure properties that have been loaded from the vault into the profiles object.""" secure_props = CredentialManager.secure_props.get(self.filepath or "", {}) - if self.profiles is None: - return for key, value in secure_props.items(): segments = [name for i, name in enumerate(key.split(".")) if i % 2 == 1] property_name = segments.pop()