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 22 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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ All notable changes to the Zowe Client Python SDK will be documented in this fil
## Recent Changes

- 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 load profile properties from environment variables
- Feature: Added method to load profile properties from environment variables
- 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)
samadpls marked this conversation as resolved.
Show resolved Hide resolved
163 changes: 152 additions & 11 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 @@ -51,6 +51,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 @@ -130,12 +132,13 @@ def init_from_file(self) -> None:

self.profiles = profile_jsonc.get("profiles", {})
self.defaults = profile_jsonc.get("defaults", {})
self.jsonc = profile_jsonc
self.schema_property = profile_jsonc.get("$schema", None)

# 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 schema_list(
self,
Expand Down Expand Up @@ -206,7 +209,6 @@ def get_profile(
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 @@ -279,7 +281,7 @@ def get_profilename_from_profiletype(self, profile_type: str) -> str:
profile_name=profile_type,
error_msg=f"No profile with matching profile_type '{profile_type}' found",
)

def find_profile(self, path: str, profiles: dict):
"""
Find a profile at a specified location from within a set of nested profiles
Expand Down Expand Up @@ -310,8 +312,6 @@ def load_profile_properties(self, profile_name: str) -> dict:
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 @@ -320,22 +320,21 @@ def load_profile_properties(self, profile_name: str) -> dict:
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()
self.secure_props=CredentialManager.secure_props.get(self.filepath, {})
# load properties with key as profile.{profile_name}.properties.{*}
for (key, value) in self.secure_props.items():
for (key, value) in CredentialManager.secure_props.items():
samadpls marked this conversation as resolved.
Show resolved Hide resolved
if re.match(
"profiles\\." + profile_name + "\\.properties\\.[a-z]+", key
):
Expand All @@ -348,3 +347,145 @@ def load_profile_properties(self, profile_name: str) -> dict:
# 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

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()

# 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:
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:
current_secure.remove(property_name)
current_properties[property_name] = value

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()
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
for field in new_secure_fields:
json_path = f"{profile_path}.properties.{field}.value"
CredentialManager.secure_props[self.filepath] = {
**CredentialManager.secure_props.get(self.filepath, {}),
json_path: profile_data["properties"][field]
}
samadpls marked this conversation as resolved.
Show resolved Hide resolved

# 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
"""
# Update the config file with any changes
if self.profiles is None:
self.init_from_file()
samadpls marked this conversation as resolved.
Show resolved Hide resolved
elif any(self.profiles.values()):
with open(self.filepath, 'w') as file:
# Update the profiles in the JSON data
self.jsonc["profiles"] = self.profiles
file.seek(0) # Move the file pointer to the beginning of the file
commentjson.dump(self.jsonc, file, indent=4)
file.truncate() # Truncate the file to the current file pointer position
samadpls marked this conversation as resolved.
Show resolved Hide resolved
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)
97 changes: 95 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 @@ -16,6 +16,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 @@ -54,7 +55,8 @@ def __init__(self, appname: str = "zowe", show_warnings: bool = True):
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 @@ -63,7 +65,8 @@ def __init__(self, appname: str = "zowe", show_warnings: bool = True):
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 @@ -301,3 +304,93 @@ def load(
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)

if not self._show_warnings:
warnings.simplefilter("ignore")

for layer in layers:
layer.init_from_file()
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
warnings.resetwarnings()
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)
# If the profile doesn't exist in any layer, use the project user config as the default location
if not highest_priority_layer:
highest_priority_layer = self.project_user_config

# 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)

if not highest_priority_layer:
highest_priority_layer = self.project_user_config
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