diff --git a/docs/Design_for_selecting_auth_type.md b/docs/Design_for_selecting_auth_type.md new file mode 100644 index 0000000000..c7f2c9761a --- /dev/null +++ b/docs/Design_for_selecting_auth_type.md @@ -0,0 +1,411 @@ +# Order of authentication in Zowe Clients + +This document proposes a design to enable users to specify the order in which credentials are selected for authentication when multiple credentials are specified by the user. + +Users may not intentionally specify multiple credentials for the same operation. However, because configuration properties can be inherited from a base profile or within nested profiles, it is possible that multiple credentials may be available when a Zowe client attempts to make a connection to a mainframe service. + +## Use cases + +A user may use the same user and password to authenticate to most of their services. It makes sense to place that user & password in a base profile so that they are automatically available to every service. This reduces redundancy and reduces maintenance efforts. If one plugin requires a token for authentication, the user would store a token value within that plugin's profile. For that plugin, both the user & password and a token will be available when Zowe CLI attempts to make a connection to that service. For this example, the token is the right choice for this service. For historical reasons, Zowe always selects a user & password over a token when both are available. The use of user & password does not give the desired results. + +In another example, sites gradually deploy API-ML and can easily encounter another (but opposite) authentication issue. Sites login to APIML to obtain a token, which is then used to authenticate all future requests to services through API-ML. The API-ML token is typically stored in a base profile so that connections to all services are done through API-ML with its token. When a new service is brought on-line at a site, it is common for that service to not be immediately integrated with APIML. For at least a period of time, the site makes a direct connection to that service. The site adds user/password properties to that service's profile to authenticate that direct connection. Once again, both user & password and a token are available when Zowe attempts to connect to the service for that profile. In this case, the user & password are the right choice for this service. However, this is the opposite choice from the previous example. + +As these examples demonstrate, Zowe cannot simply change which authentication type should be given preference. It varies based on what a site is trying to do. That order might also change from one part of the customer's configuration to another. The preferred order in which credentials are chosen is further complicated when we consider that certificates may also be used by a site for some of its services. + +## General approach for a solution + +In this section we identify the key features that a solution would have to provide. + +- The order in which different types of authentication are used should be controlled by the user in the Zowe client configuration. + +- The user must be able to change that authentication order for different parts of the Zowe client configuration. + +- Zowe client logic must be enhanced to select the authentication type for a profile based on a user-specified preferred order. + +- Zowe client extenders (CLI plugins and ZE extensions) should **not** be able to alter the order of authentication types. The user should control that choice. + +## Detailed requirements + +- If a user does not specify the order of authentication, Zowe should use the historical order of authentication so that we do not introduce a breaking change. + +- Zowe has an order of precedence for obtaining property values (config file, environment variable, command line). While a command line option will override the same property stored in a config file, it should not alter the order of authentication. + + - For example, assume that a user has specified that a certificate should be used before a token. Assume that a certificate is supplied in a config file and a token is supplied on the command line. + + - The certificate should be used because the user-configured authentication order specifies that certificates should be used before tokens. + + - The token should not be used just because it was supplied on the command line. + +- Zowe should ignore any authentication order property that is specified in a environment variable (unlike most other connection properties). + + - A single environment variable property would override **every** authentication order property specified in **every** profile within the user's zowe.config.json file. The most likely customer use of this property will be to specify a different authentication order for different profiles. A single environment variable would likely defeat the primary purpose of enabling a user to specify a different authentication order for different profiles. + +- The authentication order **could** be specified on the command line. However, in the initial implementation of this feature, Zowe CLI will **NOT** implement a command line option for the authentication order. + + - Conceptually, specifying an authentication order as a command line option is a reasonable idea. However, implementing that behavior could involve modifications to dozens of CLI command handlers, which could increase our implementation effort and time-to-market significantly. + + - It is not clear whether a command line option for authentication order will be particularly valuable to customers. The authentication order would typically be static for a given profile. It is unlikely to change from one CLI command to another. + + - Therefore, we will **NOT** implement a command-line option for authentication order until customer demand makes such work a priority. + +- The authentication order only identifies the order in which Zowe chooses the **one** authentication method that will be used. If that first authentication method fails, Zowe will not make a second attempt to authenticate with any of the later authentication methods. + +- Once an authentication is selected, our logic should ensure that only that one type of authentication is placed into a session object. Thus, logic in down-stream Zowe code will not alter the order of authentication selection, simply by testing for the authentications from the session in a different order than the order that the user specified. + + - If we were to continue to allow multiple authentications to be placed into a session, we will have to re-work various functions to test for authentications in the desired order. We will also have to provide the object containing that well-defined order to each such function. This will increase the amount of code being changed, and thus increase the time to market and increase the probability of mistakes. + +- Zowe clients do not currently support AUTH_TYPE_CERT_PFX, so we cannot add it to a set of available authentications at this time. If AUTH_TYPE_CERT_PFX is implemented, it should be placed immediately after AUTH_TYPE_CERT_PEM in the default order. + +- Customers should be able to specify AUTH_TYPE_NONE in their preferred order of authentications. While it is not advisable to have no authentication, if a customer has a service that requires no authentication, the customer should be able to specify AUTH_TYPE_NONE at the top of their list of authentications applicable to that particular profile. + +- A customer should not have to specify every possible authentication in their ordered list of authentications. If a site only uses password and tokens, the customer should be able to specify only those two authentications in their list. + +- If a customer-specified list of authentications contains none of our supported authentications, a default order will be used. + +- The `--show-input-only` option should show the order of authentication as part of its displayed connection properties. + +## Historical behavior + +The objective of this feature is to enable users to define the order in which an authentication type is selected. However, when a user does not specify any such order, the default order should reflect past behavior. + +- The AbstractRestClient currently enforces an order of: + + - AUTH_TYPE_TOKEN + - AUTH_TYPE_BASIC + - AUTH_TYPE_BEARER + - AUTH_TYPE_CERT_PEM + +- Zowe classes other than AbstractRestClient (like AbstractSession) currently override the authentication order from AbstractRestClient into: + + - AUTH_TYPE_BASIC + - AUTH_TYPE_TOKEN + - AUTH_TYPE_BEARER + - AUTH_TYPE_CERT_PEM + +These selections of authentication should be maintained as the default selections for their respective classes to avoid introducing a breaking change. + +## Configuration enhancement + +A new profile property named **authOrder** should be created to enable users to specify their order of precedence for the authentication to be used when making a REST connection. The authOrder property should be treated like one of our key connection properties (like host and port). Thus authOrder would have the following characteristics: + +- It must be specified within a "properties" object. That "properties" object could reside in: + + - A base profile + + - A parent profile of a nested configuration. + + - Any service profile. + + - Any profile specific to a plugin (or VSCode extension) that supports a REST connction. For example an **endevor** profile could contain an **authOrder** property, but an **endevor-location** profile would not. + +- Our existing inheritance of connection properties should also apply to the inheritance of the authOrder property. + +- We should be able to use our logic for where and how the **rejectUnauthorized** property is handled as a model for how we handle the **authOrder** property. + +- A new property can be added to the **properties** of any profile. Since we are proposing that **authOrder** be handled by imperative functions, no CLI plugin or ZE extension will need modification for the **authOrder** property to be correctly handled. + +- Such a new item within the properties object will not currently be included in the zowe.schema.json file. An IntelliSense-like editor will not display **authOrder** as an option when a user starts to add a new property in a profile. + + - We do not want to require plugins/extensions to add **authOrder** to their profiles. Asking extenders to add AuthOrder to their set of options (and thus the schema) in the middle of a release's lifecycle can be a burden for those contributors. + + - As a result, we feel that losing IntelliSense is an acceptable compromise to avoid adding work to every extender. Users will lose a nice convenience, but the user's config will not be limited in any way at runtime. + +To represent a series of values, the **authOrder** property should be an array. The following example shows how a user could specify their desired authOrder. + +``` +"properties": { +    "host": ... , +    "port": ... , +    "rejectUnauthorized": ... , +    "authOrder": [ "basic", "token", "cert-pem" ] +} +``` + +The programmatic definition of authOrder would be: + +``` +authOrder: SessConstants.AUTH_TYPE_CHOICES[] +``` + +The current set of AUTH_TYPE_CHOICES are: + +- AUTH_TYPE_BASIC = "basic" + +- AUTH_TYPE_BEARER = "bearer" + +- AUTH_TYPE_TOKEN = "token" + +- AUTH_TYPE_CERT_PEM = "cert-pem" + +- AUTH_TYPE_NONE = "none" + +We should add a new AUTH_TYPE_CHOICE of: + +- AUTH_TYPE_SSH_KEY = "ssh-key" + +That addition would enable customers to also specify the authentication order of precedence for an SSH connection using an authOrder property. The only permissible values for an ssh connection would be "basic" and "ssh-key". Our ssh-handling logic will have to be modified to enforce that restriction and to honor the order. Conversely, our zosmf-handling logic would have to be modified to reject "ssh-key" in authOrder (or at least ignore it). If we choose not to implement authOrder for ssh at this time, we should at least create a design and implementation that can tolerate the addition of "ssh-key" at a later date. + +## Documentation Impact + +- We must describe the purpose and function of the new authOrder property. + +- We must describe where users can place the authOrder property. + +- We must describe the default order of authentication, when no authOrder property is supplied. + +- We must document that the new authOrder property name is a Zowe reserved word that should **NOT** be used by any extender. + +- We must notify extenders to guide their customers to supply an appropriate authOrder property if their extension needs a non-default order. + +## Edge cases that must be confirmed during implementation + +Every edge case was not included in the protype used to confirm the validity of this design document. The following edge cases must be confirmed and appropriate logic must be written during the implementation of this design. + +- As an alternative to user & password, a property named base64EncodedAuth can be used in a session. The new APIs must handle this alternative. + +- To login to APIML, a user supplies a user & password (or cert) and receives a token. The new APIs must determine the correct authentication type for such a session which kind of morphs its authentication type during the transaction. + +- Zowe Explorer currently inherits the same hard-coded order as the CLI from the client SDKs. The new authentication order APIs must be integrated into the APIs used by Zowe Explorer. We must confirm whether any additional logic changes must be made within Zowe Explorer itself. + +- TSO commands can create a sequence of actions, which use a user & password, receive a token from TSO, and then use that token for additional actions. This change in authentication type during the life of a single transaction may be self contained within the transaction and may not not influenced by either the existing hard-coded authentication order or the new user-controlled authentication order. This behavior must be confirmed and modifications made if necessary. + +## A new class must be created to support authOrder + +A new class located in: + +    [zowe-cli/packages/imperative/src/rest/src/session/AuthOrder.ts](https://github.com/zowe/zowe-cli/blob/master/packages/imperative/src/rest/src/session/AuthOrder.ts) + +should be implemented to detect the authentication order supplied by a user, and place the correct set of credentials into a session. The class currently exists as a workable proof of concept. With no confirmation of the many edge cases, it does appear to accomplished the basic needs. It can be finalized once it is fully integrated with existing Zowe logic. + +This class is declared **@internal** which will limit its use to the imperative package. More access can be provided during implementation if we identify additional places in Zowe code that need to use functions from this class. + +Functions in the AuthOrder class include: + +- setTopDefaultAuth - This function will set the top authentication choice (either basic or token) when a default authOrder must be created because the user did not supply an auth order. Through the use of this function we can maintain backward compatibility and not introduce a breaking change. + + When authOrder has not been configured into zowe.config.json, we create a default order to be backward compatible. + + - If an argument of AUTH_TYPE_TOKEN is supplied, it should place token at the top of the order, resulting in this order: + + - AUTH_TYPE_TOKEN + + - AUTH_TYPE_BASIC + + - AUTH_TYPE_BEARER + + - AUTH_TYPE_CERT_PEM + + - AUTH_TYPE_NONE + + - If AUTH_TYPE_BASIC is supplied (or this function is not called), it should place basic at the top of the order, resulting in this order: + + - AUTH_TYPE_BASIC + + - AUTH_TYPE_TOKEN + + -  AUTH_TYPE_BEARER + + - AUTH_TYPE_CERT_PEM + + - AUTH_TYPE_NONE + + - It should be called from AbstractRestClient.constructor to replace its hard-coded array of credential types. + + - It might be called from other existing locations as needed. + +- putTopAuthInSession - This function will find the highest auth type (according to the authOrder) which exists in either the supplied session config or command line arguments, place the credentials associated with that auth type into the supplied session config, and remove the credentials for all other auth types from the supplied session config. + + - It might be called from ConnectionPropsForSessCfg.addPropsOrPrompt. + + - By temporarily modifying a local copy of addPropsOrPrompt to call putTopAuthInSession, I confirmed that the resulting session object contained only those credentials obtained from the command arguments associated with highest authentication type (as determined from the authOrder). + + - It might be called from ConnectionPropsForSessCfg.resolveSessCfgProps. + + - It might be called from ProfileInfo.createSession. + + - While apps like ZE do not take command line arguments, they do form an ICommandArguments object. The createSesion function already has such an object, so it can be passed to putTopAuthInSession. + + - Any other location in ZE-related code which needs to call putTopAuthInSession will need to form an ICommandArguments object. + + - It might be called from other existing locations as needed. + +- cacheAuthOrder - This function stores the authOrder property from the supplied command arguments for later use. In the prototype, it is only called from putTopAuthInSession. However, cacheAuthOrder is available should we find that we need to to call it from other locations (like at the end of the CommandProcessor.prepare function). + +- removeExtraCredsFromSess - This function removes all credential properties from the supplied session except for the top available authentication type. In the prototype, it is only called from putTopAuthInSession. However, removeExtraCredsFromSess is available should we find that we must scrub unneeded credentials from a session in other location in existing Zowe code. + +## Functions that reference AUTH_TYPE may need modification + +The set of candidates for modification consist of all functions that contain the string ***"AUTH_TYPE_"***. This section contains an assessment of whether each such function affects the authentication order and must be modified. + +- cli\src\config\auto-init\ApimlAutoInitHandler + + - doAutoInit - This function logins into APIML with the session object if either user & password or cert are in the session object. doAutoInit does not make a selection of order. However, it is passed an AbstractSession object. The session credentials should already have been placed into the AbstractSession by a previously called function + - **Modify doAutoInit ? No** + +- core\src\rest\ZosmfRestClient.ts + + - processError - This function just alters error message text based on authentications found in the session. If only the top authentication type resides in a session, then no need to change. + - **Modify processError ? No** + +- imperative\src\config\src\ConfigAutoStore.ts + + - _fetchTokenForSessCfg - Since this function is used to explicitly retrieve a token value to be auto-stored into a session config, its use of AUTH_TYPE_TOKEN does not affect the auth order. So, no need to change. + - **Modify _fetchTokenForSessCfg ? No** + +- imperative\src\imperative\src\config\cmd\import\import.handler.ts + + - buildSession - This function is used to import a config from a URL. That URL is an arbitrary location at a customer site where a config file is kept. It is not the target of a REST request to a mainframe service. By design, the only authentication that it will use is user & password. Supporting more authentication types in the 'import' command is beyond the scope of this authentication-order feature. Therefore, no need to change. + - **Modify buildSession ? No** + +- imperative\src\rest\src\client\AbstractRestClient.ts + + This class is the only class to use the recently created ISession.authTypeOrder property, which is an array of authentication types supplied in the order in which they should be selected. + + - buildOptions - This function tests for the authentication based on the order in which they occur in ISession.authTypeOrder. Therefore, no need to change. + + - **Modify buildOptions ? No** + + - constructor - This function currently hard-codes an order of authentication types into the ISession.authTypeOrder array contained the supplied session. After AuthOrder is implemented, the ISession.authTypeOrder array should already be set in the session which is passed as an argument to this constructor. We should remove the logic that hard-codes the authTypeOrder into the session. + + - **Modify constructor? yes** + + - Each of the following functions reference AUTH_TYPE_XXX to a place an identified type into the ISession.type property of the session. Since buildOptions calls just one of the following functions based on being the first available authentication in the ISession.authTypeOrder array, none of these functions need to change. Note that these functions may no longer be necessary since AuthOrder.putTopAuthInSession should have already set the ISession.type property. + + - **Modify setBearerAuth ? No** + + - **Modify setCertPemAuth ? No** + + - **Modify setPasswordAuth ? No** + + - **Modify setTokenAuth ? No** + +- imperative\src\rest\src\session\AbstractSession.ts + + - buildSession - This private function is called by the constructor, which accepts an Isession object. A caller could populate multiple authentications (and related properties) into that supplied session. Ideally the caller of the Session.constructor will have already called ConnectionPropsForSessCfg.addPropsOrPrompt() which will have already removed all but the highest priority available authentication from the session. We should confirm that this always true. If not, buildSession may have to call AuthOrder.putTopAuthInSession. We should also check whether assignments to session properties by buildSession will override any of the properties set by AuthOrder.putTopAuthInSession. + + - **Modify buildSession ? Maybe** + + - DEFAULT_TYPE - This is simply a constant definition set to AUTH_TYPE_NONE. It is not used in any CLI or ZE code outside of this AbstractSession class. Because it is a public property, it cannot be removed without risk of breaking change. If AUTH_TYPE_NONE is added to the ISession.authTypeOrder array, DEFAULT_TYPE should be deprecated. + + - **Modify DEFAULT_TYPE ? Yes** + +- imperative\src\rest\src\session\ConnectionPropsForSessCfg.ts + + - addPropsOrPrompt - Near the end of this function, a call to AuthOrder.putTopAuthInSession will ensure that the right authentication resides in the session. + + - **Modify addPropsOrPrompt ? Yes** + + - resolveSessCfgProps - Several functions call this function before creating a new session. Either this function must call AuthOrder.putTopAuthInSession, or each caller of resolveSessCfgProps must first call putTopAuthInSession. + + - **Modify resolveSessCfgProps ? Maybe** + + - setTypeForTokenRequest - This function handles setting authentication to AUTH_TYPE_TOKEN to get a token back from user & password. This does not appear to require any change, but it should be revisited after resolveSessCfgProps is refactored. + + - **Modify setTypeForTokenRequest ? Maybe** + +- imperative\src\rest\src\session\SessConstants.ts + + - Constants and type definitions of AUTH_TYPE_XXX are what they need to be. + - **Modify constants ? No** + +- imperative\src\rest\src\session\Session.ts + + - createFromUrl - This function is only called from ImportHandler.buildSession to enable importing a config from a URL. As with ImportHandler.buildSession, the use of AUTH_TYPE_BASIC when user & password exist is appropriate and should not need to change. + - **Modify createFromUrl ? No** + +- imperative\src\rest\src\session\doc\IOptionsForAddConnProps.ts + + - supportedAuthTypes - Our set of supported authentications will not change as part of this feature. + - **Modify supportedAuthTypes ? No** + +- imperative\src\rest\src\session\doc\ISession.ts + + - authTypeOrder - This property is used to hold the order of authentication types. It is currently populated by hard-coded login and is only used in AbstractRestClient.constructor & AbstractRestClient.buildOptions. There is no reason to change this property. AuthOrder.putTopAuthInSession should populate authTypeOrder's set of values based on customer input. The only change for authTypeOrder will be to remove the comment that states that this property is hard-coded. + + - **Modify authTypeOrder ? Yes** + +- packages\zosuss\src\SshBaseHandler.ts + + - process - This function explicitly sets a property named **supportedAuthTypes** to AUTH_TYPE_BASIC. It is unclear why there is no option in this logic to use an ssh-key. + - **Modify process ? Maybe** + + packages\zosuss\src\Shell.ts + + - connect - This function explicitly checks for an ssh key (first) or a password (second) in a hard-coded fashion. If we want the user's authOrder to apply to ssh connections, this function must call the proposed AuthOrder.putTopAuthInSession to to make the right authentication choice. + - **Modify connect ? Yes** + +## Functions that reference rejectUnauthorized may need modification + +If we treat **authOrder** like other connection properties, those functions that process **rejectUnauthorized** may also need to process **authOrder**. This section contains an assessment of whether each such function must be modified. + +- packages\cli\src\config\auto-init\ApimlAutoInitHandler.ts + + - doAutoInit - This function was analyzed in the previous section of this document. + - **Modify doAutoInit ? No** + +- packages\cli\src\imperative.ts + + - This class provides definitions used to create the Zowe command tree and other CLI operating properties. It includes command-line options for connection properties (like host, port, user, and password). We must add **authOrder** as a new command line option into this class. + - **Modify imperative.ts? Yes** + +- packages\core\src\constants\Core.constants.ts + + - BaseProfile - This object contains a schema definition for a base profile. An authOrder property must be added to this object. + - **Modify BaseProfile ? Yes** + +- packages\imperative\src\config\src\ProfileInfo.ts + + - createSession - This function creates a session with key connection properties. Depending on the modification choices made for ConnectionPropsForSessCfg.resolveSessCfgProps, this function may have to call AuthOrder.putTopAuthInSession directly. + - **Modify createSession ? Yes** + +- packages\imperative\src\imperative\src\config\cmd\import\import.handler.ts + + - buildSession - This function was analyzed in the previous section of this document. + - **Modify buildSession ? No** + +- packages\imperative\src\rest\src\client\AbstractRestClient.ts + + - buildOptions - This function was analyzed in the previous section of this document. + - **Modify buildOptions ? No** + +- packages\imperative\src\rest\src\client\ProxySettings.ts + + - getProxyAgent - This function requires an ISession object. The ISession.authTypeOrder should have already been populated. Therefore, no change should be required in getProxyAgent + - **Modify getProxyAgent ? No** + +- packages\imperative\src\rest\src\client\doc\IHTTPSOptions.ts + + - IHTTPSOptions - This interface is only used by AbstractRestClient. Credentials are never extracted from nor added to this an instance of this interface. Therefore there is no reason to add authOrder to this interface. + - **Modify IHTTPSOptions ? No** + +- packages\imperative\src\rest\src\session\AbstractSession.ts + + - buildSession - This function was analyzed in the previous section of this document. + - **Modify buildSession ? Maybe** + +- packages\imperative\src\rest\src\session\doc\ISession.ts + + - authTypeOrder - This property was analyzed in the previous section of this document. + - **Modify authTypeOrder ? No** + +- packages\zosjobs\src\GetJobs.ts + + - getJob - This function displays rejectUnauthorized in a diagnostic message. No need to process authOrder here. + - **Modify getJob ? No** + +- packages\zosmf\src\ZosmfSession.ts + + - This Class contains option definitions for connection properties that can be defined in profiles. A definition of the authOrder connection property with a name like ZOSMF_OPTION_AUTH_ORDER should be added. + - **Modify ZosmfSession.ts ? Yes** + +- packages\zosmf\src\constants\Zosmf.messages.ts + + - This class provides message text used to display error details. There is no clear reason to add authOrder to this class. + - **Modify Zosmf.messages.ts ? No** + +- packages\zosmf\src\constants\Zosmf.profile.ts + + - ZosmfProfile - This class provides the property definitions for a zosmf profile type. We must add a definition for the authOrder property. + - **Modify Zosmf.profile.ts ? Yes** + +## \ No newline at end of file diff --git a/prototypes/Readme.md b/prototypes/Readme.md new file mode 100644 index 0000000000..763191fe3d --- /dev/null +++ b/prototypes/Readme.md @@ -0,0 +1,11 @@ +The purpose of this **prototypes** folder is to provide a home for prototype or proof-of-concept code. As an example, new, experimental code that implements a compelling feature may be good model for the future permanent implementation of that feature. + +During experiments, you might place a new class into a particular package to confirm that an idea is feasible. However, that code may not be complete enough to be built and packaged into the product. Lint rules might fail on this new code. You may not have any tests yet, so code coverage verification may fail. + +You do not want this new code to cause Zowe build pipelines to fail, but you do not want to lose the useful work that has been completed. Placing your source file(s) under the **prototypes** directory is a way to keep your valuable experiment and not break the Zowe build process. + +Place your source file into a path under the **prototypes** directory that mirrors the real directory path. + +**Tip:** Create a hard link from the file in the real file path to an identical file path under the **prototypes** directory. That way imports, compiles, and debugging will work in the real directory during experiments, while all changes are automatically recorded in the **prototypes** path. + +**Note:** Remember to delete a new file or revert an existing file in the real directory path. The remaining hard-linked file(s) in the **prototypes** directory path will still have all of the latest changes. diff --git a/prototypes/packages/.gitkeep b/prototypes/packages/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prototypes/packages/cli/src/zosfiles/list/ds/DataSet.handler.ts b/prototypes/packages/cli/src/zosfiles/list/ds/DataSet.handler.ts new file mode 100644 index 0000000000..e918eb5e8a --- /dev/null +++ b/prototypes/packages/cli/src/zosfiles/list/ds/DataSet.handler.ts @@ -0,0 +1,47 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { AbstractSession, IHandlerParameters, TextUtils } from "@zowe/imperative"; +import { IZosFilesResponse, List } from "@zowe/zos-files-for-zowe-sdk"; +import { ZosFilesBaseHandler } from "../../ZosFilesBase.handler"; + +/** + * Handler to list a data sets + * @export + */ +export default class DataSetHandler extends ZosFilesBaseHandler { + public async processWithSession(commandParameters: IHandlerParameters, session: AbstractSession): Promise { + /* todo: Remove diagnostic print statements + */ + return { + commandResponse: "____ DataSetHandler: Pretend that we ran the list command.", + success: true + }; + // end Remove todo: */ + + const response = await List.dataSet(session, commandParameters.arguments.dataSetName, { + volume: commandParameters.arguments.volumeSerial, + attributes: commandParameters.arguments.attributes, + maxLength: commandParameters.arguments.maxLength, + responseTimeout: commandParameters.arguments.responseTimeout, + start: commandParameters.arguments.start + }); + + if (commandParameters.arguments.attributes && response.apiResponse.items.length > 0) { + commandParameters.response.console.log(TextUtils.prettyJson(response.apiResponse.items)); + } else { + const dsnameList = response.apiResponse.items.map((mem: any) => mem.dsname); + commandParameters.response.console.log(dsnameList.join("\n")); + } + + return response; + } +} diff --git a/prototypes/packages/cli/src/zosfiles/list/ds/Readme.md b/prototypes/packages/cli/src/zosfiles/list/ds/Readme.md new file mode 100644 index 0000000000..5a1554004d --- /dev/null +++ b/prototypes/packages/cli/src/zosfiles/list/ds/Readme.md @@ -0,0 +1,7 @@ +Files used to create a prototype of AuthOrder are: + +prototypes\packages\imperative\src\rest\src\session\AuthOrder.ts + +prototypes\packages\imperative\src\rest\src\session\ConnectionPropsForSessCfg.ts + +prototypes\packages\cli\src\zosfiles\list\ds\DataSet.handler.ts diff --git a/prototypes/packages/imperative/src/rest/src/session/AuthOrder.ts b/prototypes/packages/imperative/src/rest/src/session/AuthOrder.ts new file mode 100644 index 0000000000..dade7829d5 --- /dev/null +++ b/prototypes/packages/imperative/src/rest/src/session/AuthOrder.ts @@ -0,0 +1,316 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ICommandArguments } from "../../../cmd"; +import { ImperativeError } from "../../../error"; +import { ISession } from "./doc/ISession"; +import { Logger } from "../../../logger"; +import * as SessConstants from "./SessConstants"; + +/** + * @internal - Cannot be used outside of the imperative package + * + * The purpose of this class is to detect an authentication order property + * supplied by a user in a profile, command line, or environment variable. + * That authOrder is then used to place the correct set of credentials into + * a session for authentication. + */ +export class AuthOrder { + + /** + * When a user does not supply an auth order, Zowe clients will use a + * hard-coded default order. This property records whether AUTH_TYPE_BASIC + * or AUTH_TYPE_TOKEN is the top auth choice in the order. + */ + private static m_topDefaultAuth: string = SessConstants.AUTH_TYPE_BASIC; + + /** + * This array of authentication types specifies the order of preferred + * authentication. It contains the user-specified order, or a default order + * if the user does not specify an order. m_authOrder[0] is the highest + * preferred authentication. + */ + private static m_authOrder: SessConstants.AUTH_TYPE_CHOICES[] = null; + + // *********************************************************************** + /** + * Set the top auth type when a default authOrder is used. Previously, + * two different hard-coded orders were present in the Zowe clients. + * Both hard-coded orders are now provided by this class. Zowe code, + * that still sets a specific order for backward compatibility, now + * calls this function to ensure that that the original behavior remains + * the same and avoids a breaking change when a user has not specified + * an authOrder property. + * + * @param topDefaultAuth - Input. + * The top authentication type that will be used when forming a + * default authOrder. + */ + public static setTopDefaultAuth( + topDefaultAuth: typeof SessConstants.AUTH_TYPE_BASIC | typeof SessConstants.AUTH_TYPE_TOKEN + ): void { + AuthOrder.m_topDefaultAuth = topDefaultAuth; + } + + // *********************************************************************** + /** + * Cache the authOrder property from the supplied cmdArgs. If no authOrder exists + * in cmdArgs, a default authOrder is created and cached. + * + * @param cmdArgs - Input. + * The set of arguments that the calling function is using. + */ + private static cacheAuthOrder(cmdArgs: ICommandArguments): void { + // have we already cached the authOrder? + if (AuthOrder.m_authOrder !== null) { + // start over with an empty order. + AuthOrder.m_authOrder = null; + } + + if (cmdArgs.authOrder) { + // validate each user-supplied type of authentication + for (const nextUserAuth of cmdArgs.authOrder) { + switch (nextUserAuth) { + case SessConstants.AUTH_TYPE_BASIC: + case SessConstants.AUTH_TYPE_TOKEN: + case SessConstants.AUTH_TYPE_BEARER: + case SessConstants.AUTH_TYPE_CERT_PEM: + case SessConstants.AUTH_TYPE_NONE: + if (AuthOrder.m_authOrder === null) { + AuthOrder.m_authOrder = []; + } + AuthOrder.m_authOrder.push(nextUserAuth); + break; + default: + Logger.getImperativeLogger().error( + `The authentication = '${nextUserAuth}' is not valid and will be ignored.` + ); + // todo: Remove diagnostic print statements + console.log("____ cacheAuthOrder: nextUserAuth = '" + nextUserAuth + "' is not valid and will be ignored."); + // todo: end Remove + } + } + } + + // the user supplied an authOrder + if (AuthOrder.m_authOrder !== null) { + return; + } + + // No authOrder was supplied by the user. Create a default order. + AuthOrder.m_authOrder = []; + if (AuthOrder.m_topDefaultAuth === SessConstants.AUTH_TYPE_BASIC) { + // we want user & password auth as the top choice + AuthOrder.m_authOrder.push(SessConstants.AUTH_TYPE_BASIC); + AuthOrder.m_authOrder.push(SessConstants.AUTH_TYPE_TOKEN); + } else { + // we want token auth as the top choice + AuthOrder.m_authOrder.push(SessConstants.AUTH_TYPE_TOKEN); + AuthOrder.m_authOrder.push(SessConstants.AUTH_TYPE_BASIC); + } + // add remaining auth types. We do not include 'none' in our defaults. + AuthOrder.m_authOrder.push(SessConstants.AUTH_TYPE_BEARER); + AuthOrder.m_authOrder.push(SessConstants.AUTH_TYPE_CERT_PEM); + } + + // *********************************************************************** + /** + * Find the highest auth type (according to the authOrder) which exists + * in either the supplied session config or command line arguments. + * Then place the credentials associated with that auth type into the + * supplied session config. Credentials for all other auth types are + * removed from the session config. + * + * @param sessCfg - Modified. + * Authentication properties are added to and removed from this + * session configuration, which can already have properties in + * this object when passed to this function. + * + * @param cmdArgs - Input. + * The set of arguments with which the calling function is operating. + * For CLI, the cmdArgs come from the command line, profile, or + * environment. Other apps can place relevant arguments into this + * object to be processed by this function. + */ + public static putTopAuthInSession( + sessCfg: SessCfgType, + cmdArgs: ICommandArguments + ): void { + let sessTypeToUse: SessConstants.AUTH_TYPE_CHOICES = null; + + // todo: Remove diagnostic print statements + console.log("____ putTopAuthInSession: cmdArgs = " + JSON.stringify(cmdArgs, null, 2)); + sessCfg.tokenType = "apimlAuthenticationToken"; + sessCfg.tokenValue = "SomeTokenValue"; + sessCfg.cert = "./certFile.txt"; + sessCfg.certKey = "./certKeyFile.txt"; + sessCfg.authTypeOrder = []; + sessCfg.authTypeOrder.push("bogusAuthType1"); + sessCfg.authTypeOrder.push("bogusAuthType2"); + sessCfg.authTypeOrder.push("bogusAuthType3"); + sessCfg.authTypeOrder.push("bogusAuthType4"); + console.log("____ putTopAuthInSession: sessCfg before processing = " + JSON.stringify(sessCfg, null, 2)); + // todo: end Remove + + // cache the correct authOrder to use + AuthOrder.cacheAuthOrder(cmdArgs); + + // Detect the first auth type (from our auth order) provided in the session config or in command args. + // Ensure that the auth properties are placed in the session config. + // Record the detected auth type for use as the session type. + let errMsg: string; + for (const nextAuth of AuthOrder.m_authOrder) { + switch (nextAuth) { + case SessConstants.AUTH_TYPE_BASIC: + // todo: do we have to check for sessCfg.base64EncodedAuth ? + if (cmdArgs.user?.length > 0) { + sessCfg.user = cmdArgs.user; + } + if (cmdArgs.password?.length > 0) { + sessCfg.password = cmdArgs.password; + } + if (sessCfg.user?.length > 0 && sessCfg.password?.length > 0) { + // TODO: Confirm in ConnectionPropsForSessCfg that requestToken will work ok with sessTypeToUse + sessTypeToUse = SessConstants.AUTH_TYPE_BASIC; + } + break; + case SessConstants.AUTH_TYPE_TOKEN: + if (cmdArgs.tokenType?.length > 0) { + sessCfg.tokenType = cmdArgs.tokenType; + } + if (cmdArgs.tokenValue?.length > 0) { + sessCfg.tokenValue = cmdArgs.tokenValue; + } + if (sessCfg.tokenType?.length > 0 && sessCfg.tokenValue?.length > 0) { + sessTypeToUse = SessConstants.AUTH_TYPE_TOKEN; + } + break; + case SessConstants.AUTH_TYPE_BEARER: + if (cmdArgs.tokenType?.length > 0) { + sessCfg.tokenType = cmdArgs.tokenType; + } + if (cmdArgs.tokenValue?.length > 0) { + sessCfg.tokenValue = cmdArgs.tokenValue; + } + // a tokenValue with no tokenType implies a bearer token + if (!(sessCfg.tokenType?.length > 0) && sessCfg.tokenValue?.length > 0) { + sessTypeToUse = SessConstants.AUTH_TYPE_BEARER; + } + break; + case SessConstants.AUTH_TYPE_CERT_PEM: + if (cmdArgs.certFile?.length > 0) { + sessCfg.cert = cmdArgs.certFile; + } + if (cmdArgs.certKeyFile?.length > 0) { + sessCfg.certKey = cmdArgs.certKeyFile; + } + if (sessCfg.cert?.length > 0 && sessCfg.certKey?.length > 0) { + sessTypeToUse = SessConstants.AUTH_TYPE_CERT_PEM; + } + break; + case SessConstants.AUTH_TYPE_NONE: + sessTypeToUse = SessConstants.AUTH_TYPE_NONE; + break; + default: + // authOrder was validated. A wrong value now is our programming error. + errMsg = `authOrder contains an invalid authentication = ${nextAuth}.`; + Logger.getImperativeLogger().error(errMsg); + throw new ImperativeError({ msg: errMsg }); + } + if (sessTypeToUse !== null) { + // stop looking for auth types after we find the first one + break; + } + } + + // When no creds are in the session and AUTH_TYPE_NONE is not in the user's authOrder, + // remove the session type from the session. Otherwise set the type that we found. + if (sessTypeToUse === null) { + delete sessCfg.type; + } else { + sessCfg.type = sessTypeToUse; + } + + // remove all extra auth creds from the session + AuthOrder.removeExtraCredsFromSess(sessCfg); + + // copy our authOrder into the session object + sessCfg.authTypeOrder = [...AuthOrder.m_authOrder]; + + // todo: Should we throw error if no creds are in the session, or let other functions throw the error? + + // todo: Remove diagnostic print statements + console.log("____ putTopAuthInSession:\nsessCfg after processing = " + JSON.stringify(sessCfg, null, 2)); + // todo: end Remove + } + + // *********************************************************************** + /** + * Remove all credential properties from the supplied session except for the + * creds related to the session type specified within the sessCfg argument. + * + * @param sessCfg - Modified. + * Authentication credentials are removed from this session configuration. + */ + private static removeExtraCredsFromSess( + sessCfg: SessCfgType + ): void { + if (AuthOrder.cacheAuthOrder == null) { + const errMsg = "AuthOrder.cacheAuthOrder must be called before AuthOrder.removeExtraCredsFromSess"; + Logger.getImperativeLogger().error(errMsg); + throw new ImperativeError({ msg: errMsg }); + } + if (!sessCfg?.type) { + const errMsg = "Session type must exist in the supplied session."; + Logger.getImperativeLogger().error(errMsg); + throw new ImperativeError({ msg: errMsg }); + } + + // initially set all creds to be removed from the session. Later we delete the desired creds from this set + const credsToRemove = new Set(["user", "password", "base64EncodedAuth", "tokenType", "tokenValue", "cert", "certKey"]); + + // Delete the selected creds from the set of creds that will be removed from our session config. + let errMsg: string; + switch (sessCfg.type) { + case SessConstants.AUTH_TYPE_BASIC: + credsToRemove.delete("user"); + credsToRemove.delete("password"); + credsToRemove.delete("base64EncodedAuth"); + break; + case SessConstants.AUTH_TYPE_TOKEN: + credsToRemove.delete("tokenType"); + credsToRemove.delete("tokenValue"); + break; + case SessConstants.AUTH_TYPE_BEARER: + credsToRemove.delete("tokenValue"); + break; + case SessConstants.AUTH_TYPE_CERT_PEM: + credsToRemove.delete("cert"); + credsToRemove.delete("certKey"); + break; + case SessConstants.AUTH_TYPE_NONE: + break; + default: + // authOrder was validated. A wrong value now is our programming error. + errMsg = `Session an invalid type = ${sessCfg.type}.`; + Logger.getImperativeLogger().error(errMsg); + throw new ImperativeError({ msg: errMsg }); + } + + // remove all auth creds from the session, except the creds for the auth type that we chose to keep + const credIter = credsToRemove.values(); + let nextCredToRemove = credIter.next(); + while (!nextCredToRemove.done) { + delete (sessCfg as any)[nextCredToRemove.value]; + nextCredToRemove = credIter.next(); + } + } +} \ No newline at end of file diff --git a/prototypes/packages/imperative/src/rest/src/session/ConnectionPropsForSessCfg.ts b/prototypes/packages/imperative/src/rest/src/session/ConnectionPropsForSessCfg.ts new file mode 100644 index 0000000000..76ea99871b --- /dev/null +++ b/prototypes/packages/imperative/src/rest/src/session/ConnectionPropsForSessCfg.ts @@ -0,0 +1,569 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { CliUtils, ImperativeConfig, TextUtils } from "../../../utilities"; +import { ICommandArguments, IHandlerParameters } from "../../../cmd"; +import { ImperativeError } from "../../../error"; +import { IOptionsForAddConnProps } from "./doc/IOptionsForAddConnProps"; +import { Logger } from "../../../logger"; +import * as SessConstants from "./SessConstants"; +import { IPromptOptions } from "../../../cmd/src/doc/response/api/handler/IPromptOptions"; +import { ISession } from "./doc/ISession"; +import { IProfileProperty } from "../../../profiles"; +import { ConfigAutoStore } from "../../../config/src/ConfigAutoStore"; +import { ConfigUtils } from "../../../config/src/ConfigUtils"; +import { AuthOrder } from "./AuthOrder"; + +/** + * Extend options for IPromptOptions for internal wrapper method + * @interface IHandlePromptOptions + * @extends {IPromptOptions} + */ +interface IHandlePromptOptions extends IPromptOptions { + + /** + * Adds IHandlerParameters to IPromptOptions + * @type {IHandlerParameters} + * @memberof IHandlePromptOptions + */ + parms?: IHandlerParameters; +} + +/** + * This class adds connection information to an existing session configuration + * object for making REST API calls with the Imperative RestClient. + */ +export class ConnectionPropsForSessCfg { + + // *********************************************************************** + /** + * Create a REST session configuration object starting with the supplied + * initialSessCfg and retrieving connection properties from the command + * line arguments (or environment, or profile). If required connection + * properties are missing we interactively prompt the user for the values. + * for any of the following properties: + * host + * port + * user name + * password + * + * Any prompt will timeout after 30 seconds so that this function can + * be run from an automated script, and will not indefinitely hang that + * script. + * + * In addition to properties that we prompt for, we will also add the following + * properties to the session configuration if they are available. + * type + * tokenType + * tokenValue + * + * @param initialSessCfg + * An initial session configuration (like ISession, or other + * specially defined configuration) that contains your desired + * session configuration properties. + * + * @param cmdArgs + * The arguments specified by the user on the command line + * (or in environment, or in profile). The contents of the + * supplied cmdArgs will be modified. + * + * @param connOpts + * Options that alter our connection actions. See IOptionsForAddConnProps. + * The connOpts parameter need not be supplied. + * + * @example + * // Within the process() function of a command handler, + * // do steps similar to the following: + * const sessCfg: ISession = { + * rejectUnauthorized: commandParameters.arguments.rejectUnauthorized, + * basePath: commandParameters.arguments.basePath + * }; + * const connectableSessCfg = await ConnectionPropsForSessCfg.addPropsOrPrompt( + * sessCfg, commandParameters.arguments + * ); + * mySession = new Session(connectableSessCfg); + * + * @returns A session configuration object with connection information + * added to the initialSessCfg. Its intended use is for our + * caller to create a session for a REST Client. + */ + public static async addPropsOrPrompt( + initialSessCfg: SessCfgType, + cmdArgs: ICommandArguments, + connOpts: IOptionsForAddConnProps = {} + ): Promise { + const impLogger = Logger.getImperativeLogger(); + + /* Create copies of our initialSessCfg and connOpts so that + * we can modify them without changing the caller's copy. + */ + const sessCfgToUse = { ...initialSessCfg }; + const connOptsToUse = { ...connOpts }; + + // resolve all values between sessCfg and cmdArgs using option choices + ConnectionPropsForSessCfg.resolveSessCfgProps( + sessCfgToUse, cmdArgs, connOptsToUse + ); + + // This function will provide all the needed properties in one array + let promptForValues: (keyof SessCfgType & string)[] = []; + const doNotPromptForValues: (keyof SessCfgType & string)[] = []; + + /* Add the override properties to the session object. + */ + if (connOpts.propertyOverrides?.length > 0) { + for (const override of connOpts.propertyOverrides) { + const argName = override.argumentName ?? override.propertyName; + // If the override is found on the session or command arguments, start setting things and do not prompt for overridden properties + if ((sessCfgToUse as any)[override.propertyName] != null || cmdArgs[argName] != null) { + // Set the session config to use the command line argument if it exists. + if (cmdArgs[argName] != null) { (sessCfgToUse as any)[override.propertyName] = cmdArgs[argName]; } + for (const prop of override.propertiesOverridden) { + // Make sure we do not prompt for the overridden property. + if (!doNotPromptForValues.includes(prop)) { doNotPromptForValues.push(prop); } + if (prop in sessCfgToUse) { (sessCfgToUse as any)[prop] = undefined; } + } + } + } + } + + // Set default values on propsToPromptFor + if(connOpts.propsToPromptFor?.length > 0) { + connOpts.propsToPromptFor.forEach(obj => { + if(obj.secure == null) obj.secure = true; + if(obj.secure) this.secureSessCfgProps.add(obj.name.toString()); + promptForValues.push(obj.name as keyof ISession); + this.promptTextForValues[obj.name.toString()] = obj.description; + }); + } + // check what properties are needed to be prompted + if (ConnectionPropsForSessCfg.propHasValue(sessCfgToUse.hostname) === false && !doNotPromptForValues.includes("hostname")) { + promptForValues.push("hostname"); + } + + if ((ConnectionPropsForSessCfg.propHasValue(sessCfgToUse.port) === false || sessCfgToUse.port === 0) && + !doNotPromptForValues.includes("port")) { + promptForValues.push("port"); + } + + const isTokenIrrelevant = ConnectionPropsForSessCfg.propHasValue(sessCfgToUse.tokenValue) === false || + connOpts.supportedAuthTypes && !connOpts.supportedAuthTypes.includes(SessConstants.AUTH_TYPE_TOKEN); + const isCertIrrelevant = ConnectionPropsForSessCfg.propHasValue(sessCfgToUse.cert) === false || + connOpts.supportedAuthTypes && !connOpts.supportedAuthTypes.includes(SessConstants.AUTH_TYPE_CERT_PEM); + if (isTokenIrrelevant && isCertIrrelevant) { + if (ConnectionPropsForSessCfg.propHasValue(sessCfgToUse.user) === false && !doNotPromptForValues.includes("user")) { + promptForValues.push("user"); + } + + if (ConnectionPropsForSessCfg.propHasValue(sessCfgToUse.password) === false && !doNotPromptForValues.includes("password")) { + promptForValues.push("password"); + } + } + + this.loadSecureSessCfgProps(connOptsToUse.parms, promptForValues); + + if (connOptsToUse.getValuesBack == null && connOptsToUse.doPrompting) { + connOptsToUse.getValuesBack = this.getValuesBack(connOptsToUse); + } + + if (connOptsToUse.getValuesBack != null) { + // put all the needed properties in an array and call the external function + const answers = await connOptsToUse.getValuesBack(promptForValues); + + if(connOpts.propsToPromptFor?.length > 0) + { + connOpts.propsToPromptFor.forEach(obj => { + if(obj.isGivenValueValid != null) + { + if(!obj.isGivenValueValid(answers[obj.name])) promptForValues = promptForValues.filter(item => obj.name !== item); + } + }); + } + // validate what values are given back and move it to sessCfgToUse + for (const value of promptForValues) { + if (ConnectionPropsForSessCfg.propHasValue(answers[value])) { + (sessCfgToUse as any)[value] = answers[value]; + } + } + + // + if (connOptsToUse.autoStore !== false && connOptsToUse.parms != null) { + await ConfigAutoStore.storeSessCfgProps(connOptsToUse.parms, sessCfgToUse, promptForValues); + } + } + + AuthOrder.putTopAuthInSession(sessCfgToUse, cmdArgs); + + impLogger.debug("Session config after any prompting for missing values:"); + ConnectionPropsForSessCfg.logSessCfg(sessCfgToUse); + return sessCfgToUse; + } + + // *********************************************************************** + /** + * Resolve the overlapping or mutually exclusive properties that can + * occur. Ensure that the resulting session configuration object contains + * only the applicable properties. The contents of the supplied sessCfg, + * cmdArgs, and connOpts will be modified. + * + * @param sessCfg + * An initial session configuration that contains your desired + * session configuration properties. + * + * @param cmdArgs + * The arguments specified by the user on the command line + * (or in environment, or in profile) + * + * @param connOpts + * Options that alter our actions. See IOptionsForAddConnProps. + * The connOpts parameter need not be supplied. + * The only option values used by this function are: + * connOpts.requestToken + * connOpts.defaultTokenType + * + * @example + * let sessCfg = YouCollectAllProfilePropertiesRelatedToSession(); + * let cmdArgs = YouSetPropertiesRequiredInCmdArgs(); + * ConnectionPropsForSessCfg.resolveSessCfgProps(sessCfg, cmdArgs); + * sessionToUse = new Session(sessCfg); + */ + public static resolveSessCfgProps( + sessCfg: SessCfgType, + cmdArgs: ICommandArguments = { $0: "", _: [] }, + connOpts: IOptionsForAddConnProps = {} + ) { + const impLogger = Logger.getImperativeLogger(); + + // use defaults if caller has not specified these properties. + if (!Object.prototype.hasOwnProperty.call(connOpts, "requestToken")) { + connOpts.requestToken = false; + } + if (!Object.prototype.hasOwnProperty.call(connOpts, "doPrompting")) { + connOpts.doPrompting = true; + } + if (!Object.prototype.hasOwnProperty.call(connOpts, "defaultTokenType")) { + connOpts.defaultTokenType = SessConstants.TOKEN_TYPE_JWT; + } + + /* Override properties from our caller's sessCfg + * with any values from the command line. + */ + if (ConnectionPropsForSessCfg.propHasValue(cmdArgs.host)) { + sessCfg.hostname = cmdArgs.host; + } + if (ConnectionPropsForSessCfg.propHasValue(cmdArgs.port)) { + sessCfg.port = cmdArgs.port; + } + if (ConnectionPropsForSessCfg.propHasValue(cmdArgs.user)) { + sessCfg.user = cmdArgs.user; + } + if (ConnectionPropsForSessCfg.propHasValue(cmdArgs.password)) { + sessCfg.password = cmdArgs.password; + } + + if (connOpts.requestToken) { + // deleting any tokenValue, ensures that basic creds are used to authenticate and get token + delete sessCfg.tokenValue; + } else if (ConnectionPropsForSessCfg.propHasValue(sessCfg.user) === false && + ConnectionPropsForSessCfg.propHasValue(sessCfg.password) === false && + ConnectionPropsForSessCfg.propHasValue(cmdArgs.tokenValue)) { + // set tokenValue if token is in args, and user and password are NOT supplied. + sessCfg.tokenValue = cmdArgs.tokenValue; + } + + // we use a cert when none of user, password, or token are supplied + if (ConnectionPropsForSessCfg.propHasValue(sessCfg.user) === false && + ConnectionPropsForSessCfg.propHasValue(sessCfg.password) === false && + ConnectionPropsForSessCfg.propHasValue(sessCfg.tokenValue) === false && + ConnectionPropsForSessCfg.propHasValue(cmdArgs.certFile)) { + if (ConnectionPropsForSessCfg.propHasValue(cmdArgs.certKeyFile)) { + sessCfg.cert = cmdArgs.certFile; + sessCfg.certKey = cmdArgs.certKeyFile; + } + // else if (ConnectionPropsForSessCfg.propHasValue(cmdArgs.certFilePassphrase)) { + // sessCfg.cert = cmdArgs.certFile; + // sessCfg.passphrase = cmdArgs.certFilePassphrase; + // } + } + + const isTokenUsed = ConnectionPropsForSessCfg.propHasValue(sessCfg.tokenValue) && + (connOpts.supportedAuthTypes == null || connOpts.supportedAuthTypes.includes(SessConstants.AUTH_TYPE_TOKEN)); + const isCertUsed = ConnectionPropsForSessCfg.propHasValue(sessCfg.cert) && + (connOpts.supportedAuthTypes == null || connOpts.supportedAuthTypes.includes(SessConstants.AUTH_TYPE_CERT_PEM)); + if (isTokenUsed) { + // when tokenValue is set at this point, we are definitely using the token. + impLogger.debug("Using token authentication"); + + // override any token type in sessCfg with cmdArgs value + if (ConnectionPropsForSessCfg.propHasValue(cmdArgs.tokenType)) { + sessCfg.tokenType = cmdArgs.tokenType; + } + + // set the auth type based on token type + if (ConnectionPropsForSessCfg.propHasValue(sessCfg.tokenType)) { + sessCfg.type = SessConstants.AUTH_TYPE_TOKEN; + } else { + // When no tokenType supplied, user wants bearer + sessCfg.type = SessConstants.AUTH_TYPE_BEARER; + } + } else if (isCertUsed) { + // when cert property is set at this point, we will use the certificate + if (ConnectionPropsForSessCfg.propHasValue(sessCfg.certKey)) { + impLogger.debug("Using PEM Certificate authentication"); + sessCfg.type = SessConstants.AUTH_TYPE_CERT_PEM; + } + // else if (ConnectionPropsForSessCfg.propHasValue(sessCfg.passphrase)) { + // impLogger.debug("Using PFX Certificate authentication"); + // sessCfg.type = SessConstants.AUTH_TYPE_CERT_PFX; + // } + } else { + // we are using basic auth + impLogger.debug("Using basic authentication"); + sessCfg.type = SessConstants.AUTH_TYPE_BASIC; + } + ConnectionPropsForSessCfg.setTypeForTokenRequest(sessCfg, connOpts, cmdArgs.tokenType); + ConnectionPropsForSessCfg.logSessCfg(sessCfg); + } + + // *********************************************************************** + /** + * Confirm whether the given session has a credentials. + * + * @param sessToTest + * the session to be confirmed. + * + * @returns true is the session has credentials. false otherwise. + */ + public static sessHasCreds(sessToTest: ISession) { + if (sessToTest == null) { + return false; + } + const hasToken = sessToTest.tokenType != null && sessToTest.tokenValue != null; + const hasCert = sessToTest.certKey != null && sessToTest.cert; + const hasBasicAuth = sessToTest.base64EncodedAuth != null; + const hasCreds = sessToTest.user != null && sessToTest.password; + return hasToken || hasCert || hasBasicAuth || hasCreds; + } + + /** + * List of properties on `sessCfg` object that should be kept secret and + * may not appear in Imperative log files. + * + * NOTE(Kelosky): redundant from LoggerUtils.SECURE_PROMPT_OPTIONS - leaving + * for future date to consolidate + */ + private static secureSessCfgProps: Set = new Set(["user", "password", "tokenValue", "passphrase"]); + + /** + * List of prompt messages that is used when the CLI prompts for session + * config values. + */ + private static readonly promptTextForValues: { [key: string]: string } = { + hostname: "Enter the host name of", + port: "Enter the port number of", + user: "Enter the user name for", + password: "Enter the password for" + }; + + /** + * Prompts the user to input session config values in a CLI environment. + * This is the default implementation of the `getValuesBack` callback when + * `connOpts.doPrompting` is true. + * @param connOpts Options for adding connection properties + * @returns Name-value pairs of connection properties + */ + private static getValuesBack(connOpts: IOptionsForAddConnProps): + (properties: string[]) => Promise<{ [key: string]: any }> { + return async (promptForValues: string[]) => { + /* The check for console.log in the following 'if' statement is only needed for tests + * which do not create a mock for the connOpts.parms.response.console.log property. + * In the real world, that property always exists for this CLI-only path of logic. + */ + if (promptForValues.length > 0 && connOpts.parms?.response.console.log) { + // We need to prompt for some values. Determine why we need to prompt. + let reasonForPrompts: string = ""; + if (ImperativeConfig.instance.config?.exists) { + reasonForPrompts += "Some required connection properties have not been specified " + + "in your Zowe client configuration. "; + } else if (ConfigUtils.onlyV1ProfilesExist) { + reasonForPrompts += "Only V1 profiles exist. V1 profiles are no longer supported. " + + "You should convert your V1 profiles to a newer Zowe client configuration. "; + } else { + reasonForPrompts += "No Zowe client configuration exists. "; + } + + reasonForPrompts += "Therefore, you will be asked for the connection properties " + + "that are required to complete your command.\n"; + connOpts.parms.response.console.log(TextUtils.wordWrap( + TextUtils.chalk.yellowBright(reasonForPrompts)) + ); + } + + const answers: { [key: string]: any } = {}; + const profileSchema = this.loadSchemaForSessCfgProps(connOpts.parms, promptForValues); + const serviceDescription = connOpts.serviceDescription || "your service"; + + for (const value of promptForValues) { + let answer; + while (answer === undefined) { + const hideText = profileSchema[value]?.secure || this.secureSessCfgProps.has(value); + const valuePrompt = this.promptTextForValues[value] ?? `Enter your ${value} for`; + let promptText = `${valuePrompt} ${serviceDescription}`; + if (hideText) { + promptText += " (will be hidden)"; + } + answer = await this.clientPrompt(`${promptText}: `, { hideText, parms: connOpts.parms }); + if (answer === null) { + throw new ImperativeError({ msg: `Timed out waiting for ${value}.` }); + } + } + if (profileSchema[value]?.type === "number") { + answer = Number(answer); + if (isNaN(answer)) { + throw new ImperativeError({ msg: `Specified ${value} was not a number.` }); + } + } + answers[value] = answer; + } + + return answers; + }; + } + + /** + * Handle prompting for clients. If in a CLI environment, use the IHandlerParameters.response + * object prompt method. + * @private + * @static + * @param {string} promptText + * @param {IHandlePromptOptions} opts + * @returns {Promise} + * @memberof ConnectionPropsForSessCfg + */ + private static async clientPrompt(promptText: string, opts: IHandlePromptOptions): Promise { + if (opts.parms) { + return opts.parms.response.console.prompt(promptText, opts); + } else { + return CliUtils.readPrompt(promptText, opts); + } + } + + // *********************************************************************** + /** + * Determine if we want to request a token. + * Set the session's type and tokenType accordingly. + * + * @param sessCfg + * The session configuration to be updated. + * + * @param options + * Options that alter our actions. See IOptionsForAddConnProps. + * + * @param tokenType + * The type of token that we expect to receive. + */ + private static setTypeForTokenRequest( + sessCfg: SessCfgType, + options: IOptionsForAddConnProps, + tokenType: SessConstants.TOKEN_TYPE_CHOICES + ) { + const impLogger = Logger.getImperativeLogger(); + if (options.requestToken) { + impLogger.debug("Requesting a token"); + if (sessCfg.type === SessConstants.AUTH_TYPE_BASIC) { + // Set our type to token to get a token from user and pass + sessCfg.type = SessConstants.AUTH_TYPE_TOKEN; + } + sessCfg.tokenType = tokenType || sessCfg.tokenType || options.defaultTokenType; + } + } + + // *********************************************************************** + /** + * Log the session configuration that resulted from the addition of + * credentials. Hide the password. + * + * @param sessCfg + * The session configuration to be logged. + */ + private static logSessCfg(sessCfg: any) { + const impLogger = Logger.getImperativeLogger(); + + // create copy of sessCfg and obscure secure fields for displaying in the log + const sanitizedSessCfg = JSON.parse(JSON.stringify(sessCfg)); + for (const secureProp of ConnectionPropsForSessCfg.secureSessCfgProps) { + if (sanitizedSessCfg[secureProp] != null) { + sanitizedSessCfg[secureProp] = `${secureProp}_is_hidden`; + } + } + impLogger.debug("Creating a session config with these properties:\n" + + JSON.stringify(sanitizedSessCfg, null, 2) + ); + } + + // *********************************************************************** + /** + * Confirm whether the specified property has a value. + * + * @param propToTest + * the property key to be confirmed. + * + * @returns true is the property exists and has a value. false otherwise. + */ + private static propHasValue(propToTest: any) { + return propToTest != null && propToTest !== ""; + } + + /** + * Load base profile property schema for connection properties. + * @param params CLI handler parameters object + * @param promptForValues List of ISessCfg properties to prompt for + * @returns Key-value pairs of ISessCfg property name and profile property schema + */ + private static loadSchemaForSessCfgProps(params: IHandlerParameters | undefined, promptForValues: string[]): { [key: string]: IProfileProperty } { + if (params == null || ImperativeConfig.instance.loadedConfig?.baseProfile == null) { + return {}; + } + + const schemas: { [key: string]: IProfileProperty } = {}; + for (const propName of promptForValues) { + const profilePropName = propName === "hostname" ? "host" : propName; + schemas[propName] = ImperativeConfig.instance.loadedConfig.baseProfile.schema.properties[profilePropName]; + } + return schemas; + } + + /** + * Load list of secure property names defined in team config. + * @param params CLI handler parameters object + * @param promptForValues List of ISessCfg properties to prompt for + */ + private static loadSecureSessCfgProps(params: IHandlerParameters | undefined, promptForValues: string[]): void { + if (params == null || !ImperativeConfig.instance.config?.exists) { + return; + } + + // Find profile that includes all the properties being prompted for + const profileProps = promptForValues.map(propName => propName === "hostname" ? "host" : propName); + const profileData = ConfigAutoStore.findActiveProfile(params, profileProps); + if (profileData == null) { + return; + } + + // Load secure property names that are defined for active profiles + const config = ImperativeConfig.instance.config; + const baseProfileName = ConfigUtils.getActiveProfileName("base", params.arguments); + for (const secureProp of [...config.api.secure.securePropsForProfile(profileData[1]), + ...config.api.secure.securePropsForProfile(baseProfileName)]) { + this.secureSessCfgProps.add(secureProp === "host" ? "hostname" : secureProp); + } + } +} \ No newline at end of file diff --git a/prototypes/packages/imperative/src/rest/src/session/Readme.md b/prototypes/packages/imperative/src/rest/src/session/Readme.md new file mode 100644 index 0000000000..5a1554004d --- /dev/null +++ b/prototypes/packages/imperative/src/rest/src/session/Readme.md @@ -0,0 +1,7 @@ +Files used to create a prototype of AuthOrder are: + +prototypes\packages\imperative\src\rest\src\session\AuthOrder.ts + +prototypes\packages\imperative\src\rest\src\session\ConnectionPropsForSessCfg.ts + +prototypes\packages\cli\src\zosfiles\list\ds\DataSet.handler.ts