Skip to content

Commit

Permalink
Merge branch 'main' into save-profile-prop
Browse files Browse the repository at this point in the history
Signed-off-by: Timothy Johnson <[email protected]>
  • Loading branch information
t1m0thyj committed Sep 27, 2023
2 parents 8c64d72 + 18c9385 commit 858c66e
Show file tree
Hide file tree
Showing 11 changed files with 740 additions and 76 deletions.
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ All notable changes to the Zowe Client Python SDK will be documented in this fil

## Recent Changes

- Bugfix: Fixed issue for datasets and jobs with special characters in URL [#211] (https://github.com/zowe/zowe-client-python-sdk/issues/211)


- 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 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)
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ coverage==5.4
flake8==3.8.4
idna==2.10
importlib-metadata==3.6.0
jsonschema==4.14.0
jsonschema==4.17.3
keyring
lxml==4.9.3
mccabe==0.6.1
Expand Down
58 changes: 45 additions & 13 deletions src/core/zowe/core_for_zowe_sdk/config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import commentjson

from .constants import constants
from .validators import validate_config_json
from .credential_manager import CredentialManager
from .custom_warnings import (
ProfileNotFoundWarning,
Expand Down Expand Up @@ -74,8 +76,9 @@ class ConfigFile:
_location: Optional[str] = None
profiles: Optional[dict] = None
defaults: Optional[dict] = None
secure_props: Optional[dict] = None
schema_property: Optional[dict] = None
secure_props: Optional[dict] = None
jsonc: Optional[dict] = None
_missing_secure_props: list = field(default_factory=list)

@property
Expand All @@ -101,7 +104,7 @@ def location(self) -> Optional[str]:

@property
def schema_path(self) -> Optional[str]:
self.schema_property
return self.schema_property

@location.setter
def location(self, dirname: str) -> None:
Expand All @@ -110,7 +113,10 @@ def location(self, dirname: str) -> None:
else:
raise FileNotFoundError(f"given path {dirname} is not valid")

def init_from_file(self) -> None:
def init_from_file(
self,
validate_schema: Optional[bool] = True,
) -> None:
"""
Initializes the class variable after
setting filepath (or if not set, autodiscover the file)
Expand All @@ -122,26 +128,51 @@ def init_from_file(self) -> None:
profile_jsonc = commentjson.load(fileobj)

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

if self.schema_property and validate_schema:
self.validate_schema()
# 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
# CredentialManager.load_secure_props()

def validate_schema(
self
) -> None:
"""
Get the $schema_property from the config and load the schema
Returns
-------
file_path to the $schema property
"""

path_schema_json = None

path_schema_json = self.schema_path
if path_schema_json is None: # check if the $schema property is not defined
warnings.warn(
f"$schema property could not found"
)

# validate the $schema property
if path_schema_json:
validate_config_json(self.jsonc, path_schema_json, cwd = self.location)

def schema_list(
self,
) -> list:
"""
Loads the schema properties
in a sorted order according to the priority
Returns
-------
Dictionary
Returns the profile properties from schema (prop: value)
"""

Expand All @@ -152,23 +183,23 @@ def schema_list(
if schema.startswith("https://") or schema.startswith("http://"):
schema_json = requests.get(schema).json()

elif os.path.isfile(schema) or schema.startswith("file://"):
with open(schema.replace("file://", "")) as f:
schema_json = json.load(f)

elif not os.path.isabs(schema):
schema = os.path.join(self.location, schema)
with open(schema) as f:
schema_json = json.load(f)

elif os.path.isfile(schema):
with open(schema) as f:
schema_json = json.load(f)
else:
return []

profile_props:dict = {}
schema_json = dict(schema_json)

for props in schema_json['properties']['profiles']['patternProperties']["^\\S*$"]["allOf"]:
props = props["then"]

while "properties" in props:
props = props.pop("properties")
profile_props = props
Expand All @@ -179,6 +210,7 @@ def get_profile(
self,
profile_name: Optional[str] = None,
profile_type: Optional[str] = None,
validate_schema: Optional[bool] = True,
) -> Profile:
"""
Load given profile including secure properties and excluding values from base profile
Expand All @@ -188,7 +220,7 @@ def get_profile(
Returns a namedtuple called Profile
"""
if self.profiles is None:
self.init_from_file()
self.init_from_file(validate_schema)

if profile_name is None and profile_type is None:
raise ProfileNotFound(
Expand Down
40 changes: 20 additions & 20 deletions src/core/zowe/core_for_zowe_sdk/credential_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
Copyright Contributors to the Zowe Project.
"""
import sys
import warnings
import base64
import warnings
import base64
import logging
from typing import Optional
import commentjson
Expand All @@ -30,7 +30,7 @@
class CredentialManager:
secure_props = {}



@staticmethod
def load_secure_props() -> None:
Expand All @@ -51,7 +51,7 @@ def load_secure_props() -> None:
secret_value = CredentialManager._retrieve_credential(service_name)
# Handle the case when secret_value is None
if secret_value is None:
return
return

except Exception as exc:
raise SecureProfileLoadFailed(
Expand All @@ -63,9 +63,9 @@ def load_secure_props() -> None:
secure_config_json = commentjson.loads(base64.b64decode(secure_config).decode())
# update the secure props
CredentialManager.secure_props = secure_config_json
@staticmethod


@staticmethod
def _retrieve_credential(service_name: str) -> Optional[str]:
"""
Retrieve the credential from the keyring or storage.
Expand Down Expand Up @@ -96,7 +96,7 @@ def _retrieve_credential(service_name: str) -> Optional[str]:
encoded_credential += temp_value
index += 1
temp_value = keyring.get_password(f"{service_name}-{index}", f"{constants['ZoweAccountName']}-{index}")

if is_win32:
try:
encoded_credential = encoded_credential.encode('utf-16le').decode()
Expand All @@ -106,10 +106,10 @@ def _retrieve_credential(service_name: str) -> Optional[str]:

if encoded_credential is not None and encoded_credential.endswith("\0"):
encoded_credential = encoded_credential[:-1]
return encoded_credential

return encoded_credential


@staticmethod
def delete_credential(service_name: str, account_name: str) -> None:
"""
Expand All @@ -125,7 +125,7 @@ def delete_credential(service_name: str, account_name: str) -> None:
-------
None
"""

try:
keyring.delete_password(service_name, account_name)
except keyring.errors.PasswordDeleteError:
Expand All @@ -143,7 +143,7 @@ def delete_credential(service_name: str, account_name: str) -> None:
break
index += 1


@staticmethod
def save_secure_props()-> None:
"""
Expand All @@ -154,16 +154,16 @@ def save_secure_props()-> None:
"""
if not HAS_KEYRING:
return

service_name = constants["ZoweServiceName"]
credential = CredentialManager.secure_props
# Check if credential is a non-empty string
if credential:
is_win32 = sys.platform == "win32"
encoded_credential = base64.b64encode(commentjson.dumps(credential).encode()).decode()

encoded_credential = base64.b64encode(commentjson.dumps(credential).encode()).decode()
if is_win32:
service_name += "/" + constants["ZoweAccountName"]
service_name += "/" + constants["ZoweAccountName"]
# Delete the existing credential
CredentialManager.delete_credential(service_name , constants["ZoweAccountName"])
# Check if the encoded credential exceeds the maximum length for win32
Expand All @@ -177,9 +177,9 @@ def save_secure_props()-> None:
password=(chunk + '\0' *(len(chunk)%2)).encode().decode('utf-16le')
field_name = f"{constants['ZoweAccountName']}-{index}"
keyring.set_password(f"{service_name}-{index}", field_name, password)

else:
# Credential length is within the maximum limit or not on win32, set it as a single keyring entry
keyring.set_password(
service_name, constants["ZoweAccountName"],
service_name, constants["ZoweAccountName"],
encoded_credential)
Loading

0 comments on commit 858c66e

Please sign in to comment.