diff --git a/CHANGELOG.md b/CHANGELOG.md index 410ac4bb..ef48d36a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/requirements.txt b/requirements.txt index b904d1a4..238206c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 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 a67440ee..265a08ae 100644 --- a/src/core/zowe/core_for_zowe_sdk/config_file.py +++ b/src/core/zowe/core_for_zowe_sdk/config_file.py @@ -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, @@ -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 @@ -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: @@ -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) @@ -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) """ @@ -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 @@ -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 @@ -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( diff --git a/src/core/zowe/core_for_zowe_sdk/credential_manager.py b/src/core/zowe/core_for_zowe_sdk/credential_manager.py index 656878c3..de9de637 100644 --- a/src/core/zowe/core_for_zowe_sdk/credential_manager.py +++ b/src/core/zowe/core_for_zowe_sdk/credential_manager.py @@ -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 @@ -30,7 +30,7 @@ class CredentialManager: secure_props = {} - + @staticmethod def load_secure_props() -> None: @@ -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( @@ -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. @@ -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() @@ -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: """ @@ -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: @@ -143,7 +143,7 @@ def delete_credential(service_name: str, account_name: str) -> None: break index += 1 - + @staticmethod def save_secure_props()-> None: """ @@ -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 @@ -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) \ No newline at end of file diff --git a/src/core/zowe/core_for_zowe_sdk/profile_manager.py b/src/core/zowe/core_for_zowe_sdk/profile_manager.py index 43c8db3b..fc57c364 100644 --- a/src/core/zowe/core_for_zowe_sdk/profile_manager.py +++ b/src/core/zowe/core_for_zowe_sdk/profile_manager.py @@ -13,6 +13,7 @@ import os.path import os import warnings +import jsonschema from typing import Optional from .config_file import ConfigFile, Profile @@ -119,24 +120,24 @@ def get_env( ) -> dict: """ Maps the env variables to the profile properties - + Returns ------- Dictionary Containing profile properties from env variables (prop: value) """ - + props = cfg.schema_list() if props == []: return {} - + env, env_var = {}, {} - + for var in list(os.environ.keys()): if var.startswith("ZOWE_OPT"): env[var[len("ZOWE_OPT_"):].lower()] = os.environ.get(var) - + for k, v in env.items(): word = k.split("_") @@ -156,13 +157,14 @@ def get_env( env_var[k] = bool(v) return env_var - + @staticmethod def get_profile( cfg: ConfigFile, profile_name: Optional[str], profile_type: Optional[str], - config_type: str, + config_type: Optional[str], + validate_schema: Optional[bool] = True, ) -> Profile: """ Get just the profile from the config file (overriden with base props in the config file) @@ -177,7 +179,27 @@ def get_profile( cfg_profile = Profile() try: cfg_profile = cfg.get_profile( - profile_name=profile_name, profile_type=profile_type + profile_name=profile_name, profile_type=profile_type, validate_schema=validate_schema + ) + except jsonschema.exceptions.ValidationError as exc: + raise jsonschema.exceptions.ValidationError( + f"Instance was invalid under the provided $schema property, {exc}" + ) + except jsonschema.exceptions.SchemaError as exc: + raise jsonschema.exceptions.SchemaError( + f"The provided schema is invalid, {exc}" + ) + except jsonschema.exceptions.UndefinedTypeCheck as exc: + raise jsonschema.exceptions.UndefinedTypeCheck( + f"A type checker was asked to check a type it did not have registered, {exc}" + ) + except jsonschema.exceptions.UnknownType as exc: + raise jsonschema.exceptions.UnknownType( + f"Unknown type is found in schema_json, exc" + ) + except jsonschema.exceptions.FormatError as exc: + raise jsonschema.exceptions.FormatError( + f"Validating a format config_json failed for schema_json, {exc}" ) except ProfileNotFound: if profile_name: @@ -215,14 +237,15 @@ def get_profile( f"because {type(exc).__name__}'{exc}'.", ConfigNotFoundWarning, ) - finally: - return cfg_profile + + return cfg_profile def load( self, profile_name: Optional[str] = None, profile_type: Optional[str] = None, check_missing_props: bool = True, + validate_schema: Optional[bool] = True, override_with_env: Optional[bool] = False, ) -> dict: """Load connection details from a team config profile. @@ -258,13 +281,14 @@ def load( "Global Config": self.global_config, } profile_props: dict = {} + schema_path = None env_var: dict = {} missing_secure_props = [] # track which secure props were not loaded for i, (config_type, cfg) in enumerate(config_layers.items()): profile_loaded = self.get_profile( - cfg, profile_name, profile_type, config_type + cfg, profile_name, profile_type, config_type, validate_schema ) # TODO Why don't user and password show up here for Project User Config? # Probably need to update load_profile_properties method in config_file.py diff --git a/src/core/zowe/core_for_zowe_sdk/session.py b/src/core/zowe/core_for_zowe_sdk/session.py index 5d5cc6cb..5b85f652 100644 --- a/src/core/zowe/core_for_zowe_sdk/session.py +++ b/src/core/zowe/core_for_zowe_sdk/session.py @@ -45,7 +45,7 @@ def __init__(self, props: dict) -> None: if props.get("host") is not None: self.session: ISession = ISession(host=props.get("host")) else: - raise "Host must be supplied" + raise Exception("Host must be supplied") # determine authentication type if props.get("user") is not None and props.get("password") is not None: @@ -61,7 +61,7 @@ def __init__(self, props: dict) -> None: self.session.tokenValue = props.get("tokenValue") self.session.type = session_constants.AUTH_TYPE_BEARER else: - raise "An authentication method must be supplied" + raise Exception("An authentication method must be supplied") # set additional parameters self.session.basePath = props.get("basePath") diff --git a/src/core/zowe/core_for_zowe_sdk/validators.py b/src/core/zowe/core_for_zowe_sdk/validators.py index c1a11719..7d0a1551 100644 --- a/src/core/zowe/core_for_zowe_sdk/validators.py +++ b/src/core/zowe/core_for_zowe_sdk/validators.py @@ -12,9 +12,12 @@ import commentjson from jsonschema import validate +import os +from typing import Union, Optional +import requests -def validate_config_json(path_config_json: str, path_schema_json: str): +def validate_config_json(path_config_json: Union[str, dict], path_schema_json: str, cwd: str): """ Function validating that zowe.config.json file matches zowe.schema.json. @@ -31,10 +34,29 @@ def validate_config_json(path_config_json: str, path_schema_json: str): Provides details if config.json doesn't match schema.json, otherwise it returns None. """ - with open(path_config_json) as file: - config_json = commentjson.load(file) - - with open(path_schema_json) as file: - schema_json = commentjson.load(file) + # checks if the path_schema_json point to an internet URI and download the schema using the URI + if path_schema_json.startswith("https://") or path_schema_json.startswith("http://"): + schema_json = requests.get(path_schema_json).json() + + # checks if the path_schema_json is a file + elif os.path.isfile(path_schema_json) or path_schema_json.startswith("file://"): + with open(path_schema_json.replace("file://", "")) as file: + schema_json = commentjson.load(file) + + # checks if the path_schema_json is absolute + elif not os.path.isabs(path_schema_json): + path_schema_json = os.path.join(cwd, path_schema_json) + with open(path_schema_json) as file: + schema_json = commentjson.load(file) + + # if there is no path_schema_json it will return None + else: + return None + + if isinstance(path_config_json, str): + with open(path_config_json) as file: + config_json = commentjson.load(file) + else: + config_json = path_config_json return validate(instance=config_json, schema=schema_json) diff --git a/tests/unit/fixtures/invalid.zowe.config.json b/tests/unit/fixtures/invalid.zowe.config.json index bf06a900..89be0a69 100644 --- a/tests/unit/fixtures/invalid.zowe.config.json +++ b/tests/unit/fixtures/invalid.zowe.config.json @@ -1,5 +1,5 @@ { - "$schema": "./zowe.schema.json", + "$schema": "./invalid.zowe.schema.json", "profiles": { "zosmf": { "type": "zosmf", diff --git a/tests/unit/fixtures/invalidUri.zowe.config.json b/tests/unit/fixtures/invalidUri.zowe.config.json new file mode 100644 index 00000000..68b06dbe --- /dev/null +++ b/tests/unit/fixtures/invalidUri.zowe.config.json @@ -0,0 +1,55 @@ +{ + "$schema": "./invalidUri.zowe.schema.json", + "profiles": { + "zosmf": { + "type": "zosmf", + "properties": { + "port": 10443 + }, + "secure": [] + }, + "tso": { + "type": "tso", + "properties": { + "account": "", + "codePage": "1047", + "logonProcedure": "IZUFPROC" + }, + "secure": [] + }, + "ssh": { + "type": "ssh", + "properties": { + "port": 22 + }, + "secure": ["user"] + }, + "zftp": { + "type": "zftp", + "properties": { + "port": 21, + "secureFtp": true + }, + "secure": [] + }, + "base": { + "type": "base", + "properties": { + "host": "zowe.test.cloud", + "rejectUnauthorized": false + }, + "secure": [ + "user", + "password" + ] + } + }, + "defaults": { + "zosmf": "zosmf", + "tso": "tso", + "ssh": "ssh", + "zftp": "zftp", + "base": "base" + }, + "autoStore": true +} \ No newline at end of file diff --git a/tests/unit/fixtures/invalidUri.zowe.schema.json b/tests/unit/fixtures/invalidUri.zowe.schema.json new file mode 100644 index 00000000..3ef45c26 --- /dev/null +++ b/tests/unit/fixtures/invalidUri.zowe.schema.json @@ -0,0 +1,427 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$version": "1.0", + "type": "invalid", + "description": "Zowe configuration", + "properties": { + "profiles": { + "type": "object", + "description": "Mapping of profile names to profile configurations", + "patternProperties": { + "^\\S*$": { + "type": "object", + "description": "Profile configuration object", + "properties": { + "type": { + "description": "Profile type", + "type": "boolean", + "enum": [ + "zosmf", + "tso", + "ssh", + "zftp", + "base" + ] + }, + "properties": { + "description": "Profile properties object", + "type": "object" + }, + "profiles": { + "description": "Optional subprofile configurations", + "type": "object", + "$ref": "#/properties/profiles" + }, + "secure": { + "description": "Secure property names", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": false + } + }, + "then": { + "properties": { + "properties": { + "title": "Missing profile type" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "zosmf" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "z/OSMF Profile", + "description": "z/OSMF Profile", + "properties": { + "host": { + "type": "string", + "description": "The z/OSMF server host name." + }, + "port": { + "type": "number", + "description": "The z/OSMF server port.", + "default": 443 + }, + "user": { + "type": "string", + "description": "Mainframe (z/OSMF) user name, which can be the same as your TSO login." + }, + "password": { + "type": "string", + "description": "Mainframe (z/OSMF) password, which can be the same as your TSO password." + }, + "rejectUnauthorized": { + "type": "boolean", + "description": "Reject self-signed certificates.", + "default": true + }, + "certFile": { + "type": "string", + "description": "The file path to a certificate file to use for authentication" + }, + "certKeyFile": { + "type": "string", + "description": "The file path to a certificate key file to use for authentication" + }, + "basePath": { + "type": "string", + "description": "The base path for your API mediation layer instance. Specify this option to prepend the base path to all z/OSMF resources when making REST requests. Do not specify this option if you are not using an API mediation layer." + }, + "protocol": { + "type": "string", + "description": "The protocol used (HTTP or HTTPS)", + "default": "https", + "enum": [ + "http", + "https" + ] + }, + "encoding": { + "type": "string", + "description": "The encoding for download and upload of z/OS data set and USS files. The default encoding if not specified is IBM-1047." + }, + "responseTimeout": { + "type": "number", + "description": "The maximum amount of time in seconds the z/OSMF Files TSO servlet should run before returning a response. Any request exceeding this amount of time will be terminated and return an error. Allowed values: 5 - 600" + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tso" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "TSO Profile", + "description": "z/OS TSO/E User Profile", + "properties": { + "account": { + "type": "string", + "description": "Your z/OS TSO/E accounting information." + }, + "characterSet": { + "type": "string", + "description": "Character set for address space to convert messages and responses from UTF-8 to EBCDIC.", + "default": "697" + }, + "codePage": { + "type": "string", + "description": "Codepage value for TSO/E address space to convert messages and responses from UTF-8 to EBCDIC.", + "default": "1047" + }, + "columns": { + "type": "number", + "description": "The number of columns on a screen.", + "default": 80 + }, + "logonProcedure": { + "type": "string", + "description": "The logon procedure to use when creating TSO procedures on your behalf.", + "default": "IZUFPROC" + }, + "regionSize": { + "type": "number", + "description": "Region size for the TSO/E address space.", + "default": 4096 + }, + "rows": { + "type": "number", + "description": "The number of rows on a screen.", + "default": 24 + } + }, + "required": [] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "ssh" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "z/OS SSH Profile", + "description": "z/OS SSH Profile", + "properties": { + "host": { + "type": "string", + "description": "The z/OS SSH server host name." + }, + "port": { + "type": "number", + "description": "The z/OS SSH server port.", + "default": 22 + }, + "user": { + "type": "string", + "description": "Mainframe user name, which can be the same as your TSO login." + }, + "password": { + "type": "string", + "description": "Mainframe password, which can be the same as your TSO password." + }, + "privateKey": { + "type": "string", + "description": "Path to a file containing your private key, that must match a public key stored in the server for authentication" + }, + "keyPassphrase": { + "type": "string", + "description": "Private key passphrase, which unlocks the private key." + }, + "handshakeTimeout": { + "type": "number", + "description": "How long in milliseconds to wait for the SSH handshake to complete." + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password", + "keyPassphrase" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "zftp" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "Configuration profile for z/OS FTP", + "description": "Configuration profile for z/OS FTP", + "properties": { + "host": { + "type": "string", + "description": "The hostname or IP address of the z/OS server to connect to." + }, + "port": { + "type": "number", + "description": "The port of the z/OS FTP server.", + "default": 21 + }, + "user": { + "type": "string", + "description": "Username for authentication on z/OS" + }, + "password": { + "type": "string", + "description": "Password to authenticate to FTP." + }, + "secureFtp": { + "type": [ + "boolean", + "null" + ], + "description": "Set to true for both control and data connection encryption, 'control' for control connection encryption only, or 'implicit' for implicitly encrypted control connection (this mode is deprecated in modern times, but usually uses port 990). Note: Unfortunately, this plugin's functionality only works with FTP and FTPS, not 'SFTP' which is FTP over SSH.", + "default": true + }, + "rejectUnauthorized": { + "type": [ + "boolean", + "null" + ], + "description": "Reject self-signed certificates. Only specify this if you are connecting to a secure FTP instance." + }, + "servername": { + "type": [ + "string", + "null" + ], + "description": "Server name for the SNI (Server Name Indication) TLS extension. Only specify if you are connecting securely" + }, + "connectionTimeout": { + "type": "number", + "description": "How long (in milliseconds) to wait for the control connection to be established.", + "default": 10000 + } + } + }, + "secure": { + "items": { + "enum": [ + "user", + "password" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "base" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "Base Profile", + "description": "Base profile that stores values shared by multiple service profiles", + "properties": { + "host": { + "type": "string", + "description": "Host name of service on the mainframe." + }, + "port": { + "type": "number", + "description": "Port number of service on the mainframe." + }, + "user": { + "type": "string", + "description": "User name to authenticate to service on the mainframe." + }, + "password": { + "type": "string", + "description": "Password to authenticate to service on the mainframe." + }, + "rejectUnauthorized": { + "type": "boolean", + "description": "Reject self-signed certificates.", + "default": true + }, + "tokenType": { + "type": "string", + "description": "The type of token to get and use for the API. Omit this option to use the default token type, which is provided by 'zowe auth login'." + }, + "tokenValue": { + "type": "string", + "description": "The value of the token to pass to the API." + }, + "certFile": { + "type": "string", + "description": "The file path to a certificate file to use for authentication" + }, + "certKeyFile": { + "type": "string", + "description": "The file path to a certificate key file to use for authentication" + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password", + "tokenValue" + ] + } + } + } + } + } + ] + } + } + }, + "defaults": { + "type": "object", + "description": "Mapping of profile types to default profile names", + "properties": { + "zosmf": { + "description": "Default zosmf profile", + "type": "string" + }, + "tso": { + "description": "Default tso profile", + "type": "string" + }, + "ssh": { + "description": "Default ssh profile", + "type": "string" + }, + "zftp": { + "description": "Default zftp profile", + "type": "string" + }, + "base": { + "description": "Default base profile", + "type": "string" + } + } + }, + "autoStore": { + "type": "boolean", + "description": "If true, values you enter when prompted are stored for future use" + } + } +} \ No newline at end of file diff --git a/tests/unit/test_zowe_core.py b/tests/unit/test_zowe_core.py index 952ed3bf..963ce129 100644 --- a/tests/unit/test_zowe_core.py +++ b/tests/unit/test_zowe_core.py @@ -2,18 +2,20 @@ # Including necessary paths import base64 +import commentjson +import importlib.util import json +import keyring import os import shutil -import keyring -import sys import unittest + +from jsonschema import validate, ValidationError, SchemaError +from pyfakefs.fake_filesystem_unittest import TestCase from unittest import mock -from unittest.mock import patch , call -from jsonschema import validate, ValidationError +from unittest.mock import call, patch + from zowe.core_for_zowe_sdk.validators import validate_config_json -import commentjson -from pyfakefs.fake_filesystem_unittest import TestCase from zowe.core_for_zowe_sdk import ( ApiConnection, ConfigFile, @@ -129,7 +131,7 @@ def test_encode_uri_component(self): """Test string is being adjusted to the correct URL parameter""" sdk_api = SdkApi(self.basic_props, self.default_url) - + actual_not_empty = sdk_api._encode_uri_component('MY.STRING@.TEST#.$HERE(MBR#NAME)') expected_not_empty = 'MY.STRING%40.TEST%23.%24HERE(MBR%23NAME)' self.assertEqual(actual_not_empty, expected_not_empty) @@ -183,22 +185,43 @@ class TestZosmfProfileManager(TestCase): def setUp(self): """Setup fixtures for ZosmfProfile class.""" # setup pyfakefs + self.session_arguments = {"verify": False} self.setUpPyfakefs() self.original_file_path = os.path.join(FIXTURES_PATH, "zowe.config.json") self.original_user_file_path = os.path.join( FIXTURES_PATH, "zowe.config.user.json" ) + self.original_invalid_file_path = os.path.join( + FIXTURES_PATH, "invalid.zowe.config.json" + ) self.original_nested_file_path = os.path.join( FIXTURES_PATH, "nested.zowe.config.json" ) self.original_schema_file_path = os.path.join( FIXTURES_PATH, "zowe.schema.json" ) + self.original_invalid_schema_file_path = os.path.join( + FIXTURES_PATH, "invalid.zowe.schema.json" + ) + self.original_invalidUri_file_path = os.path.join( + FIXTURES_PATH, "invalidUri.zowe.config.json" + ) + self.original_invalidUri_schema_file_path = os.path.join( + FIXTURES_PATH, "invalidUri.zowe.schema.json" + ) + + loader = importlib.util.find_spec('jsonschema') + module_path = loader.origin + self.fs.add_real_directory(os.path.dirname(module_path)) + self.fs.add_real_file(self.original_file_path) self.fs.add_real_file(self.original_user_file_path) self.fs.add_real_file(self.original_nested_file_path) self.fs.add_real_file(self.original_schema_file_path) - + self.fs.add_real_file(self.original_invalid_file_path) + self.fs.add_real_file(self.original_invalid_schema_file_path) + self.fs.add_real_file(self.original_invalidUri_file_path) + self.fs.add_real_file(self.original_invalidUri_schema_file_path) self.custom_dir = os.path.dirname(FIXTURES_PATH) self.custom_appname = "zowe_abcd" self.custom_filename = f"{self.custom_appname}.config.json" @@ -240,7 +263,7 @@ def test_autodiscovery_and_base_profile_loading(self, get_pass_func): # Test prof_manager = ProfileManager() - props: dict = prof_manager.load(profile_type="base") + props: dict = prof_manager.load(profile_type="base", validate_schema=False) self.assertEqual(prof_manager.config_filepath, cwd_up_file_path) expected_props = { @@ -270,7 +293,7 @@ def test_custom_file_and_custom_profile_loading(self, get_pass_func): # Test prof_manager = ProfileManager(appname=self.custom_appname) prof_manager.config_dir = self.custom_dir - props: dict = prof_manager.load(profile_name="zosmf") + props: dict = prof_manager.load(profile_name="zosmf", validate_schema=False) self.assertEqual(prof_manager.config_filepath, custom_file_path) expected_props = { @@ -301,7 +324,7 @@ def test_custom_file_and_custom_profile_loading_with_nested_profile(self, get_pa # Test prof_manager = ProfileManager(appname=self.custom_appname) prof_manager.config_dir = self.custom_dir - props: dict = prof_manager.load(profile_name="lpar1.zosmf") + props: dict = prof_manager.load(profile_name="lpar1.zosmf", validate_schema=False) self.assertEqual(prof_manager.config_filepath, custom_file_path) expected_props = { @@ -332,7 +355,7 @@ def test_profile_loading_with_user_overriden_properties(self, get_pass_func): # Test prof_manager = ProfileManager() - props: dict = prof_manager.load(profile_type="zosmf") + props: dict = prof_manager.load(profile_type="zosmf", validate_schema=False) self.assertEqual(prof_manager.config_filepath, cwd_up_file_path) expected_props = { @@ -363,7 +386,7 @@ def test_profile_loading_exception(self, get_pass_func): # Test config_file = ConfigFile(name=self.custom_appname, type="team_config") - props: dict = config_file.get_profile(profile_name="non_existent_profile") + props: dict = config_file.get_profile(profile_name="non_existent_profile", validate_schema=False) @patch("keyring.get_password", side_effect=keyring_get_password_exception) def test_secure_props_loading_warning(self, get_pass_func): @@ -382,7 +405,25 @@ def test_secure_props_loading_warning(self, get_pass_func): prof_manager = ProfileManager() prof_manager.config_dir = self.custom_dir props: dict = prof_manager.load("base") - + + @patch("keyring.get_password", side_effect=keyring_get_password) + def test_profile_not_found_warning(self, get_pass_func): + """ + Test correct warnings are being thrown when profile is not found + in config file. + + Only the config folder will be set + """ + with self.assertWarns(custom_warnings.ProfileNotFoundWarning): + # Setup + custom_file_path = os.path.join(self.custom_dir, "zowe.config.json") + shutil.copy(self.original_file_path, custom_file_path) + + # Test + prof_manager = ProfileManager() + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load("non_existent_profile", validate_schema=False) + @patch("sys.platform", "win32") @patch("zowe.core_for_zowe_sdk.CredentialManager._retrieve_credential") def test_load_secure_props(self, retrieve_cred_func): @@ -522,7 +563,7 @@ def test_save_secure_props_normal_credential(self, delete_pass_func, retrieve_cr @patch("keyring.set_password") @patch("zowe.core_for_zowe_sdk.CredentialManager.delete_credential") def test_save_secure_props_exceed_limit(self, delete_pass_func, set_pass_func, retrieve_cred_func): - + # Set up mock values and expected results service_name = constants["ZoweServiceName"] + "/" + constants["ZoweAccountName"] # Setup - copy profile to fake filesystem created by pyfakefs @@ -563,7 +604,70 @@ def test_save_secure_props_exceed_limit(self, delete_pass_func, set_pass_func, r )) set_pass_func.assert_has_calls(expected_calls) - + @patch("keyring.get_password", side_effect=keyring_get_password) + def test_profile_loading_with_valid_schema(self, get_pass_func): + """ + Test Validation, no error should be raised for valid schema + """ + # Setup - copy profile to fake filesystem created by pyfakefs + custom_file_path = os.path.join(self.custom_dir, "zowe.config.json") + shutil.copy(self.original_nested_file_path, custom_file_path) + shutil.copy(self.original_schema_file_path, self.custom_dir) + os.chdir(self.custom_dir) + + self.setUpCreds(custom_file_path, { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }) + + # Test + prof_manager = ProfileManager(appname="zowe") + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load(profile_name="zosmf") + + @patch("keyring.get_password", side_effect=keyring_get_password) + def test_profile_loading_with_invalid_schema(self, get_pass_func): + """ + Test Validation, no error should be raised for valid schema + """ + # Setup - copy profile to fake filesystem created by pyfakefs + with self.assertRaises(ValidationError): + custom_file_path = os.path.join(self.custom_dir, "invalid.zowe.config.json") + shutil.copy(self.original_invalid_file_path, custom_file_path) + shutil.copy(self.original_invalid_schema_file_path, self.custom_dir) + os.chdir(self.custom_dir) + + self.setUpCreds(custom_file_path, { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }) + + # Test + prof_manager = ProfileManager(appname="invalid.zowe") + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load(profile_name="zosmf", validate_schema=True) + + @patch("keyring.get_password", side_effect=keyring_get_password) + def test_profile_loading_with_invalid_schema_internet_URI(self, get_pass_func): + """ + Test Validation, no error should be raised for valid schema + """ + # Setup - copy profile to fake filesystem created by pyfakefs + with self.assertRaises(SchemaError): + custom_file_path = os.path.join(self.custom_dir, "invalidUri.zowe.config.json") + shutil.copy(self.original_invalidUri_file_path, custom_file_path) + shutil.copy(self.original_invalidUri_schema_file_path, self.custom_dir) + os.chdir(self.custom_dir) + + self.setUpCreds(custom_file_path, { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }) + + # Test + prof_manager = ProfileManager(appname="invalidUri.zowe") + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load(profile_name="zosmf", validate_schema=True) @patch("keyring.get_password", side_effect=keyring_get_password) def test_profile_loading_with_env_variables(self, get_pass_func): @@ -858,7 +962,7 @@ def test_validate_config_json_valid(self): schema_json = commentjson.load(open(path_to_schema)) expected = validate(config_json, schema_json) - result = validate_config_json(path_to_config, path_to_schema) + result = validate_config_json(path_to_config, path_to_schema, cwd = FIXTURES_PATH) self.assertEqual(result, expected) @@ -874,7 +978,7 @@ def test_validate_config_json_invalid(self): validate(invalid_config_json, invalid_schema_json) with self.assertRaises(ValidationError) as actual_info: - validate_config_json(path_to_invalid_config, path_to_invalid_schema) + validate_config_json(path_to_invalid_config, path_to_invalid_schema, cwd = FIXTURES_PATH) self.assertEqual(str(actual_info.exception), str(expected_info.exception))