From d38590751a3854cdd1de1ab96a4e17cae1b65a6a Mon Sep 17 00:00:00 2001 From: Petr Weinfurt Date: Fri, 14 Jul 2023 16:19:35 +0200 Subject: [PATCH 01/20] Improve log events for zosmf detection and verification as JWT producer. Signed-off-by: Petr Weinfurt --- .../gateway/security/login/Providers.java | 9 +++ .../gateway/security/service/JwtSecurity.java | 57 +++++++++++-------- 2 files changed, 43 insertions(+), 23 deletions(-) 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..1902c9151e 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 @@ -39,6 +39,15 @@ public boolean isZosmfAvailable() { 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. * 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..043d94b3df 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 @@ -37,11 +37,9 @@ import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.time.Duration; +import java.time.Instant; 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,12 +75,14 @@ public class JwtSecurity { private final Providers providers; private final ZosmfListener zosmfListener; + private final String zosmfServiceId; - private final List events = new ArrayList<>(); + private final List events = Collections.synchronizedList(new ArrayList<>()); @Autowired public JwtSecurity(Providers providers, ApimlDiscoveryClient discoveryClient) { this.providers = providers; + this.zosmfServiceId = providers.getZosmfServiceId(); this.zosmfListener = new ZosmfListener(discoveryClient); } @@ -120,18 +120,18 @@ 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); + addEvent("z/OSMF instance " + zosmfServiceId + " is recognized as authentication provider."); validateInitializationAgainstZosmf(); break; case APIML: log.info("API ML is used as the JWT producer"); - events.add("API ML is recognized as authentication provider."); + addEvent("API ML is recognized as authentication provider."); 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); + addEvent("Wait for z/OSMF instance " + zosmfServiceId + " to come online before deciding who provides JWT tokens."); validateInitializationWhenZosmfIsAvailable(); break; default: @@ -207,12 +207,12 @@ 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"); + addEvent("API ML is responsible for token generation."); + 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"); + addEvent("z/OSMF instance " + zosmfServiceId + " is UP and supports JWT"); + log.debug("z/OSMF instance {} is UP and supports JWT", zosmfServiceId); } } @@ -260,16 +260,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."); + addEvent("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(); @@ -298,11 +300,11 @@ public void onEvent(EurekaEvent event) { return; } - events.add("Discovery Service Cache was updated."); - log.debug("Trying to reach the zOSMF."); + addEvent("Discovery Service Cache was updated."); + 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 "); + addEvent("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 +312,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 { + addEvent("z/OSMF instance " + zosmfServiceId + " is not available and online yet."); } } }; @@ -338,4 +344,9 @@ public enum JwtProducer { APIML, UNKNOWN } + + private void addEvent(String event) { + Instant time = Instant.now(); + events.add(time + " " + event); + } } From 852457964a4228baf2e7f0607cb7519ad65d49c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Fri, 14 Jul 2023 17:23:06 +0200 Subject: [PATCH 02/20] wip minor changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../java/org/zowe/apiml/product/web/HttpConfig.java | 9 ++++++--- certificate-analyser/README.md | 10 +++++----- .../apiml/gateway/health/GatewayHealthIndicator.java | 2 -- .../gateway/security/service/JwtSecurityTest.java | 8 ++++---- integration-tests/README.md | 7 +++++-- .../zowe/apiml/util/service/FullApiMediationLayer.java | 8 +++++++- 6 files changed, 27 insertions(+), 17 deletions(-) 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..bb8e2fcfd9 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 @@ -23,6 +23,8 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -112,6 +114,7 @@ public class HttpConfig { private SSLContext secureSslContext; private HostnameVerifier secureHostnameVerifier; private EurekaJerseyClientBuilder eurekaJerseyClientBuilder; + private ApplicationContext context; private final Timer connectionManagerTimer = new Timer( "ApimlHttpClientConfiguration.connectionManagerTimer", true); @@ -172,11 +175,11 @@ public void init() { publicKeyCertificatesBase64 = SecurityUtils.loadCertificateChainBase64(httpsConfig); } catch (HttpsConfigError e) { - log.error("Invalid configuration of HTTPs: {}", e.getMessage()); - System.exit(1); // NOSONAR + log.error("Invalid configuration of HTTPs: {}", e.getMessage()); // Why not print stack trace? Should we have a log for stacktraces only? + SpringApplication.exit(context, () -> 1); } catch (Exception e) { log.error("Cannot construct configuration of HTTPs: {}", e.getMessage()); - System.exit(1); // NOSONAR + SpringApplication.exit(context, () -> 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/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/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java index 3452922d12..da0ae92a2f 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java @@ -54,7 +54,7 @@ void setUp() { class WhenInitializedWithValidJWT { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); + underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); } @Test @@ -86,7 +86,7 @@ void givenZosmfConfiguredWithLtpa_thenProperKeysAreInitialized() { class WhenInitializedWithoutValidJWT { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, null, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); + underTest = new JwtSecurity(providers, null, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); } @Test @@ -124,7 +124,7 @@ class WhenZosmfNotOnlineAndAvailableAtStart { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); + underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); } @Test @@ -192,7 +192,7 @@ void givenCacheRefreshedEvents_thenCheckZosmfForEach() { class GetJwkPublicKey { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); + underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); when(providers.isZosfmUsed()).thenReturn(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); From 07623384607547f90bf243b8732f08cb8bce1014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Mon, 17 Jul 2023 10:29:29 +0200 Subject: [PATCH 03/20] wip - logs on wrong request to zosmf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../gateway/security/login/Providers.java | 30 +++++++++++++++++-- .../gateway/security/service/JwtSecurity.java | 18 +++++++---- .../token/ApimlAccessTokenProvider.java | 1 + .../security/service/zosmf/ZosmfService.java | 25 ++++++++++++++-- .../schemes/ZoweJwtSchemeTest.java | 3 ++ 5 files changed, 66 insertions(+), 11 deletions(-) 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 1902c9151e..3a4edefb32 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 @@ -35,7 +35,7 @@ public class Providers { */ public boolean isZosmfAvailable() { boolean isZosmfRegisteredAndPropagated = !this.discoveryClient.getInstances(authConfigurationProperties.validatedZosmfServiceId()).isEmpty(); - log.debug("zOSMF registered with the Discovery Service and propagated to Gateway: {}", isZosmfRegisteredAndPropagated); + log.debug("z/OSMF registered with the Discovery Service and propagated to Gateway: {}", isZosmfRegisteredAndPropagated); return isZosmfRegisteredAndPropagated; } @@ -54,19 +54,43 @@ public String getZosmfServiceId() { * @return true if the service is registered and properly responds. */ public boolean isZosmfAvailableAndOnline() { + // TODO Validate + /* + +GW does not trust its certificate --> seems like this is not really part of the rest template in GW +CN and hostname are not matching --> this is already covered. +certificate is expired --> this is already covered. +cannot find the public certificate (or CA) in the truststore --> no truststore for this communication, probably yes for the discovery service. +not match TLS versions --> now covered in exception validation. +Connection was rejected --> now covered in exception validation. +Unknown hostname (a DNS issue)z/OSMF return unexpected return code (ie. 500) --> now covered in status code validation. + +*/ 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); + + if (!isAccessible) { + log.debug(""); + analyse(); + // Why is it not accessible + } + + // TODO Some errors could be unsalvable? + + 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"); + log.debug("z/OSMF is not registered to the Gateway yet"); return false; } } + private void analyse() { + } + /** * This method decides whether the Zosmf is used for authentication * 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 043d94b3df..a5c247a329 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; @@ -23,6 +24,8 @@ import org.awaitility.core.ConditionTimeoutException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.zowe.apiml.gateway.discovery.ApimlDiscoveryClient; import org.zowe.apiml.gateway.security.login.Providers; @@ -79,15 +82,19 @@ public class JwtSecurity { private final List events = Collections.synchronizedList(new ArrayList<>()); + private ApplicationContext applicationContext; + @Autowired - public JwtSecurity(Providers providers, ApimlDiscoveryClient discoveryClient) { + public JwtSecurity(Providers providers, ApimlDiscoveryClient discoveryClient, ApplicationContext context) { this.providers = providers; this.zosmfServiceId = providers.getZosmfServiceId(); this.zosmfListener = new ZosmfListener(discoveryClient); + this.applicationContext = context; } - public JwtSecurity(Providers providers, String keyAlias, String keyStore, char[] keyStorePassword, char[] keyPassword, ApimlDiscoveryClient discoveryClient) { - this(providers, discoveryClient); + @VisibleForTesting + JwtSecurity(Providers providers, String keyAlias, String keyStore, char[] keyStorePassword, char[] keyPassword, ApimlDiscoveryClient discoveryClient, ApplicationContext context) { + this(providers, discoveryClient, context); this.keyStore = keyStore; this.keyStorePassword = keyStorePassword; @@ -272,7 +279,7 @@ private void validateInitializationWhenZosmfIsAvailable() { apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); } apimlLog.log("org.zowe.apiml.security.zosmfInstanceNotFound", zosmfServiceId); - System.exit(1); + SpringApplication.exit(applicationContext, () -> 1); } }).start(); } @@ -280,6 +287,7 @@ private void validateInitializationWhenZosmfIsAvailable() { /** * Only for unit testing */ + @VisibleForTesting ZosmfListener getZosmfListener() { return zosmfListener; } @@ -315,7 +323,7 @@ public void onEvent(EurekaEvent event) { synchronized (events) { apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); } - System.exit(1); + SpringApplication.exit(applicationContext, () -> 1); } } else { addEvent("z/OSMF instance " + zosmfServiceId + " is not available and online yet."); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java index d73263b1e3..2a1abfb96b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java @@ -172,6 +172,7 @@ public String getToken(String username, int expirationTime, Set scopes) } public boolean isValidForScopes(String jwtToken, String serviceId) { + // FIXME no logs? if (serviceId != null) { QueryResponse parsedToken = authenticationService.parseJwtWithSignature(jwtToken); if (parsedToken != null && parsedToken.getScopes() != null) { 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..77663d9ca0 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 @@ -36,6 +36,9 @@ import org.zowe.apiml.security.common.token.TokenNotValidException; import javax.annotation.PostConstruct; +import javax.net.ssl.SSLHandshakeException; + +import java.net.ConnectException; import java.text.ParseException; import java.util.*; @@ -84,7 +87,6 @@ public static class AuthenticationResponse { @JsonIgnoreProperties(ignoreUnknown = true) public static class ZosmfInfo { - @JsonProperty("zosmf_version") private int version; @@ -209,7 +211,7 @@ 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( @@ -219,9 +221,26 @@ public boolean isAccessible() { ZosmfInfo.class ); + if (info.getStatusCode() != HttpStatus.OK && HttpStatus.Series.resolve(info.getStatusCodeValue()) == HttpStatus.Series.INFORMATIONAL) { // TODO for different ranges. + log.debug("Request to z/OSMF \"{}\" failed with HTTP status code {}", infoURIEndpoint, info.getStatusCodeValue()); + } + return info.getStatusCode() == HttpStatus.OK; + } catch (ResourceAccessException ex) { + if (ex.getCause() instanceof SSLHandshakeException) { + // TODO longer message, specify things to verify, certificate common name, certificate validity + log.error("SSL Misconfiguration, z/OSMF is not accessible", ex); // TODO does it happen? + } else { + log.warn("Exception ", ex); // TODO Verify which cases fall into this + } + return false; } catch (RestClientException ex) { - log.debug("zOSMF isn't accessible on URI: {}", infoURIEndpoint); + // XXX - Which exceptions are being caught here? + if (ex.getCause() instanceof ConnectException) { + log.warn(" {}", ex.getMessage()); + } else { + log.debug("z/OSMF isn't accessible on URI: {}", infoURIEndpoint); // Fix this debug message + } return false; } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java index e700b3c05a..c50be58c19 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java @@ -61,6 +61,9 @@ void givenCorrectClientCertificateInRequest() { @Test void givenInvalidClientCertificateInRequest() { + + // TODO Could these verify logs? + given() .config(SslContext.selfSignedUntrusted) .when() From 959b7f5ec6f47d8cc257a7a71144423c90c67fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Mon, 17 Jul 2023 13:49:18 +0200 Subject: [PATCH 04/20] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../service/zosmf/AbstractZosmfService.java | 15 +++++++++++++++ .../security/service/zosmf/ZosmfService.java | 18 ++---------------- 2 files changed, 17 insertions(+), 16 deletions(-) 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..e1ed228f75 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 @@ -28,6 +28,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,6 +147,13 @@ protected String getURI(String zosmf) { */ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re) { if (re instanceof ResourceAccessException) { + if (re.getCause() instanceof SSLHandshakeException) { + + // TODO longer message, specify things to verify, certificate common name, certificate validity + log.error("SSL Misconfiguration, z/OSMF is not accessible", re); // TODO does it happen? + } else { + log.warn("Exception ", re); // TODO Verify which cases fall into this + } apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getMessage()); return new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } @@ -153,6 +163,11 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re } if (re instanceof RestClientException) { + if (re.getCause() instanceof ConnectException) { + log.warn(" {}", re.getMessage()); + } else { + log.debug("z/OSMF isn't accessible"); // Fix this debug message + } 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/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java index 77663d9ca0..d0dd7470f6 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 @@ -226,22 +226,8 @@ public boolean isAccessible() { } return info.getStatusCode() == HttpStatus.OK; - } catch (ResourceAccessException ex) { - if (ex.getCause() instanceof SSLHandshakeException) { - // TODO longer message, specify things to verify, certificate common name, certificate validity - log.error("SSL Misconfiguration, z/OSMF is not accessible", ex); // TODO does it happen? - } else { - log.warn("Exception ", ex); // TODO Verify which cases fall into this - } - return false; - } catch (RestClientException ex) { - // XXX - Which exceptions are being caught here? - if (ex.getCause() instanceof ConnectException) { - log.warn(" {}", ex.getMessage()); - } else { - log.debug("z/OSMF isn't accessible on URI: {}", infoURIEndpoint); // Fix this debug message - } - + } catch (RuntimeException ex) { + handleExceptionOnCall(infoURIEndpoint, ex); return false; } } From 8f3c1a8fb82e9c98a2556e66b0862580886b04a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Mon, 17 Jul 2023 15:54:39 +0200 Subject: [PATCH 05/20] wip - exception management, pending tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../service/zosmf/AbstractZosmfService.java | 11 +++---- .../security/service/zosmf/ZosmfService.java | 30 ++++++++++++++----- 2 files changed, 26 insertions(+), 15 deletions(-) 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 e1ed228f75..31e472c225 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 @@ -148,13 +148,10 @@ protected String getURI(String zosmf) { protected RuntimeException handleExceptionOnCall(String url, RuntimeException re) { if (re instanceof ResourceAccessException) { if (re.getCause() instanceof SSLHandshakeException) { - - // TODO longer message, specify things to verify, certificate common name, certificate validity - log.error("SSL Misconfiguration, z/OSMF is not accessible", re); // TODO does it happen? + log.error("SSL Misconfiguration, z/OSMF is not accessible", re); } else { - log.warn("Exception ", re); // TODO Verify which cases fall into this + apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getMessage()); } - apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getMessage()); return new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } @@ -164,9 +161,9 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re if (re instanceof RestClientException) { if (re.getCause() instanceof ConnectException) { - log.warn(" {}", re.getMessage()); + log.warn("Please verify z/OSMF instance is up and running {}", re.getMessage()); } else { - log.debug("z/OSMF isn't accessible"); // Fix this debug message + 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/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java index d0dd7470f6..7dcef503ce 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,33 @@ 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.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,11 +49,12 @@ import org.zowe.apiml.security.common.token.TokenNotValidException; import javax.annotation.PostConstruct; -import javax.net.ssl.SSLHandshakeException; -import java.net.ConnectException; 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; @@ -222,7 +236,7 @@ public boolean isAccessible() { ); if (info.getStatusCode() != HttpStatus.OK && HttpStatus.Series.resolve(info.getStatusCodeValue()) == HttpStatus.Series.INFORMATIONAL) { // TODO for different ranges. - log.debug("Request to z/OSMF \"{}\" failed with HTTP status code {}", infoURIEndpoint, info.getStatusCodeValue()); + log.debug("Request to verify z/OSMF availability \"{}\" failed with HTTP status code {}", infoURIEndpoint, info.getStatusCodeValue()); } return info.getStatusCode() == HttpStatus.OK; From d9238bb3d850d07b4a91afe6e94e1e3f78688e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Tue, 18 Jul 2023 15:57:24 +0200 Subject: [PATCH 06/20] remove comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../gateway/security/login/Providers.java | 23 ------------------- 1 file changed, 23 deletions(-) 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 3a4edefb32..8e5fb9fc15 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 @@ -54,30 +54,10 @@ public String getZosmfServiceId() { * @return true if the service is registered and properly responds. */ public boolean isZosmfAvailableAndOnline() { - // TODO Validate - /* - -GW does not trust its certificate --> seems like this is not really part of the rest template in GW -CN and hostname are not matching --> this is already covered. -certificate is expired --> this is already covered. -cannot find the public certificate (or CA) in the truststore --> no truststore for this communication, probably yes for the discovery service. -not match TLS versions --> now covered in exception validation. -Connection was rejected --> now covered in exception validation. -Unknown hostname (a DNS issue)z/OSMF return unexpected return code (ie. 500) --> now covered in status code validation. - -*/ try { boolean isAvailable = isZosmfAvailable(); boolean isAccessible = zosmfService.isAccessible(); - if (!isAccessible) { - log.debug(""); - analyse(); - // Why is it not accessible - } - - // TODO Some errors could be unsalvable? - log.debug("z/OSMF is registered and propagated to the DS: {} and is accessible based on the information: {}", isAvailable, isAccessible); return isAvailable && isAccessible; @@ -88,9 +68,6 @@ Unknown hostname (a DNS issue)z/OSMF return unexpected return code (ie. 500) --> } } - private void analyse() { - } - /** * This method decides whether the Zosmf is used for authentication * From 7441a7df8c97f4646a49d535bcb0b91512be3e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Tue, 18 Jul 2023 17:23:31 +0200 Subject: [PATCH 07/20] update message after test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../security/service/zosmf/AbstractZosmfService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 31e472c225..94d9e330a5 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 @@ -148,20 +148,24 @@ 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", re); - } else { - apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getMessage()); + 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()); // TODO can we get the URI? return new BadCredentialsException("Invalid Credentials"); } if (re instanceof RestClientException) { if (re.getCause() instanceof ConnectException) { - log.warn("Please verify z/OSMF instance is up and running {}", re.getMessage()); + log.warn("Could not connecto to z/OSMF. Please verify z/OSMF instance is up and running {}", re.getMessage()); } else { log.debug("z/OSMF isn't accessible. {}", re.getMessage()); } From 669dce664d6a2069533a441ceb2687efad7c0de4 Mon Sep 17 00:00:00 2001 From: sj895092 Date: Thu, 20 Jul 2023 15:49:25 +0200 Subject: [PATCH 08/20] Logging a new WARNING message when z/OSMF is not available and changed Events collection to HashSet to retain only unique events. Signed-off-by: sj895092 --- .../apiml/gateway/security/login/Providers.java | 14 ++++++++++++-- .../gateway/security/service/JwtSecurity.java | 11 ++++------- 2 files changed, 16 insertions(+), 9 deletions(-) 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 8e5fb9fc15..61a42a2884 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,8 +39,13 @@ 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("z/OSMF 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; } 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 a5c247a329..19cd861206 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 @@ -24,7 +24,6 @@ import org.awaitility.core.ConditionTimeoutException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.zowe.apiml.gateway.discovery.ApimlDiscoveryClient; @@ -40,7 +39,6 @@ import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.time.Duration; -import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; @@ -80,7 +78,7 @@ public class JwtSecurity { private final ZosmfListener zosmfListener; private final String zosmfServiceId; - private final List events = Collections.synchronizedList(new ArrayList<>()); + private final Set events = Collections.synchronizedSet(new HashSet<>()); private ApplicationContext applicationContext; @@ -279,7 +277,7 @@ private void validateInitializationWhenZosmfIsAvailable() { apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); } apimlLog.log("org.zowe.apiml.security.zosmfInstanceNotFound", zosmfServiceId); - SpringApplication.exit(applicationContext, () -> 1); + System.exit(1); } }).start(); } @@ -323,7 +321,7 @@ public void onEvent(EurekaEvent event) { synchronized (events) { apimlLog.log("org.zowe.apiml.gateway.jwtProducerConfigError", StringUtils.join(events, "\n")); } - SpringApplication.exit(applicationContext, () -> 1); + System.exit(1); } } else { addEvent("z/OSMF instance " + zosmfServiceId + " is not available and online yet."); @@ -354,7 +352,6 @@ public enum JwtProducer { } private void addEvent(String event) { - Instant time = Instant.now(); - events.add(time + " " + event); + events.add( event); } } From abba8b6b38d4ab45415067075ab8d2dd30d7e4be Mon Sep 17 00:00:00 2001 From: sj895092 Date: Thu, 20 Jul 2023 15:56:05 +0200 Subject: [PATCH 09/20] Added a new WARNING message with code ZWEAG181 when z/OSMF is not available Signed-off-by: sj895092 --- .../src/main/resources/gateway-log-messages.yml | 8 ++++++++ 1 file changed, 8 insertions(+) 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." From 7ea7f5ed4fe80cb1d136eac91ac45fb87fe527bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Fri, 21 Jul 2023 10:50:49 +0200 Subject: [PATCH 10/20] handle change password error return, pending tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../client/api/ZaasClientTestController.java | 2 - .../gateway/security/login/Providers.java | 1 - .../gateway/security/service/JwtSecurity.java | 7 +- .../service/zosmf/AbstractZosmfService.java | 20 +++-- .../service/zosmf/ZosmfAuthResponse.java | 22 +++++ .../security/service/zosmf/ZosmfService.java | 33 ++++++-- .../ZosmfHttpResponseErrorHandlerTest.java | 81 +++++++++++++++++++ .../service/zosmf/ZosmfServiceTest.java | 55 ++++++++++--- 8 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfAuthResponse.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java 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/security/login/Providers.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/Providers.java index 61a42a2884..7ee9c39cb8 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 @@ -39,7 +39,6 @@ public class Providers { * @throws AuthenticationServiceException if the z/OSMF service id is not configured */ public boolean isZosmfAvailable() { - String zosmfServiceId = authConfigurationProperties.validatedZosmfServiceId(); boolean isZosmfRegisteredAndPropagated = !this.discoveryClient.getInstances(zosmfServiceId).isEmpty(); if (!isZosmfRegisteredAndPropagated) { 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 19cd861206..f97f483c3c 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 @@ -80,19 +80,16 @@ public class JwtSecurity { private final Set events = Collections.synchronizedSet(new HashSet<>()); - private ApplicationContext applicationContext; - @Autowired - public JwtSecurity(Providers providers, ApimlDiscoveryClient discoveryClient, ApplicationContext context) { + public JwtSecurity(Providers providers, ApimlDiscoveryClient discoveryClient) { this.providers = providers; this.zosmfServiceId = providers.getZosmfServiceId(); this.zosmfListener = new ZosmfListener(discoveryClient); - this.applicationContext = context; } @VisibleForTesting JwtSecurity(Providers providers, String keyAlias, String keyStore, char[] keyStorePassword, char[] keyPassword, ApimlDiscoveryClient discoveryClient, ApplicationContext context) { - this(providers, discoveryClient, context); + this(providers, discoveryClient); this.keyStore = keyStore; this.keyStorePassword = keyStorePassword; 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 94d9e330a5..216b5f8e2e 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; @@ -159,16 +160,25 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re } if (re instanceof HttpClientErrorException.Unauthorized) { - log.warn("Request to z/OSMF requires authentication", re.getMessage()); // TODO can we get the URI? + log.warn("Request to z/OSMF requires authentication", re.getMessage()); return new BadCredentialsException("Invalid Credentials"); } - if (re instanceof RestClientException) { - if (re.getCause() instanceof ConnectException) { - log.warn("Could not connecto to z/OSMF. Please verify z/OSMF instance is up and running {}", re.getMessage()); + 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 isn't accessible. {}", re.getMessage()); + 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()); + } + + 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 7dcef503ce..cfaf818ccf 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 @@ -37,6 +37,7 @@ 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.HttpClientErrorException; @@ -50,6 +51,7 @@ import javax.annotation.PostConstruct; +import java.io.IOException; import java.text.ParseException; import java.util.EnumMap; import java.util.HashMap; @@ -228,15 +230,17 @@ public boolean isAccessible() { 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 ); - if (info.getStatusCode() != HttpStatus.OK && HttpStatus.Series.resolve(info.getStatusCodeValue()) == HttpStatus.Series.INFORMATIONAL) { // TODO for different ranges. - log.debug("Request to verify z/OSMF availability \"{}\" failed with HTTP status code {}", infoURIEndpoint, info.getStatusCodeValue()); + 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; @@ -262,7 +266,7 @@ protected AuthenticationResponse issueAuthenticationRequest(Authentication authe final ResponseEntity response = restTemplateWithoutKeystore.exchange( url, httpMethod, - new HttpEntity<>(null, headers), String.class); + new HttpEntity<>(null, headers), String.class); // TODO return getAuthenticationResponse(response); } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); @@ -286,13 +290,30 @@ protected ResponseEntity issueChangePasswordRequest(Authentication authe url, httpMethod, new HttpEntity<>(new ChangePasswordRequest((LoginRequest) authentication.getCredentials()), headers), String.class); - } catch (RuntimeException re) { + } 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 handleExceptionOnChangePasswordCall(e); + } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); } } + private RuntimeException handleExceptionOnChangePasswordCall(HttpServerErrorException e) { + try { + ZosmfAuthResponse response = securityObjectMapper.readValue(e.getResponseBodyAsByteArray(), ZosmfAuthResponse.class); + if (response.getReturnCode() == 4) { + return new AuthenticationServiceException("z/OSMF internal error: " + e.getResponseBodyAsString()); + } else { + // TODO https://github.com/zowe/api-layer/issues/2995 + return new BadCredentialsException("Failed to change password, z/OSMF response " + e.getResponseBodyAsString()); + } + } catch (IOException ioe) { + log.debug("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/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java new file mode 100644 index 0000000000..73649ff7cf --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java @@ -0,0 +1,81 @@ +/* + * 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 ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@RestClientTest +@ContextConfiguration(classes = { ZosmfService.class }) +@ExtendWith(MockitoExtension.class) +public class ZosmfHttpResponseErrorHandlerTest { + + @Captor + private ArgumentCaptor loggingCaptor; + + @Mock + private Appender mockedAppender; + + @Autowired + private MockRestServiceServer server; + + @Autowired + @Qualifier("restTemplateWithoutKeystore") + private RestTemplate restTemplateWithoutKeystore; + + @Autowired + private ZosmfService zosmfService; + + @Nested + class WhenVerifyZosmfAvailability { + + @BeforeEach + void setUp() { + assertNotNull(server); + assertNotNull(zosmfService); + } + + // @Test + // void test() { + // server.expect(null); + + // assertRequestLogged(); + // assertFalse(zosmfService.isAccessible()); + // } + } + + private List assertRequestLogged() { + List lines = loggingCaptor.getAllValues(); + assertFalse(lines.isEmpty()); + assertTrue(lines.get(0).contains("")); + return lines; + } + +} 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..8bf9f8b612 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 @@ -15,30 +15,66 @@ import org.hamcrest.collection.IsMapContaining; import org.json.JSONException; import org.json.JSONObject; -import org.junit.jupiter.api.*; +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.junit.jupiter.MockitoExtension; import org.skyscreamer.jsonassert.JSONAssert; 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.ResponseEntity; import org.springframework.security.authentication.AuthenticationServiceException; 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.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.LoginRequest; import org.zowe.apiml.security.common.token.TokenNotValidException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; 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.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 +227,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 +246,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); @@ -230,7 +263,6 @@ 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,7 +272,7 @@ void thenChangePasswordWithSuccess() { new HttpEntity<>(loginRequest, null), String.class ); - ResponseEntity response = zosmfService.changePassword(authentication); + ResponseEntity response = zosmfService.changePassword(authentication); assertTrue(response.getStatusCode().is2xxSuccessful()); } @@ -628,7 +660,6 @@ void setUp() { doReturn(ZOSMF_URL).when(underTest).getURI(any()); } - @Test void givenZosmfIsAvailable_thenTrueIsReturned() { when(restTemplate.exchange( From 7493cde583c6c22f62d876717cd57d537dd236b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Fri, 21 Jul 2023 10:58:04 +0200 Subject: [PATCH 11/20] cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../zowe/apiml/gateway/security/service/JwtSecurity.java | 3 +-- .../apiml/gateway/security/service/JwtSecurityTest.java | 8 ++++---- .../authentication/schemes/ZoweJwtSchemeTest.java | 3 --- 3 files changed, 5 insertions(+), 9 deletions(-) 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 f97f483c3c..e1bc0c0257 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 @@ -24,7 +24,6 @@ import org.awaitility.core.ConditionTimeoutException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.zowe.apiml.gateway.discovery.ApimlDiscoveryClient; import org.zowe.apiml.gateway.security.login.Providers; @@ -88,7 +87,7 @@ public JwtSecurity(Providers providers, ApimlDiscoveryClient discoveryClient) { } @VisibleForTesting - JwtSecurity(Providers providers, String keyAlias, String keyStore, char[] keyStorePassword, char[] keyPassword, ApimlDiscoveryClient discoveryClient, ApplicationContext context) { + JwtSecurity(Providers providers, String keyAlias, String keyStore, char[] keyStorePassword, char[] keyPassword, ApimlDiscoveryClient discoveryClient) { this(providers, discoveryClient); this.keyStore = keyStore; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java index da0ae92a2f..3452922d12 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/JwtSecurityTest.java @@ -54,7 +54,7 @@ void setUp() { class WhenInitializedWithValidJWT { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); + underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); } @Test @@ -86,7 +86,7 @@ void givenZosmfConfiguredWithLtpa_thenProperKeysAreInitialized() { class WhenInitializedWithoutValidJWT { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, null, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); + underTest = new JwtSecurity(providers, null, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); } @Test @@ -124,7 +124,7 @@ class WhenZosmfNotOnlineAndAvailableAtStart { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); + underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); } @Test @@ -192,7 +192,7 @@ void givenCacheRefreshedEvents_thenCheckZosmfForEach() { class GetJwkPublicKey { @BeforeEach void setUp() { - underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient, null); + underTest = new JwtSecurity(providers, keyAlias, "../keystore/localhost/localhost.keystore.p12", "password".toCharArray(), "password".toCharArray(), discoveryClient); when(providers.isZosfmUsed()).thenReturn(false); } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java index c50be58c19..e700b3c05a 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java @@ -61,9 +61,6 @@ void givenCorrectClientCertificateInRequest() { @Test void givenInvalidClientCertificateInRequest() { - - // TODO Could these verify logs? - given() .config(SslContext.selfSignedUntrusted) .when() From 8537588b72897ed02d742bac229b106f9c1e3653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Fri, 21 Jul 2023 14:55:55 +0200 Subject: [PATCH 12/20] change password tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../service/zosmf/AbstractZosmfService.java | 1 + .../security/service/zosmf/ZosmfService.java | 20 ++++--- .../src/main/resources/application.yml | 9 +++ .../service/zosmf/ZosmfServiceTest.java | 57 ++++++++++++++++++- 4 files changed, 79 insertions(+), 8 deletions(-) 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 216b5f8e2e..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 @@ -175,6 +175,7 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re 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) { 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 cfaf818ccf..d960ddeece 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 @@ -266,7 +266,7 @@ protected AuthenticationResponse issueAuthenticationRequest(Authentication authe final ResponseEntity response = restTemplateWithoutKeystore.exchange( url, httpMethod, - new HttpEntity<>(null, headers), String.class); // TODO + new HttpEntity<>(null, headers), String.class); return getAuthenticationResponse(response); } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); @@ -286,30 +286,36 @@ protected ResponseEntity issueChangePasswordRequest(Authentication authe headers.add(ZOSMF_CSRF_HEADER, ""); headers.setContentType(MediaType.APPLICATION_JSON); try { + log.error("authentication: {}, headers: {}", authentication, headers); return restTemplateWithoutKeystore.exchange( url, httpMethod, - new HttpEntity<>(new ChangePasswordRequest((LoginRequest) authentication.getCredentials()), headers), String.class); + 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 provided are correct."); - throw handleExceptionOnChangePasswordCall(e); + 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 handleExceptionOnChangePasswordCall(HttpServerErrorException e) { + 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 - return new BadCredentialsException("Failed to change password, z/OSMF response " + e.getResponseBodyAsString()); + // 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.debug("Error processing change password response body: {}", ioe.getMessage()); + log.error("Error processing change password response body: {}", ioe.getMessage()); return new AuthenticationServiceException("Error processing change password response", ioe); } } 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/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 8bf9f8b612..ef0edbe6c2 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 @@ -28,8 +28,10 @@ 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; @@ -39,6 +41,7 @@ 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; @@ -258,11 +261,24 @@ void thenAuthenticateWithSuccess() { @Nested class WhenChangingPassword { + + private final HttpHeaders requiredHeaders; + private ZosmfService zosmfService; + { + requiredHeaders = new HttpHeaders(); + requiredHeaders.add("X-CSRF-ZOSMF-HEADER", ""); + requiredHeaders.setContentType(MediaType.APPLICATION_JSON); + } + + @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(); ResponseEntity responseEntity = new ResponseEntity<>("{}", null, HttpStatus.OK); doReturn(responseEntity).when(zosmfService).issueChangePasswordRequest(any(), any(), any()); @@ -276,6 +292,45 @@ void thenChangePasswordWithSuccess() { assertTrue(response.getStatusCode().is2xxSuccessful()); } + + @Nested + class WhenClientError { + + @Test + void thenChangePasswordWithClientError() { + LoginRequest loginRequest = new LoginRequest("username", "password".toCharArray(), "newPassword".toCharArray()); + Authentication authentication = mock(UsernamePasswordAuthenticationToken.class); + + 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() { + LoginRequest loginRequest = new LoginRequest("username", "password".toCharArray(), "newPassword".toCharArray()); + Authentication authentication = mock(UsernamePasswordAuthenticationToken.class); + + 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)); + } + } } } From dc1284b52e26951b63829c0d7f4fdfd19b36c1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Fri, 21 Jul 2023 15:11:45 +0200 Subject: [PATCH 13/20] remove log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../zowe/apiml/gateway/security/service/zosmf/ZosmfService.java | 1 - 1 file changed, 1 deletion(-) 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 d960ddeece..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 @@ -286,7 +286,6 @@ protected ResponseEntity issueChangePasswordRequest(Authentication authe headers.add(ZOSMF_CSRF_HEADER, ""); headers.setContentType(MediaType.APPLICATION_JSON); try { - log.error("authentication: {}, headers: {}", authentication, headers); return restTemplateWithoutKeystore.exchange( url, httpMethod, From 4fb5c96c1fd24e12438a064ee6741ff5eff77e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Fri, 21 Jul 2023 15:20:45 +0200 Subject: [PATCH 14/20] rollback changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../org/zowe/apiml/product/web/HttpConfig.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 bb8e2fcfd9..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 @@ -23,19 +23,22 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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; @@ -114,7 +117,6 @@ public class HttpConfig { private SSLContext secureSslContext; private HostnameVerifier secureHostnameVerifier; private EurekaJerseyClientBuilder eurekaJerseyClientBuilder; - private ApplicationContext context; private final Timer connectionManagerTimer = new Timer( "ApimlHttpClientConfiguration.connectionManagerTimer", true); @@ -175,11 +177,11 @@ public void init() { publicKeyCertificatesBase64 = SecurityUtils.loadCertificateChainBase64(httpsConfig); } catch (HttpsConfigError e) { - log.error("Invalid configuration of HTTPs: {}", e.getMessage()); // Why not print stack trace? Should we have a log for stacktraces only? - SpringApplication.exit(context, () -> 1); + log.error("Invalid configuration of HTTPs: {}", e.getMessage()); + System.exit(1); } catch (Exception e) { log.error("Cannot construct configuration of HTTPs: {}", e.getMessage()); - SpringApplication.exit(context, () -> 1); + System.exit(1); } } From 733e0ebc29674ffbf2670b166c4f8e0873f9c664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Fri, 21 Jul 2023 15:41:07 +0200 Subject: [PATCH 15/20] remove test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../gateway/security/login/Providers.java | 4 +- .../ZosmfHttpResponseErrorHandlerTest.java | 81 ------------------- 2 files changed, 2 insertions(+), 83 deletions(-) delete mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java 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 7ee9c39cb8..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 @@ -70,8 +70,8 @@ public boolean isZosmfAvailableAndOnline() { 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("z/OSMF is not 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/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java deleted file mode 100644 index 73649ff7cf..0000000000 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfHttpResponseErrorHandlerTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.Appender; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -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.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.client.MockRestServiceServer; -import org.springframework.web.client.RestTemplate; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@RestClientTest -@ContextConfiguration(classes = { ZosmfService.class }) -@ExtendWith(MockitoExtension.class) -public class ZosmfHttpResponseErrorHandlerTest { - - @Captor - private ArgumentCaptor loggingCaptor; - - @Mock - private Appender mockedAppender; - - @Autowired - private MockRestServiceServer server; - - @Autowired - @Qualifier("restTemplateWithoutKeystore") - private RestTemplate restTemplateWithoutKeystore; - - @Autowired - private ZosmfService zosmfService; - - @Nested - class WhenVerifyZosmfAvailability { - - @BeforeEach - void setUp() { - assertNotNull(server); - assertNotNull(zosmfService); - } - - // @Test - // void test() { - // server.expect(null); - - // assertRequestLogged(); - // assertFalse(zosmfService.isAccessible()); - // } - } - - private List assertRequestLogged() { - List lines = loggingCaptor.getAllValues(); - assertFalse(lines.isEmpty()); - assertTrue(lines.get(0).contains("")); - return lines; - } - -} From 808edddf407046fdb71cc83ab5f2f5a3a1c74d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Mon, 24 Jul 2023 10:11:41 +0200 Subject: [PATCH 16/20] add logs tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../service/zosmf/ZosmfServiceTest.java | 117 ++++++++++++++++-- 1 file changed, 108 insertions(+), 9 deletions(-) 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 ef0edbe6c2..2de5782b83 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,19 +10,27 @@ 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.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.HttpEntity; import org.springframework.http.HttpHeaders; @@ -37,6 +45,7 @@ import org.springframework.test.util.ReflectionTestUtils; 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; @@ -45,9 +54,13 @@ import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.security.common.token.TokenNotValidException; +import javax.net.ssl.SSLHandshakeException; + +import java.net.ConnectException; 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; @@ -61,7 +74,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.contains; 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; @@ -727,16 +742,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); + } - assertThat(underTest.isAccessible(), is(false)); + @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); + } + + @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)); + } } } } From 0bfae6881a36aadf0e19d92dfd95e0b9469a1404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Mon, 24 Jul 2023 10:43:00 +0200 Subject: [PATCH 17/20] fix checkstyle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../apiml/gateway/security/service/zosmf/ZosmfServiceTest.java | 1 - 1 file changed, 1 deletion(-) 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 2de5782b83..27e1fd55e3 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 @@ -74,7 +74,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; From f0f584bf55357c38aa149e4d43312e88287c073c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Mon, 24 Jul 2023 11:17:53 +0200 Subject: [PATCH 18/20] fix smells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../zowe/apiml/gateway/security/service/JwtSecurity.java | 6 +++--- .../security/service/token/ApimlAccessTokenProvider.java | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) 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 e1bc0c0257..9fdcdc9b22 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 @@ -122,7 +122,7 @@ public void loadAppropriateJwtKeyOrFail() { switch (used) { case ZOSMF: log.info("z/OSMF instance {} is used as the JWT producer", zosmfServiceId); - addEvent("z/OSMF instance " + zosmfServiceId + " is recognized as authentication provider."); + addEvent(String.format("z/OSMF instance %s is recognized as authentication provider.", zosmfServiceId)); validateInitializationAgainstZosmf(); break; case APIML: @@ -132,7 +132,7 @@ public void loadAppropriateJwtKeyOrFail() { break; case UNKNOWN: log.info("z/OSMF instance {} is probably used as the JWT producer but isn't available yet.", zosmfServiceId); - addEvent("Wait for z/OSMF instance " + zosmfServiceId + " to come online before deciding who provides JWT tokens."); + addEvent(String.format("Wait for z/OSMF instance %s to come online before deciding who provides JWT tokens.", zosmfServiceId)); validateInitializationWhenZosmfIsAvailable(); break; default: @@ -212,7 +212,7 @@ private void validateInitializationAgainstZosmf() { log.debug("z/OSMF instance {} is UP and does not support JWT", zosmfServiceId); validateJwtSecret(); } else { - addEvent("z/OSMF instance " + zosmfServiceId + " is UP and supports JWT"); + addEvent(String.format("z/OSMF instance %s is UP and supports JWT", zosmfServiceId)); log.debug("z/OSMF instance {} is UP and supports JWT", zosmfServiceId); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java index 2a1abfb96b..d73263b1e3 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/ApimlAccessTokenProvider.java @@ -172,7 +172,6 @@ public String getToken(String username, int expirationTime, Set scopes) } public boolean isValidForScopes(String jwtToken, String serviceId) { - // FIXME no logs? if (serviceId != null) { QueryResponse parsedToken = authenticationService.parseJwtWithSignature(jwtToken); if (parsedToken != null && parsedToken.getScopes() != null) { From 205c33f5d34be5ada4f9ef454926b587206090d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Hern=C3=A1n=20Carle?= Date: Mon, 24 Jul 2023 15:01:29 +0200 Subject: [PATCH 19/20] add tests for change password cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Hernán Carle --- .../service/zosmf/ZosmfServiceTest.java | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) 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 27e1fd55e3..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 @@ -57,6 +57,7 @@ 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; @@ -276,12 +277,18 @@ 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 @@ -312,9 +319,6 @@ class WhenClientError { @Test void thenChangePasswordWithClientError() { - LoginRequest loginRequest = new LoginRequest("username", "password".toCharArray(), "newPassword".toCharArray()); - Authentication authentication = mock(UsernamePasswordAuthenticationToken.class); - when(authentication.getCredentials()).thenReturn(loginRequest); when(restTemplate.exchange("http://zosmf:1433/zosmf/services/authenticate", @@ -331,9 +335,6 @@ void thenChangePasswordWithClientError() { class WhenServerError { @Test void thenChangePasswordWithServerError() { - LoginRequest loginRequest = new LoginRequest("username", "password".toCharArray(), "newPassword".toCharArray()); - Authentication authentication = mock(UsernamePasswordAuthenticationToken.class); - when(authentication.getCredentials()).thenReturn(loginRequest); when(restTemplate.exchange("http://zosmf:1433/zosmf/services/authenticate", @@ -344,6 +345,32 @@ void thenChangePasswordWithServerError() { 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)); + } } } } From edca2f277ce18cf01a141157ce8f901943103e01 Mon Sep 17 00:00:00 2001 From: Petr Weinfurt Date: Tue, 25 Jul 2023 10:48:18 +0200 Subject: [PATCH 20/20] Remove unnecessary private function. Signed-off-by: Petr Weinfurt --- .../gateway/security/service/JwtSecurity.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) 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 9fdcdc9b22..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 @@ -122,17 +122,17 @@ public void loadAppropriateJwtKeyOrFail() { switch (used) { case ZOSMF: log.info("z/OSMF instance {} is used as the JWT producer", zosmfServiceId); - addEvent(String.format("z/OSMF instance %s is recognized as authentication provider.", zosmfServiceId)); + events.add(String.format("z/OSMF instance %s is recognized as authentication provider.", zosmfServiceId)); validateInitializationAgainstZosmf(); break; case APIML: log.info("API ML is used as the JWT producer"); - addEvent("API ML is recognized as authentication provider."); + events.add("API ML is recognized as authentication provider."); validateJwtSecret(); break; case UNKNOWN: log.info("z/OSMF instance {} is probably used as the JWT producer but isn't available yet.", zosmfServiceId); - addEvent(String.format("Wait for z/OSMF instance %s to come online before deciding who provides JWT tokens.", 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 +208,11 @@ private HttpsConfig currentConfig() { */ private void validateInitializationAgainstZosmf() { if (!providers.zosmfSupportsJwt()) { - addEvent("API ML is responsible for token generation."); + events.add("API ML is responsible for token generation."); log.debug("z/OSMF instance {} is UP and does not support JWT", zosmfServiceId); validateJwtSecret(); } else { - addEvent(String.format("z/OSMF instance %s is UP and supports JWT", zosmfServiceId)); + 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); } } @@ -261,7 +261,7 @@ private void validateInitializationWhenZosmfIsAvailable() { new Thread(() -> { try { - addEvent("Started waiting for z/OSMF instance " + zosmfServiceId + " 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)) @@ -302,10 +302,10 @@ public void onEvent(EurekaEvent event) { return; } - addEvent("Discovery Service Cache was updated."); + events.add("Discovery Service Cache was updated."); log.debug("Trying to reach the z/OSMF instance " + zosmfServiceId + "."); if (providers.isZosmfAvailableAndOnline()) { - addEvent("z/OSMF instance " + zosmfServiceId + " is available and online."); + 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 @@ -320,7 +320,7 @@ public void onEvent(EurekaEvent event) { System.exit(1); } } else { - addEvent("z/OSMF instance " + zosmfServiceId + " is not available and online yet."); + events.add("z/OSMF instance " + zosmfServiceId + " is not available and online yet."); } } }; @@ -346,8 +346,4 @@ public enum JwtProducer { APIML, UNKNOWN } - - private void addEvent(String event) { - events.add( event); - } }