Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save Profile Properties to Config File & Secure Vault Storage (#73, #72) #201

Merged
merged 33 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3f5ee4c
added methods for saving profile properties to profile_manager
samadpls Jul 21, 2023
ff47822
updated the logic of `set_property`
samadpls Jul 24, 2023
d60cdca
updated the logic to save the profile properties to the config file
samadpls Jul 27, 2023
4cd58a3
added a is_secure logic to the config_file.py
samadpls Jul 27, 2023
82e3d46
working on it
samadpls Jul 27, 2023
97c9e2c
updated the set_profile_property method to update the profile property
samadpls Jul 27, 2023
028b0bc
refactor: set profile properties in config file
samadpls Jul 28, 2023
8b0c155
working on save profile prop
samadpls Jul 29, 2023
3395018
refactored `set_property()` and added `save()` on both profile and co…
samadpls Jul 30, 2023
4ed9126
updated
samadpls Jul 31, 2023
584da0e
added `set_profile` method to ProfileManager & ConfigFile class
samadpls Jul 31, 2023
83d8d58
updated the config file
samadpls Aug 3, 2023
0e9ca9c
Merge branch 'multi-credentials' into save-profile-prop
t1m0thyj Aug 11, 2023
f79f447
Added unit tests for profile_manager `set_property`, `save`, and `ge…
samadpls Aug 14, 2023
4b885ee
working on nested profile logic
samadpls Aug 15, 2023
3ce1c93
added nested logic in get_highest_priority for handling profiles not …
samadpls Aug 16, 2023
0cf156e
updated the test cases
samadpls Aug 17, 2023
4f7e483
Merge branch 'multi-credentials' into save-profile-prop
samadpls Aug 17, 2023
13dff17
updated save() method
samadpls Aug 17, 2023
b02d437
Implemented and Validated Nested and Single Profile Functionality
samadpls Aug 18, 2023
5721d09
typo fixed
samadpls Aug 19, 2023
4058b07
added unit test of config_file methods
samadpls Aug 21, 2023
edf6eef
fixed typo
samadpls Aug 23, 2023
2627db3
refactored code and addressed issue
samadpls Aug 23, 2023
b13bad3
fixed typo
samadpls Aug 23, 2023
46ac09d
Updated tests and added load_secure_props to set_property & set_profi…
samadpls Aug 24, 2023
00c9349
Merge branch 'main' into save-profile-prop
zFernand0 Aug 29, 2023
e20f624
Merge branch 'main' into save-profile-prop
samadpls Aug 30, 2023
ffc50b1
updated set_proprty
samadpls Aug 30, 2023
08f2897
updated CHANGELOG.md file
samadpls Sep 1, 2023
0f27841
refactored the code, and added error handling support
samadpls Sep 14, 2023
8c64d72
Merge branch 'main' into save-profile-prop
samadpls Sep 14, 2023
858c66e
Merge branch 'main' into save-profile-prop
t1m0thyj Sep 27, 2023
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to the Zowe Client Python SDK will be documented in this fil

- Feature: Added method to load profile properties from environment variables
- Feature: Added a CredentialManager class to securely retrieve values from credentials and manage multiple credential entries on Windows [#134](https://github.com/zowe/zowe-client-python-sdk/issues/134)
- Feature: Added method to Save profile properties to zowe.config.json file [#73](https://github.com/zowe/zowe-client-python-sdk/issues/73)
- Feature: Added method to Save secure profile properties to vault [#72](https://github.com/zowe/zowe-client-python-sdk/issues/72)
- Bugfix: Fixed issue for datasets and jobs with special characters in URL [#211] (https://github.com/zowe/zowe-client-python-sdk/issues/211)
- Bugfix: Fixed exception handling in session.py [#213] (https://github.com/zowe/zowe-client-python-sdk/issues/213)
- BugFix: Validation of zowe.config.json file matching the schema [#192](https://github.com/zowe/zowe-client-python-sdk/issues/192)
161 changes: 151 additions & 10 deletions src/core/zowe/core_for_zowe_sdk/config_file.py
traeok marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@

# Profile datatype is used by ConfigFile to return Profile Data along with
# metadata such as profile_name and secure_props_not_found


class Profile(NamedTuple):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the autodiscover_config_dir function, do we want to skip over zowe.config.user.json if it does not exist? It is optional, so we should avoid throwing exceptions for a missing user-level config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be resolved by implementing a try-except block while invoking the function.

data: dict = {}
name: str = ""
Expand Down Expand Up @@ -135,7 +137,7 @@
# loading secure props is done in load_profile_properties
# since we want to try loading secure properties only when
# we know that the profile has saved properties
# self.load_secure_props()
# CredentialManager.load_secure_props()

def validate_schema(
self
Expand Down Expand Up @@ -230,7 +232,6 @@
profile_name = self.get_profilename_from_profiletype(
profile_type=profile_type
)

props: dict = self.load_profile_properties(profile_name=profile_name)

return Profile(props, profile_name, self._missing_secure_props)
Expand Down Expand Up @@ -334,8 +335,6 @@
Load exact profile properties (without prepopulated fields from base profile)
from the profile dict and populate fields from the secure credentials storage
"""
# if self.profiles is None:
# self.init_from_file()
props = {}
lst = profile_name.split(".")
secure_fields: list = []
Expand All @@ -344,16 +343,15 @@
profile_name = ".".join(lst)
profile = self.find_profile(profile_name, self.profiles)
if profile is not None:
props = { **profile.get("properties", {}), **props }
props = {**profile.get("properties", {}), **props}
secure_fields.extend(profile.get("secure", []))
else:
warnings.warn(
f"Profile {profile_name} not found",
ProfileNotFoundWarning
)
f"Profile {profile_name} not found",
ProfileNotFoundWarning
)
lst.pop()


# load secure props only if there are secure fields
if secure_fields:
CredentialManager.load_secure_props()
Expand All @@ -363,7 +361,7 @@
if re.match(
"profiles\\." + profile_name + "\\.properties\\.[a-z]+", key
):
property_name = key.split(".")[3]
property_name = key.split(".")[-1]
if property_name in secure_fields:
props[property_name] = value
secure_fields.remove(property_name)
Expand All @@ -372,3 +370,146 @@
# self._missing_secure_props.extend(secure_fields)

return props

def __set_or_create_nested_profile(self, profile_name, profile_data):
"""
Set or create a nested profile.
"""
path = self.get_profile_path_from_name(profile_name)
keys = path.split(".")[1:]
nested_profiles = self.profiles
for key in keys:
nested_profiles = nested_profiles.setdefault(key, {})
nested_profiles.update(profile_data)

def __is_secure(self, json_path: str, property_name: str) -> bool:
"""
Check whether the given JSON path corresponds to a secure property.

Parameters:
json_path (str): The JSON path of the property to check.
property_name (str): The name of the property to check.

Returns:
bool: True if the property should be stored securely, False otherwise.
"""

profile = self.find_profile(json_path, self.profiles)
if profile and profile.get("secure"):
return property_name in profile["secure"]
return False

Check warning on line 400 in src/core/zowe/core_for_zowe_sdk/config_file.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/config_file.py#L400

Added line #L400 was not covered by tests

def set_property(self, json_path, value, secure=None) -> None:
"""
Set a property in the profile, storing it securely if necessary.

Parameters:
json_path (str): The JSON path of the property to set.
value (str): The value to be set for the property.
profile_name (str): The name of the profile to set the property in.
secure (bool): If True, the property will be stored securely. Default is None.
"""
if self.profiles is None:
self.init_from_file()

Check warning on line 413 in src/core/zowe/core_for_zowe_sdk/config_file.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/config_file.py#L413

Added line #L413 was not covered by tests

# Checking whether the property should be stored securely or in plain text
property_name = json_path.split(".")[-1]
profile_name = self.get_profile_name_from_path(json_path)
# check if the property is already secure
is_property_secure = self.__is_secure(profile_name, property_name)
is_secure = secure if secure is not None else is_property_secure

current_profile = self.find_profile(profile_name, self.profiles) or {}
current_properties = current_profile.setdefault("properties", {})
current_secure = current_profile.setdefault("secure", [])
if is_secure:
CredentialManager.load_secure_props()
if not is_property_secure:
current_secure.append(property_name)

CredentialManager.secure_props[self.filepath] = {
**CredentialManager.secure_props.get(self.filepath, {}), json_path: value}
current_properties.pop(property_name, None)

else:
if is_property_secure:
CredentialManager.secure_props[self.filepath].pop(json_path,None)
current_secure.remove(property_name)
current_properties[property_name] = value

Check warning on line 438 in src/core/zowe/core_for_zowe_sdk/config_file.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/config_file.py#L435-L438

Added lines #L435 - L438 were not covered by tests

current_profile["properties"] = current_properties
current_profile["secure"] = current_secure
self.__set_or_create_nested_profile(profile_name, current_profile)

def set_profile(self, profile_path: str, profile_data: dict) -> None:
"""
Set a profile in the config file.

Parameters:
profile_path (str): The path of the profile to be set. eg: profiles.zosmf
profile_data (dict): The data to be set for the profile.
"""
if self.profiles is None:
self.init_from_file()

Check warning on line 453 in src/core/zowe/core_for_zowe_sdk/config_file.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/config_file.py#L453

Added line #L453 was not covered by tests
profile_name = self.get_profile_name_from_path(profile_path)
if "secure" in profile_data:
# Checking if the profile has a 'secure' field with values
secure_fields = profile_data["secure"]
current_profile = self.find_profile(profile_name,self.profiles) or {}
existing_secure_fields = current_profile.get("secure", [])
new_secure_fields = [field for field in secure_fields if field not in existing_secure_fields]

# JSON paths for new secure properties and store their values in CredentialManager.secure_props
CredentialManager.load_secure_props()
CredentialManager.secure_props[self.filepath] = {}
for field in new_secure_fields:
json_path = f"{profile_path}.properties.{field}"
profile_value = profile_data["properties"][field]
CredentialManager.secure_props[self.filepath][json_path] = profile_value
# Updating the 'secure' field of the profile with the combined list of secure fields
profile_data["secure"] = existing_secure_fields + new_secure_fields
# If a field is provided in the 'secure' list and its value exists in 'profile_data', remove it
profile_data["properties"] = {
field: value
for field, value in profile_data.get("properties", {}).items()
if field not in profile_data["secure"]
}
self.__set_or_create_nested_profile(profile_name, profile_data)


def save(self, secure_props=True):
"""
Save the config file to disk. and secure props to vault
parameters:
secure_props (bool): If True, the secure properties will be stored in the vault. Default is False.
Returns:
None
"""
# Updating the config file with any changes
if self.profiles is None:
try:
self.init_from_file()
except FileNotFoundError:
pass

Check warning on line 493 in src/core/zowe/core_for_zowe_sdk/config_file.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/config_file.py#L490-L493

Added lines #L490 - L493 were not covered by tests

elif any(self.profiles.values()):
with open(self.filepath, 'w') as file:
self.jsonc["profiles"] = self.profiles
commentjson.dump(self.jsonc, file, indent=4)
if secure_props:
CredentialManager.save_secure_props()


def get_profile_name_from_path(self, path: str) -> str:
"""
Get the name of the profile from the given path.
"""
segments = path.split(".")
profile_name = ".".join(segments[i] for i in range(1, len(segments), 2) if segments[i - 1] != "properties")
return profile_name

def get_profile_path_from_name(self, short_path: str) -> str:
"""
Get the path of the profile from the given name.
"""
return re.sub(r'(^|\.)', r'\1profiles.', short_path)
101 changes: 99 additions & 2 deletions src/core/zowe/core_for_zowe_sdk/profile_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Optional

from .config_file import ConfigFile, Profile
from .credential_manager import CredentialManager
from .custom_warnings import (
ConfigNotFoundWarning,
ProfileNotFoundWarning,
Expand Down Expand Up @@ -55,7 +56,8 @@
self.project_config = ConfigFile(type=TEAM_CONFIG, name=appname)
self.project_user_config = ConfigFile(type=USER_CONFIG, name=appname)

self.global_config = ConfigFile(type=TEAM_CONFIG, name=GLOBAL_CONFIG_NAME)
self.global_config = ConfigFile(
type=TEAM_CONFIG, name=GLOBAL_CONFIG_NAME)
try:
self.global_config.location = GLOBAl_CONFIG_LOCATION
except Exception:
Expand All @@ -64,7 +66,8 @@
ConfigNotFoundWarning,
)

self.global_user_config = ConfigFile(type=USER_CONFIG, name=GLOBAL_CONFIG_NAME)
self.global_user_config = ConfigFile(
type=USER_CONFIG, name=GLOBAL_CONFIG_NAME)
try:
self.global_user_config.location = GLOBAl_CONFIG_LOCATION
except Exception:
Expand Down Expand Up @@ -325,3 +328,97 @@
profile_props[k] = env_var[k]

return profile_props

def get_highest_priority_layer(self, json_path: str) -> Optional[ConfigFile]:
"""
Get the highest priority layer (configuration file) based on the given profile name

Parameters:
profile_name (str): The name of the profile to look for in the layers.

Returns:
Optional[ConfigFile]: The highest priority layer (configuration file) that contains the specified profile,
or None if the profile is not found in any layer.
"""
highest_layer = None
longest_match = ""
layers = [
self.project_user_config,
self.project_config,
self.global_user_config,
self.global_config
]

original_name = layers[0].get_profile_name_from_path(json_path)

for layer in layers:
try:
layer.init_from_file()
except FileNotFoundError:
continue

Check warning on line 358 in src/core/zowe/core_for_zowe_sdk/profile_manager.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/profile_manager.py#L357-L358

Added lines #L357 - L358 were not covered by tests
parts = original_name.split(".")
current_name = ""

while parts:
current_name = ".".join(parts)
profile = layer.find_profile(current_name, layer.profiles)

if profile is not None and len(current_name) > len(longest_match):
highest_layer = layer
longest_match = current_name

else:
parts.pop()
if original_name == longest_match:
break

if highest_layer is None:
highest_layer = layer

Check warning on line 376 in src/core/zowe/core_for_zowe_sdk/profile_manager.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/profile_manager.py#L375-L376

Added lines #L375 - L376 were not covered by tests

if highest_layer is None:
raise FileNotFoundError(f"Could not find a valid layer for {json_path}")

Check warning on line 379 in src/core/zowe/core_for_zowe_sdk/profile_manager.py

View check run for this annotation

Codecov / codecov/patch

src/core/zowe/core_for_zowe_sdk/profile_manager.py#L379

Added line #L379 was not covered by tests

return highest_layer


def set_property(self, json_path, value, secure=None) -> None:
"""
Set a property in the profile, storing it securely if necessary.

Parameters:
json_path (str): The JSON path of the property to set.
value (str): The value to be set for the property.
secure (bool): If True, the property will be stored securely. Default is None.
"""

# highest priority layer for the given profile name
highest_priority_layer = self.get_highest_priority_layer(json_path)

# Set the property in the highest priority layer

highest_priority_layer.set_property(json_path, value, secure=secure)

def set_profile(self, profile_path: str, profile_data: dict) -> None:
"""
Set a profile in the highest priority layer (configuration file) based on the given profile name

Parameters:
profile_path (str): TThe path of the profile to be set. eg: profiles.zosmf
profile_data (dict): The data of the profile to set.
"""
highest_priority_layer = self.get_highest_priority_layer(profile_path)

highest_priority_layer.set_profile(profile_path, profile_data)

def save(self) -> None:
"""
Save the layers (configuration files) to disk.
"""
layers = [self.project_user_config,
self.project_config,
self.global_user_config,
self.global_config]

for layer in layers:
layer.save(False)
CredentialManager.save_secure_props()
Loading