diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java index 607636127e..6eb82f8c23 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java @@ -28,12 +28,17 @@ import org.springframework.context.annotation.Primary; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; -import org.zowe.apiml.security.*; +import org.zowe.apiml.security.ApimlPoolingHttpClientConnectionManager; +import org.zowe.apiml.security.HttpsConfig; +import org.zowe.apiml.security.HttpsConfigError; +import org.zowe.apiml.security.HttpsFactory; +import org.zowe.apiml.security.SecurityUtils; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; + import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -173,10 +178,10 @@ public void init() { } catch (HttpsConfigError e) { log.error("Invalid configuration of HTTPs: {}", e.getMessage()); - System.exit(1); // NOSONAR + System.exit(1); } catch (Exception e) { log.error("Cannot construct configuration of HTTPs: {}", e.getMessage()); - System.exit(1); // NOSONAR + System.exit(1); } } diff --git a/certificate-analyser/README.md b/certificate-analyser/README.md index 7327a14915..00e5b6705c 100644 --- a/certificate-analyser/README.md +++ b/certificate-analyser/README.md @@ -3,6 +3,7 @@ ### Usage java -jar certificate-analyser-.jar --help + ``` Usage:
[-hl] [-kp[=]] [-tp[=]] [-a=] [-k=] [-kt=] @@ -28,9 +29,10 @@ Usage:
[-hl] [-kp[=]] [-tp[=]] -tt, --truststoretype= Truststore type, default is PKCS12 ``` + *NOTE* -keypasswd - if you specify this parameter without a value(e.g. java -jar --keypasswd), you will be asked to enter the password +keypasswd - if you specify this parameter without a value(e.g. java -jar --keypasswd), you will be asked to enter the password trustpasswd - if you specify this parameter without a value(e.g. java -jar --trustpasswd), you will be asked to enter the password - if this parameter is omitted completely, value from keypasswd will be used @@ -39,14 +41,12 @@ truststoretype - if this parameter is omitted completely, value from keystoretyp ### Do local handshake -java -jar -Djavax.net.debug=ssl:handshake:verbose certificate-analyser-.jar --keystore ../../../keystore/localhost/localhost.keystore.p12 --truststore ../../../keystore/localhost/localhost.truststore.p12 --keypasswd password --keyalias localhost --local +java -jar -Djavax.net.debug=ssl:handshake:verbose certificate-analyser-.jar --keystore ../../../keystore/localhost/localhost.keystore.p12 --truststore ../../../keystore/localhost/localhost.truststore.p12 --keypasswd password --keyalias localhost --local ### Keyring If you are using SAF keyrings, you need to provide an additional parameter in command line `-Djava.protocol.handler.pkgs=com.ibm.crypto.provider`. -### Possible issues: +### Possible issues Keystore/truststore is owned by different user - permission error. Temporarily Change read permission to all. - - diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java b/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java index a70034b43d..666f643238 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java @@ -80,6 +80,4 @@ public ResponseEntity forwardLogout( class LoginRequest { private String username; private char[] password; - } - diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/health/GatewayHealthIndicator.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/health/GatewayHealthIndicator.java index bfac341721..b6b206b539 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/health/GatewayHealthIndicator.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/health/GatewayHealthIndicator.java @@ -10,7 +10,6 @@ package org.zowe.apiml.gateway.health; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; @@ -33,7 +32,6 @@ public class GatewayHealthIndicator extends AbstractHealthIndicator { private final Providers loginProviders; private String apiCatalogServiceId; - @Autowired public GatewayHealthIndicator(DiscoveryClient discoveryClient, Providers providers, @Value("${apiml.catalog.serviceId:}") String apiCatalogServiceId) { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/Providers.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/Providers.java index 2e1ac964c8..1b21fa8a97 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/Providers.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/Providers.java @@ -16,6 +16,8 @@ import org.springframework.security.authentication.AuthenticationServiceException; import org.zowe.apiml.gateway.security.config.CompoundAuthProvider; import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; @@ -27,6 +29,9 @@ public class Providers { private final CompoundAuthProvider compoundAuthProvider; private final ZosmfService zosmfService; + @InjectApimlLogger + private ApimlLogger apimlLog = ApimlLogger.empty(); + /** * This method decides whether the Zosmf service is available. * @@ -34,11 +39,24 @@ public class Providers { * @throws AuthenticationServiceException if the z/OSMF service id is not configured */ public boolean isZosmfAvailable() { - boolean isZosmfRegisteredAndPropagated = !this.discoveryClient.getInstances(authConfigurationProperties.validatedZosmfServiceId()).isEmpty(); - log.debug("zOSMF registered with the Discovery Service and propagated to Gateway: {}", isZosmfRegisteredAndPropagated); + String zosmfServiceId = authConfigurationProperties.validatedZosmfServiceId(); + boolean isZosmfRegisteredAndPropagated = !this.discoveryClient.getInstances(zosmfServiceId).isEmpty(); + if (!isZosmfRegisteredAndPropagated) { + apimlLog.log("org.zowe.apiml.security.auth.zosmf.serviceId", zosmfServiceId); + } + log.debug("z/OSMF registered with the Discovery Service and propagated to Gateway: {}", isZosmfRegisteredAndPropagated); return isZosmfRegisteredAndPropagated; } + /** + * Provide configured z/OSMF service ID from the Gateway auth configuration. + * + * @return service ID of z/OSMF instance + */ + public String getZosmfServiceId() { + return authConfigurationProperties.validatedZosmfServiceId(); + } + /** * Verify that the zOSMF is registered in the Discovery service and that we can actually reach it. * @@ -48,11 +66,12 @@ public boolean isZosmfAvailableAndOnline() { try { boolean isAvailable = isZosmfAvailable(); boolean isAccessible = zosmfService.isAccessible(); - log.debug("zOSMF is registered and propagated to the DS: {} and is accessible based on the information: {}", isAvailable, isAccessible); + + log.debug("z/OSMF is registered and propagated to the DS: {} and is accessible based on the information: {}", isAvailable, isAccessible); return isAvailable && isAccessible; - } catch (ServiceNotAccessibleException exception) { - log.debug("zOSMF isn't registered to the Gateway yet"); + } catch (ServiceNotAccessibleException e) { + log.debug("z/OSMF is not registered to the Gateway yet: {}", e.getMessage()); return false; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/JwtSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/JwtSecurity.java index 80899784ea..31a8af5a87 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/JwtSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/JwtSecurity.java @@ -10,6 +10,7 @@ package org.zowe.apiml.gateway.security.service; +import com.google.common.annotations.VisibleForTesting; import com.netflix.discovery.CacheRefreshedEvent; import com.netflix.discovery.EurekaEvent; import com.netflix.discovery.EurekaEventListener; @@ -38,10 +39,7 @@ import java.security.interfaces.RSAPublicKey; import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; +import java.util.*; import static org.awaitility.Awaitility.await; @@ -77,16 +75,19 @@ public class JwtSecurity { private final Providers providers; private final ZosmfListener zosmfListener; + private final String zosmfServiceId; - private final List events = new ArrayList<>(); + private final Set events = Collections.synchronizedSet(new HashSet<>()); @Autowired public JwtSecurity(Providers providers, ApimlDiscoveryClient discoveryClient) { this.providers = providers; + this.zosmfServiceId = providers.getZosmfServiceId(); this.zosmfListener = new ZosmfListener(discoveryClient); } - public JwtSecurity(Providers providers, String keyAlias, String keyStore, char[] keyStorePassword, char[] keyPassword, ApimlDiscoveryClient discoveryClient) { + @VisibleForTesting + JwtSecurity(Providers providers, String keyAlias, String keyStore, char[] keyStorePassword, char[] keyPassword, ApimlDiscoveryClient discoveryClient) { this(providers, discoveryClient); this.keyStore = keyStore; @@ -120,8 +121,8 @@ public void loadAppropriateJwtKeyOrFail() { loadJwtSecret(); switch (used) { case ZOSMF: - log.info("zOSMF is used as the JWT producer"); - events.add("zOSMF is recognized as authentication provider."); + log.info("z/OSMF instance {} is used as the JWT producer", zosmfServiceId); + events.add(String.format("z/OSMF instance %s is recognized as authentication provider.", zosmfServiceId)); validateInitializationAgainstZosmf(); break; case APIML: @@ -130,8 +131,8 @@ public void loadAppropriateJwtKeyOrFail() { validateJwtSecret(); break; case UNKNOWN: - log.info("zOSMF is probably used as the JWT producer but isn't available yet."); - events.add("Wait for zOSMF to come online before deciding who provides JWT tokens."); + log.info("z/OSMF instance {} is probably used as the JWT producer but isn't available yet.", zosmfServiceId); + events.add(String.format("Wait for z/OSMF instance %s to come online before deciding who provides JWT tokens.", zosmfServiceId)); validateInitializationWhenZosmfIsAvailable(); break; default: @@ -208,11 +209,11 @@ private HttpsConfig currentConfig() { private void validateInitializationAgainstZosmf() { if (!providers.zosmfSupportsJwt()) { events.add("API ML is responsible for token generation."); - log.debug("zOSMF is UP and does not support JWT"); + log.debug("z/OSMF instance {} is UP and does not support JWT", zosmfServiceId); validateJwtSecret(); } else { - events.add("zOSMF is UP and supports JWT"); - log.debug("zOSMF is UP and supports JWT"); + events.add(String.format("z/OSMF instance %s is UP and supports JWT", zosmfServiceId)); + log.debug("z/OSMF instance {} is UP and supports JWT", zosmfServiceId); } } @@ -260,16 +261,18 @@ private void validateInitializationWhenZosmfIsAvailable() { new Thread(() -> { try { - events.add("Started waiting for zOSMF to be registered and known by the discovery service"); - log.debug("Waiting for zOSMF to be registered and known by the Discovery Service."); + events.add("Started waiting for z/OSMF instance " + zosmfServiceId + " to be registered and known by the discovery service"); + log.debug("Waiting for z/OSMF instance {} to be registered and known by the Discovery Service.", zosmfServiceId); await() .atMost(Duration.of(timeout, ChronoUnit.MINUTES)) .with() .pollInterval(Durations.ONE_MINUTE) .until(zosmfListener::isZosmfReady); } catch (ConditionTimeoutException e) { - apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); - apimlLog.log("org.zowe.apiml.security.zosmfInstanceNotFound", "zOSMF"); + synchronized (events) { + apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); + } + apimlLog.log("org.zowe.apiml.security.zosmfInstanceNotFound", zosmfServiceId); System.exit(1); } }).start(); @@ -278,6 +281,7 @@ private void validateInitializationWhenZosmfIsAvailable() { /** * Only for unit testing */ + @VisibleForTesting ZosmfListener getZosmfListener() { return zosmfListener; } @@ -299,10 +303,10 @@ public void onEvent(EurekaEvent event) { } events.add("Discovery Service Cache was updated."); - log.debug("Trying to reach the zOSMF."); + log.debug("Trying to reach the z/OSMF instance " + zosmfServiceId + "."); if (providers.isZosmfAvailableAndOnline()) { - events.add("zOSMF is avaiable and online."); - log.debug("The zOSMF was reached "); + events.add("z/OSMF instance " + zosmfServiceId + " is available and online."); + log.debug("The z/OSMF instance {} was reached.", zosmfServiceId); discoveryClient.unregisterEventListener(this); // only need to see zosmf up once to validate jwt secret isZosmfReady = true; @@ -310,9 +314,13 @@ public void onEvent(EurekaEvent event) { try { validateInitializationAgainstZosmf(); } catch (HttpsConfigError e) { - apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); + synchronized (events) { + apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); + } System.exit(1); } + } else { + events.add("z/OSMF instance " + zosmfServiceId + " is not available and online yet."); } } }; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java index 0ce546f408..6f2321583a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java @@ -20,6 +20,7 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestTemplate; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; @@ -28,6 +29,9 @@ import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.util.EurekaUtils; +import javax.net.ssl.SSLHandshakeException; + +import java.net.ConnectException; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.Supplier; @@ -144,15 +148,38 @@ protected String getURI(String zosmf) { */ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re) { if (re instanceof ResourceAccessException) { + if (re.getCause() instanceof SSLHandshakeException) { + log.error("SSL Misconfiguration, z/OSMF is not accessible. Please verify the following: \n" + + " - CN (Common Name) and z/OSMF hostname have to match.\n" + + " - Certificate is expired\n" + + " - TLS version match\n" + + "Further details and a stack trace will follow", re); + } apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getMessage()); return new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } if (re instanceof HttpClientErrorException.Unauthorized) { + log.warn("Request to z/OSMF requires authentication", re.getMessage()); return new BadCredentialsException("Invalid Credentials"); } + if (re instanceof RestClientResponseException) { + RestClientResponseException responseException = (RestClientResponseException) re; + if (log.isTraceEnabled()) { + log.trace("z/OSMF request {} failed with status code {}, server response: {}", url, responseException.getRawStatusCode(), responseException.getResponseBodyAsString()); + } else { + log.debug("z/OSMF request {} failed with status code {}", url, responseException.getRawStatusCode()); + } + } + + if (re.getCause() instanceof ConnectException) { + log.warn("Could not connecto to z/OSMF. Please verify z/OSMF instance is up and running {}", re.getMessage()); + return new ServiceNotAccessibleException("Could not connect to z/OSMF service."); + } + if (re instanceof RestClientException) { + log.debug("z/OSMF isn't accessible. {}", re.getMessage()); apimlLog.log("org.zowe.apiml.security.generic", re.getMessage(), url); return new AuthenticationServiceException("A failure occurred when authenticating.", re); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfAuthResponse.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfAuthResponse.java new file mode 100644 index 0000000000..fc7e8ff889 --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfAuthResponse.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +package org.zowe.apiml.gateway.security.service.zosmf; + +import lombok.Data; + +@Data +public class ZosmfAuthResponse { + + private int returnCode; + private int reasonCode; + private String message; + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java index 919cb3f8da..cd8439cdbf 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java @@ -15,20 +15,34 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import com.nimbusds.jose.jwk.JWKSet; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; -import org.springframework.http.*; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; -import org.springframework.web.client.*; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.login.ChangePasswordRequest; @@ -36,8 +50,13 @@ import org.zowe.apiml.security.common.token.TokenNotValidException; import javax.annotation.PostConstruct; + +import java.io.IOException; import java.text.ParseException; -import java.util.*; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.JWT; import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.LTPA; @@ -84,7 +103,6 @@ public static class AuthenticationResponse { @JsonIgnoreProperties(ignoreUnknown = true) public static class ZosmfInfo { - @JsonProperty("zosmf_version") private int version; @@ -209,20 +227,25 @@ public boolean isAccessible() { headers.add(ZOSMF_CSRF_HEADER, ""); String infoURIEndpoint = getURI(getZosmfServiceId()) + ZOSMF_INFO_END_POINT; - log.debug("Verifying zOSMF accessibility on info endpoint: {}", infoURIEndpoint); + log.debug("Verifying z/OSMF accessibility on info endpoint: {}", infoURIEndpoint); try { - final ResponseEntity info = restTemplateWithoutKeystore.exchange( + final ResponseEntity info = restTemplateWithoutKeystore + .exchange( infoURIEndpoint, HttpMethod.GET, new HttpEntity<>(headers), ZosmfInfo.class ); - return info.getStatusCode() == HttpStatus.OK; - } catch (RestClientException ex) { - log.debug("zOSMF isn't accessible on URI: {}", infoURIEndpoint); + if (info.getStatusCode() != HttpStatus.OK) { + log.error("Unexpected status code {} from z/OSMF accessing URI {}\n" + + "Response from z/OSMF was \"{}\"", info.getStatusCodeValue(), infoURIEndpoint, String.valueOf(info.getBody())); + } + return info.getStatusCode() == HttpStatus.OK; + } catch (RuntimeException ex) { + handleExceptionOnCall(infoURIEndpoint, ex); return false; } } @@ -266,14 +289,36 @@ protected ResponseEntity issueChangePasswordRequest(Authentication authe return restTemplateWithoutKeystore.exchange( url, httpMethod, - new HttpEntity<>(new ChangePasswordRequest((LoginRequest) authentication.getCredentials()), headers), String.class); - } catch (RuntimeException re) { + new HttpEntity<>(new ChangePasswordRequest((LoginRequest) authentication.getCredentials()), headers), + String.class); + } catch (HttpServerErrorException e) { log.warn("The change password endpoint has failed, ensure that the PTF for APAR PH34912 " + - "(https://www.ibm.com/support/pages/apar/PH34912) has been installed and that the user ID and old password you provide are correct."); + "(https://www.ibm.com/support/pages/apar/PH34912) has been installed and that the user ID and old password you provided are correct."); + throw handleServerErrorOnChangePasswordCall(e); + } catch (HttpClientErrorException e) { + throw new BadCredentialsException("Client error in change password: " + e.getResponseBodyAsString(), e); + } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); } } + private RuntimeException handleServerErrorOnChangePasswordCall(HttpServerErrorException e) { + try { + ZosmfAuthResponse response = securityObjectMapper.readValue(e.getResponseBodyAsByteArray(), ZosmfAuthResponse.class); + if (response.getReturnCode() == 4) { + log.error("z/OSMF internal error attempting password change: {}", e.getResponseBodyAsString()); + return new AuthenticationServiceException("z/OSMF internal error: " + e.getResponseBodyAsString()); + } else { + // TODO https://github.com/zowe/api-layer/issues/2995 - API ML will return 401 in these cases now, the message is still not accurate + log.debug("Failed to change password, z/OSMF response: {}", e.getResponseBodyAsString()); + return new BadCredentialsException("Failed to change password, z/OSMF response: " + e.getResponseBodyAsString()); + } + } catch (IOException ioe) { + log.error("Error processing change password response body: {}", ioe.getMessage()); + return new AuthenticationServiceException("Error processing change password response", ioe); + } + } + /** * Check if call to ZOSMF_AUTHENTICATE_END_POINT resolves * diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 66e5a2073b..4b60506a47 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -360,3 +360,12 @@ logging: org.springframework.security: DEBUG org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter: DEBUG org.zowe.apiml.gateway.security: DEBUG + +--- +spring: + profiles: zosmfDebug + +logging: + level: + root: INFO + org.zowe.apiml.gateway.security.service.zosmf: DEBUG diff --git a/gateway-service/src/main/resources/gateway-log-messages.yml b/gateway-service/src/main/resources/gateway-log-messages.yml index 21614d4f15..1636695bd5 100644 --- a/gateway-service/src/main/resources/gateway-log-messages.yml +++ b/gateway-service/src/main/resources/gateway-log-messages.yml @@ -371,3 +371,11 @@ messages: text: "There was an error while reading webfinger configuration" reason: "Webfinger provider contains incorrect configuration." action: "Contact the administrator to validate webfinger configuration in gateway service." + + # z/OSMF warning messages + - key: org.zowe.apiml.security.auth.zosmf.serviceId + number: ZWEAG181 + type: WARNING + text: "apiml.security.auth.zosmf.serviceId = '%s' is either not registered or not online yet." + reason: "An incorrect value of the apiml.security.auth.zosmf.serviceId parameter is set in the configuration or it is not registered." + action: "Ensure that the value of apiml.security.auth.provider is set either to 'dummy' if you want to use dummy mode, or to 'zosmf' if you want to use the z/OSMF authentication provider." diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java index 5eca62ee1f..f6d2154c11 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java @@ -10,35 +10,89 @@ package org.zowe.apiml.gateway.security.service.zosmf; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import org.hamcrest.collection.IsMapContaining; import org.json.JSONException; import org.json.JSONObject; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; -import org.springframework.http.*; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.client.*; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import org.zowe.apiml.security.common.login.ChangePasswordRequest; import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.security.common.token.TokenNotValidException; -import java.util.*; +import javax.net.ssl.SSLHandshakeException; + +import java.net.ConnectException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.LTPA; +@ExtendWith(MockitoExtension.class) class ZosmfServiceTest { + @Captor + private ArgumentCaptor loggingCaptor; + private static final String ZOSMF_ID = "zosmf"; private final AuthConfigurationProperties authConfigurationProperties = mock(AuthConfigurationProperties.class); @@ -191,8 +245,6 @@ void thenAuthenticateWithSuccess() { Authentication authentication = mock(UsernamePasswordAuthenticationToken.class); LoginRequest loginRequest = mock(LoginRequest.class); ZosmfService zosmfService = getZosmfServiceSpy(); - doReturn(true).when(zosmfService).loginEndpointExists(); - ZosmfService.AuthenticationResponse responseMock = mock(ZosmfService.AuthenticationResponse.class); when(authentication.getCredentials()).thenReturn(loginRequest); doReturn(true).when(zosmfService).loginEndpointExists(); @@ -212,7 +264,6 @@ void thenAuthenticateWithSuccess() { ); when(loginRequest.getPassword()).thenReturn("password".toCharArray()); when(authentication.getPrincipal()).thenReturn("principal"); - doReturn(responseMock).when(zosmfService).issueAuthenticationRequest(authentication, eq(any()), any()); ZosmfService.AuthenticationResponse response = zosmfService.authenticate(authentication); @@ -225,12 +276,30 @@ void thenAuthenticateWithSuccess() { @Nested class WhenChangingPassword { + + private LoginRequest loginRequest; + private Authentication authentication; + + private final HttpHeaders requiredHeaders; + private ZosmfService zosmfService; + { + requiredHeaders = new HttpHeaders(); + requiredHeaders.add("X-CSRF-ZOSMF-HEADER", ""); + requiredHeaders.setContentType(MediaType.APPLICATION_JSON); + + loginRequest = new LoginRequest("username", "password".toCharArray(), "newPassword".toCharArray()); + authentication = mock(UsernamePasswordAuthenticationToken.class); + } + + @BeforeEach + void setUp() { + this.zosmfService = getZosmfServiceSpy(); + } + @Test void thenChangePasswordWithSuccess() { LoginRequest loginRequest = new LoginRequest("username", "password".toCharArray(), "newPassword".toCharArray()); Authentication authentication = mock(UsernamePasswordAuthenticationToken.class); - ZosmfService zosmfService = getZosmfServiceSpy(); - when(authentication.getCredentials()).thenReturn(loginRequest); ResponseEntity responseEntity = new ResponseEntity<>("{}", null, HttpStatus.OK); doReturn(responseEntity).when(zosmfService).issueChangePasswordRequest(any(), any(), any()); @@ -240,10 +309,69 @@ void thenChangePasswordWithSuccess() { new HttpEntity<>(loginRequest, null), String.class ); - ResponseEntity response = zosmfService.changePassword(authentication); + ResponseEntity response = zosmfService.changePassword(authentication); assertTrue(response.getStatusCode().is2xxSuccessful()); } + + @Nested + class WhenClientError { + + @Test + void thenChangePasswordWithClientError() { + when(authentication.getCredentials()).thenReturn(loginRequest); + + when(restTemplate.exchange("http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.PUT, + new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), + String.class)) + .thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST)); + + assertThrows(BadCredentialsException.class, () -> zosmfService.changePassword(authentication)); + } + } + + @Nested + class WhenServerError { + @Test + void thenChangePasswordWithServerError() { + when(authentication.getCredentials()).thenReturn(loginRequest); + + when(restTemplate.exchange("http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.PUT, + new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), + String.class)) + .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR)); + + assertThrows(AuthenticationServiceException.class, () -> zosmfService.changePassword(authentication)); + } + + @Test + void thenChangePasswordWithZosmfInternalError() { + when(authentication.getCredentials()).thenReturn(loginRequest); + + when(restTemplate.exchange("http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.PUT, + new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), + String.class)) + .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal error", "{\"returnCode\": 4}".getBytes(), Charset.defaultCharset())); + + assertThrows(AuthenticationServiceException.class, () -> zosmfService.changePassword(authentication)); + } + + @Test + void thenChangePasswordWithZosmfValidationError() { + when(authentication.getCredentials()).thenReturn(loginRequest); + + when(restTemplate.exchange("http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.PUT, + new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), + String.class)) + .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal error", "{\"returnCode\": 8}".getBytes(), Charset.defaultCharset())); + + assertThrows(BadCredentialsException.class, () -> zosmfService.changePassword(authentication)); + } + } } } @@ -628,7 +756,6 @@ void setUp() { doReturn(ZOSMF_URL).when(underTest).getURI(any()); } - @Test void givenZosmfIsAvailable_thenTrueIsReturned() { when(restTemplate.exchange( @@ -641,16 +768,100 @@ void givenZosmfIsAvailable_thenTrueIsReturned() { assertThat(underTest.isAccessible(), is(true)); } - @Test - void givenZosmfIsUnavailable_thenFalseIsReturned() { - when(restTemplate.exchange( - eq(ZOSMF_URL + AbstractZosmfService.ZOSMF_INFO_END_POINT), - eq(HttpMethod.GET), - any(HttpEntity.class), - eq(ZosmfService.ZosmfInfo.class) - )).thenThrow(RestClientException.class); + @Nested + class WhenZosmfIsNotAvailable { + + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingCaptor; + + private Logger logger; + + @BeforeEach + void setUp() { + logger = (Logger) LoggerFactory.getLogger(AbstractZosmfService.class); + logger.detachAndStopAllAppenders(); + logger.getLoggerContext().resetTurboFilterList(); + logger.addAppender(mockedAppender); + logger.setLevel(Level.TRACE); + } + + @AfterEach + void tearDown() { + logger.detachAppender(mockedAppender); + } + + private String loggedValues() { + List values = loggingCaptor.getAllValues(); + assertNotNull(values); + assertFalse(values.isEmpty()); + return values.stream().map(element -> element.getFormattedMessage()).collect(Collectors.joining("\n")); + } + + @Test + void givenZosmfIsUnavailable_thenFalseIsReturned() { + when(restTemplate.exchange( + eq(ZOSMF_URL + AbstractZosmfService.ZOSMF_INFO_END_POINT), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(ZosmfService.ZosmfInfo.class) + )).thenThrow(RestClientException.class); + + assertThat(underTest.isAccessible(), is(false)); + verify(mockedAppender, atLeast(1)).doAppend(loggingCaptor.capture()); + String values = loggedValues(); + assertTrue(values.length() > 0); + assertTrue(values.contains("z/OSMF isn't accessible"), values); + } + + @Test + void givenSSLError_thenFalseAndException() { + when(restTemplate.exchange( + eq(ZOSMF_URL + AbstractZosmfService.ZOSMF_INFO_END_POINT), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(ZosmfService.ZosmfInfo.class) + )).thenThrow(new ResourceAccessException("resource access exception", new SSLHandshakeException("handshake exception"))); + + assertThat(underTest.isAccessible(), is(false)); + verify(mockedAppender, atLeast(1)).doAppend(loggingCaptor.capture()); + String values = loggedValues(); + assertTrue(values.length() > 0); + assertTrue(values.contains("SSL Misconfiguration, z/OSMF is not accessible. Please verify the following:"), values); + assertTrue(values.contains("- CN (Common Name) and z/OSMF hostname have to match."), values); + assertTrue(values.contains("- Certificate is expired"), values); + assertTrue(values.contains("- TLS version match"), values); + } + + @Test + void givenConnectionIssue_thenFalseAndException() { + when(restTemplate.exchange( + eq(ZOSMF_URL + AbstractZosmfService.ZOSMF_INFO_END_POINT), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(ZosmfService.ZosmfInfo.class) + )).thenThrow(new RestClientException("resource access exception", new ConnectException("connection exception"))); + + assertThat(underTest.isAccessible(), is(false)); + verify(mockedAppender, atLeast(1)).doAppend(loggingCaptor.capture()); + String values = loggedValues(); + assertTrue(values.length() > 0); + assertTrue(values.contains("Could not connecto to z/OSMF. Please verify z/OSMF instance is up and running"), values); + assertTrue(values.contains("connection exception"), values); + } - assertThat(underTest.isAccessible(), is(false)); + @Test + void givenUnexpectedStatusCode_thenFalseAndException() { + when(restTemplate.exchange( + eq(ZOSMF_URL + AbstractZosmfService.ZOSMF_INFO_END_POINT), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(ZosmfService.ZosmfInfo.class) + )).thenReturn(new ResponseEntity<>(HttpStatus.NO_CONTENT)); + assertThat(underTest.isAccessible(), is(false)); + } } } } diff --git a/integration-tests/README.md b/integration-tests/README.md index 9dcf3e28aa..054fe37c32 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -2,9 +2,9 @@ # Integration Tests - [Introduction](#introduction) -- [The tests take care of the services.](#the-tests-take-care-of-the-services) +- [The tests take care of the services](#the-tests-take-care-of-the-services) - [Local run of services and then integration tests](#local-run-of-services-and-then-integration-tests) -- [The services run elsewhere.](#the-services-run-elsewhere) +- [The services run elsewhere](#the-services-run-elsewhere) - [Manual testing of Discovery Service in HTTP mode](#manual-testing-of-discovery-service-in-http-mode) - [Running all tests (including slow)](#running-all-tests-including-slow) - [Running a Specific Test](#running-a-specific-test) @@ -20,6 +20,9 @@ The Integration tests can be run against specific setup and instance or they can In this setup the integration test suite starts and stops the service. It is aimed at all runs for testing the integrations off-platform. +**Note:** In this mode, the code assumes both Java and NodeJs are accessible in the PATH, make sure these are the correct versions. +`JAVA_HOME` and `NODE_HOME` can be used to customise their location as well. + **Follow these steps:** Perform a Localhost Quick start when you need to run the tests on your local machine. The setup won't work on the Windows machines as we use production shell scripts in this setup. In case of Window consult [Local run of services and then integration tests](#local-run-of-services-and-then-integration-tests) diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java b/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java index 84a5fce59c..5933745d16 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; //TODO this class doesn't lend itself well to switching of configurations. //attls is integrated in a kludgy way, and deserves a rewrite @@ -61,7 +62,12 @@ private FullApiMediationLayer() { private void prepareNodeJsSampleApp() { List parameters = new ArrayList<>(); - parameters.add("node"); + + // If NODE_HOME is defined in environment variable, use it, otherwise assume in PATH + String path = Optional.ofNullable(System.getenv("NODE_HOME")) + .map(javaHome -> javaHome + "/bin/") + .orElse(""); + parameters.add(path + "node"); parameters.add("src/index.js"); ProcessBuilder builder1 = new ProcessBuilder(parameters);