From a3309078b994ee36f4a2d3ba5392e7597afde1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Wed, 11 Sep 2024 15:04:54 +0200 Subject: [PATCH] fix: ClosableHttpClient.execute() resource leak on API catalog (#3722) --- api-catalog-services/build.gradle | 1 + .../instance/InstanceRetrievalService.java | 48 +++-- .../status/APIDocRetrievalService.java | 49 +++-- .../staticapi/StaticAPIService.java | 29 +-- .../InstanceRetrievalServiceTest.java | 18 +- .../status/LocalApiDocServiceTest.java | 134 ++++++------- .../staticapi/StaticAPIServiceTest.java | 12 +- api-catalog-ui/frontend/.env | 2 +- apiml-common/build.gradle | 11 ++ .../zowe/apiml/util/HttpClientMockHelper.java | 64 +++++++ .../zowe/apiml/util/config/SslContext.java | 0 .../util/config/SslContextConfigurer.java | 0 apiml-security-common/build.gradle | 1 + .../verify/TrustedCertificatesProvider.java | 30 ++- .../TrustedCertificatesProviderTest.java | 76 +++----- caching-service/build.gradle | 9 +- discovery-service/build.gradle | 2 +- gradle/versions.gradle | 1 + integration-tests/build.gradle | 8 +- security-service-client-spring/build.gradle | 1 + .../client/handler/RestResponseHandler.java | 4 +- .../service/GatewaySecurityService.java | 64 +++---- .../service/GatewaySecurityServiceTest.java | 66 ++----- zaas-client/build.gradle | 4 +- .../exception/ZaasClientErrorCodes.java | 6 +- .../apiml/zaasclient/service/ZaasClient.java | 3 +- .../internal/PassTicketServiceImpl.java | 28 ++- .../service/internal/TokenService.java | 3 +- .../service/internal/ZaasClientImpl.java | 47 +++-- .../internal/ZaasHttpsClientProvider.java | 8 - .../service/internal/ZaasJwtService.java | 180 +++++++----------- .../zaasclient/util/SimpleHttpResponse.java | 93 +++++++++ .../internal/ZaasClientImplHttpsTests.java | 93 +++------ .../ZaasHttpsClientProviderTests.java | 4 +- .../service/internal/ZaasJwtServiceTest.java | 82 ++++---- zaas-service/build.gradle | 2 +- .../zaas/security/mapping/ExternalMapper.java | 42 ++-- .../security/mapping/ExternalMapperTest.java | 15 +- .../mapping/OIDCExternalMapperTest.java | 9 +- 39 files changed, 645 insertions(+), 604 deletions(-) create mode 100644 apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java rename {integration-tests => apiml-common}/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java (100%) rename {integration-tests => apiml-common}/src/testFixtures/java/org/zowe/apiml/util/config/SslContextConfigurer.java (100%) create mode 100644 zaas-client/src/main/java/org/zowe/apiml/zaasclient/util/SimpleHttpResponse.java diff --git a/api-catalog-services/build.gradle b/api-catalog-services/build.gradle index 0252168099..e579870346 100644 --- a/api-catalog-services/build.gradle +++ b/api-catalog-services/build.gradle @@ -82,6 +82,7 @@ dependencies { testImplementation libs.spring.boot.starter.test testImplementation libs.spring.mock.mvc + testImplementation(testFixtures(project(":apiml-common"))) compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java index b10d09fe50..f99c9964c6 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java @@ -16,14 +16,17 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.converters.jackson.EurekaJsonJacksonCodec; import com.netflix.discovery.shared.Applications; +import jakarta.validation.constraints.NotBlank; import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.message.BasicHeader; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; @@ -32,7 +35,6 @@ import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.product.registry.ApplicationWrapper; -import jakarta.validation.constraints.NotBlank; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -139,30 +141,34 @@ private Applications extractApplications(String responseBody) { * @param eurekaServiceInstanceRequest information used to query the discovery service * @return ResponseEntity query response */ - private String queryDiscoveryForInstances(EurekaServiceInstanceRequest eurekaServiceInstanceRequest) throws IOException, ParseException { + private String queryDiscoveryForInstances(EurekaServiceInstanceRequest eurekaServiceInstanceRequest) throws IOException { HttpGet httpGet = new HttpGet(eurekaServiceInstanceRequest.getEurekaRequestUrl()); for (Header header : createRequestHeader(eurekaServiceInstanceRequest)) { httpGet.setHeader(header); } - CloseableHttpResponse response = httpClient.execute(httpGet); - final int statusCode = response.getCode(); - final HttpEntity responseEntity = response.getEntity(); - String responseBody = ""; - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); - } - if (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES) { - return responseBody; - } - apimlLog.log("org.zowe.apiml.apicatalog.serviceRetrievalRequestFailed", - eurekaServiceInstanceRequest.getServiceId(), - eurekaServiceInstanceRequest.getEurekaRequestUrl(), - statusCode, - response.getReasonPhrase() != null ? response.getReasonPhrase() : responseBody + return httpClient.execute(httpGet, response -> { + final int statusCode = response.getCode(); + final HttpEntity responseEntity = response.getEntity(); + + String responseBody = ""; + if (responseEntity != null) { + responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); + } + + if (HttpStatus.valueOf(statusCode).is2xxSuccessful()) { + return responseBody; + } + + apimlLog.log("org.zowe.apiml.apicatalog.serviceRetrievalRequestFailed", + eurekaServiceInstanceRequest.getServiceId(), + eurekaServiceInstanceRequest.getEurekaRequestUrl(), + statusCode, + response.getReasonPhrase() != null ? response.getReasonPhrase() : responseBody ); - return null; + return null; + }); } /** @@ -177,7 +183,7 @@ private InstanceInfo extractSingleInstanceFromApplication(String serviceId, Stri try { application = mapper.readValue(responseBody, ApplicationWrapper.class); } catch (IOException e) { - log.debug("Could not extract service: " + serviceId + " info from discovery --" + e.getMessage(), e); + log.debug("Could not extract service: {} info from discovery --{}", serviceId, e.getMessage(), e); } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIDocRetrievalService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIDocRetrievalService.java index b5c4729ab5..171b4f5e31 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIDocRetrievalService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIDocRetrievalService.java @@ -14,12 +14,10 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.Pair; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.MediaType; @@ -35,8 +33,8 @@ import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.gateway.GatewayClient; -import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.instance.InstanceInitializationException; +import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.RoutedServices; @@ -46,7 +44,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; /** * Retrieves the API documentation for a registered service @@ -60,7 +57,6 @@ @Slf4j public class APIDocRetrievalService { - @Autowired @Qualifier("secureHttpClientWithoutKeystore") private final CloseableHttpClient secureHttpClientWithoutKeystore; @@ -311,17 +307,21 @@ private String getApiDocContentByUrl(@NonNull String serviceId, String apiDocUrl HttpGet httpGet = new HttpGet(apiDocUrl); httpGet.setHeader(org.apache.http.HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); - var responseCodeBodyPair = secureHttpClientWithoutKeystore.execute(httpGet, response -> { - var responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); - return Pair.of(response.getCode(), responseBody); + return secureHttpClientWithoutKeystore.execute(httpGet, response -> { + String responseBody = ""; + var responseEntity = response.getEntity(); + if (responseEntity != null) { + responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); + } + + if (HttpStatus.SC_OK == response.getCode()) { + return responseBody; + } else { + throw new ApiDocNotFoundException("No API Documentation was retrieved due to " + serviceId + + " server error: '" + responseBody + "'."); + } } ); - - if (responseCodeBodyPair.getLeft() != HttpStatus.SC_OK) { - throw new ApiDocNotFoundException("No API Documentation was retrieved due to " + serviceId + - " server error: '" + responseCodeBodyPair.getRight() + "'."); - } - return responseCodeBodyPair.getRight(); } /** @@ -364,19 +364,16 @@ private ApiInfo findApi(List apiInfos, String apiVersion) { String apiId = api.length > 0 ? api[0] : ""; String version = api.length > 1 ? api[1].replace("v", "") : ""; - Optional result = apiInfos.stream() + return apiInfos.stream() .filter( - f -> apiId.equals(f.getApiId()) && (version == null || version.equals(f.getVersion())) + f -> apiId.equals(f.getApiId()) && (version.equals(f.getVersion())) ) - .findFirst(); - - if (!result.isPresent()) { - String errMessage = String.format("Error finding api doc: there is no api doc for '%s %s'.", apiId, version); - log.error(errMessage); - throw new ApiDocNotFoundException(errMessage); - } else { - return result.get(); - } + .findFirst() + .orElseThrow(() -> { + String errMessage = String.format("Error finding api doc: there is no api doc for '%s %s'.", apiId, version); + log.error(errMessage); + return new ApiDocNotFoundException(errMessage); + }); } private InstanceInfo getInstanceInfo(String serviceId) { diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java index d0d53cfbf6..7e7af29f23 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java @@ -14,14 +14,15 @@ import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.HttpEntity; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Base64; @@ -57,18 +58,20 @@ public StaticAPIResponse refresh() { try { HttpPost post = getHttpRequest(discoveryServiceUrl); - CloseableHttpResponse response = httpClient.execute(post); - final HttpEntity responseEntity = response.getEntity(); - String responseBody = ""; - if (responseEntity != null) { - responseBody = new BufferedReader(new InputStreamReader(responseEntity.getContent())).lines().collect(Collectors.joining("\n")); - } - // Return response if successful response or if none have been successful and this is the last URL to try - if (isSuccessful(response) || i == discoveryServiceUrls.size() - 1) { + var staticApiResponse = httpClient.execute(post, response -> { + final HttpEntity responseEntity = response.getEntity(); + String responseBody = ""; + if (responseEntity != null) { + responseBody = new BufferedReader(new InputStreamReader(responseEntity.getContent())).lines().collect(Collectors.joining("\n")); + } return new StaticAPIResponse(response.getCode(), responseBody); - } + }); - } catch (Exception e) { + // Return response if successful or if none have been successful and this is the last URL to try + if (isSuccessful(staticApiResponse) || i == discoveryServiceUrls.size() - 1) { + return staticApiResponse; + } + } catch (IOException e) { log.debug("Error refreshing static APIs from {}, error message: {}", discoveryServiceUrl, e.getMessage()); } } @@ -76,8 +79,8 @@ public StaticAPIResponse refresh() { return new StaticAPIResponse(500, "Error making static API refresh request to the Discovery Service"); } - private boolean isSuccessful(CloseableHttpResponse response) { - return response.getCode() >= 200 && response.getCode() <= 299; + private boolean isSuccessful(StaticAPIResponse response) { + return HttpStatus.valueOf(response.getStatusCode()).is2xxSuccessful(); } private HttpPost getHttpRequest(String discoveryServiceUrl) { diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java index d97c61062b..c7e185b15f 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java @@ -17,7 +17,9 @@ import org.apache.commons.io.IOUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.entity.BasicHttpEntity; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,6 +35,7 @@ import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.instance.InstanceInitializationException; import org.zowe.apiml.product.registry.ApplicationWrapper; +import org.zowe.apiml.util.HttpClientMockHelper; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -58,15 +61,16 @@ class InstanceRetrievalServiceTest { private DiscoveryConfigProperties discoveryConfigProperties; @Mock - CloseableHttpClient httpClient; + private CloseableHttpClient httpClient; + @Mock private CloseableHttpResponse response; @BeforeEach - void setup() throws IOException { - response = mock(CloseableHttpResponse.class); - when(response.getCode()).thenReturn(HttpStatus.SC_OK); - when(httpClient.execute(any())).thenReturn(response); + void setup() { + HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK); + instanceRetrievalService = new InstanceRetrievalService(discoveryConfigProperties, httpClient); } @@ -75,7 +79,7 @@ void whenDiscoveryServiceIsNotAvailable_thenTryOthersFromTheList() throws IOExce when(response.getCode()).thenReturn(HttpStatus.SC_FORBIDDEN).thenReturn(HttpStatus.SC_OK); instanceRetrievalService.getAllInstancesFromDiscovery(false); - verify(httpClient, times(2)).execute(any()); + verify(httpClient, times(2)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); } @Test @@ -137,7 +141,7 @@ void testGetAllInstancesFromDiscovery_whenResponseCodeIsNotSuccess() { } @Test - void testGetAllInstancesFromDiscovery_whenResponseCodeIsSuccessWithUnParsedJsonText() throws IOException { + void testGetAllInstancesFromDiscovery_whenResponseCodeIsSuccessWithUnParsedJsonText() { Applications actualApplications = instanceRetrievalService.getAllInstancesFromDiscovery(false); assertNull(actualApplications); } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java index 79ab45d2a9..35ee59fe3b 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java @@ -11,10 +11,9 @@ package org.zowe.apiml.apicatalog.services.status; import com.netflix.appinfo.InstanceInfo; -import org.apache.commons.lang3.tuple.Pair; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -31,6 +30,7 @@ import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; +import org.zowe.apiml.util.HttpClientMockHelper; import java.io.IOException; import java.util.Collections; @@ -39,7 +39,6 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; @@ -59,9 +58,6 @@ class LocalApiDocServiceTest { private static final String API_ID = "test.app"; private static final String SWAGGER_URL = "https://service:8080/service/api-doc"; - @Mock - private CloseableHttpClient httpClient; - @Mock private InstanceRetrievalService instanceRetrievalService; @@ -70,13 +66,20 @@ class LocalApiDocServiceTest { @Mock private ApimlLogger apimlLogger; + @Mock + private CloseableHttpClient httpClient; + + @Mock + private CloseableHttpResponse response; + @BeforeEach void setup() { - GatewayClient gatewayClient = new GatewayClient(getProperties()); + HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); + apiDocRetrievalService = new APIDocRetrievalService( httpClient, instanceRetrievalService, - gatewayClient); + new GatewayClient(getProperties())); ReflectionTestUtils.setField(apiDocRetrievalService, "apimlLogger", apimlLogger); } @@ -84,13 +87,13 @@ void setup() { @Nested class WhenGetApiDoc { @Test - void givenValidApiInfo_thenReturnApiDoc() throws IOException { + void givenValidApiInfo_thenReturnApiDoc() { String responseBody = "api-doc body"; when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getStandardMetadata(), true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); @@ -115,26 +118,26 @@ void givenNoApiDocFoundForService() { } @Test - void givenServerErrorWhenRequestingSwaggerUrl() throws IOException { + void givenServerErrorWhenRequestingSwaggerUrl() { String responseBody = "Server not found"; when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getStandardMetadata(), true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_INTERNAL_SERVER_ERROR, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_INTERNAL_SERVER_ERROR, responseBody); Exception exception = assertThrows(ApiDocNotFoundException.class, () -> apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)); assertEquals("No API Documentation was retrieved due to " + SERVICE_ID + " server error: '" + responseBody + "'.", exception.getMessage()); } @Test - void givenNoInstanceMetadata() throws IOException { + void givenNoInstanceMetadata() { String responseBody = "api-doc body"; when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(new HashMap<>(), true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); Exception exception = assertThrows(ApiDocNotFoundException.class, () -> apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)); assertEquals("No API Documentation defined for service " + SERVICE_ID + ".", exception.getMessage()); @@ -143,42 +146,45 @@ void givenNoInstanceMetadata() throws IOException { } @Test - void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() throws IOException { - String generatedResponseBody = "{\n" + - " \"swagger\": \"2.0\",\n" + - " \"info\": {\n" + - " \"title\": \"Test service\"\n" + - " , \"description\": \"Test service description\"\n" + - " , \"version\": \"1.0.0\"\n" + - " },\n" + - " \"host\": \"gateway:10000\",\n" + - " \"basePath\": \"/service/api/v1\",\n" + - " \"schemes\": [\"http\"],\n" + - " \"tags\": [\n" + - " {\n" + - " \"name\": \"apimlHidden\"\n" + - " }\n" + - " ],\n" + - " \"paths\": {\n" + - " \"/apimlHidden\": {\n" + - " \"get\": {\n" + - " \"tags\": [\"apimlHidden\"],\n" + - " \"responses\": {\n" + - " \"200\": {\n" + - " \"description\": \"OK\"\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - "}\n"; + void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() { + //language=JSON + String generatedResponseBody = """ + { + "swagger": "2.0", + "info": { + "title": "Test service" + , "description": "Test service description" + , "version": "1.0.0" + }, + "host": "gateway:10000", + "basePath": "/service/api/v1", + "schemes": ["http"], + "tags": [ + { + "name": "apimlHidden" + } + ], + "paths": { + "/apimlHidden": { + "get": { + "tags": ["apimlHidden"], + "responses": { + "200": { + "description": "OK" + } + } + } + } + } + } + """; String responseBody = "api-doc body"; generatedResponseBody = generatedResponseBody.replaceAll("\\s+", ""); when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getMetadataWithoutSwaggerUrl(), true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); @@ -195,13 +201,13 @@ void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() throws IOException { } @Test - void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() throws IOException { + void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() { String responseBody = "api-doc body"; when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getMetadataWithoutApiInfo(), true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); @@ -212,13 +218,13 @@ void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() thr } @Test - void shouldCreateApiDocUrlFromRoutingAndUseHttp() throws IOException { + void shouldCreateApiDocUrlFromRoutingAndUseHttp() { String responseBody = "api-doc body"; when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getMetadataWithoutApiInfo(), false)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); @@ -229,12 +235,12 @@ void shouldCreateApiDocUrlFromRoutingAndUseHttp() throws IOException { } @Test - void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() throws IOException { + void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() { when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getStandardMetadata(), true)); var exception = new IOException("Unable to reach the host"); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenThrow(exception); + HttpClientMockHelper.whenExecuteThenThrow(httpClient, exception); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); @@ -252,14 +258,14 @@ void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() @Nested class WhenGetDefaultApiDoc { @Test - void givenDefaultApiDoc_thenReturnIt() throws IOException { + void givenDefaultApiDoc_thenReturnIt() { String responseBody = "api-doc body"; Map metadata = getMetadataWithMultipleApiInfo(); when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(metadata, true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); @@ -276,7 +282,7 @@ void givenDefaultApiDoc_thenReturnIt() throws IOException { } @Test - void givenNoDefaultApiDoc_thenReturnHighestVersion() throws IOException { + void givenNoDefaultApiDoc_thenReturnHighestVersion() { String responseBody = "api-doc body"; Map metadata = getMetadataWithMultipleApiInfo(); metadata.remove(API_INFO + ".1." + API_INFO_IS_DEFAULT); // unset default API, so higher version becomes default @@ -284,7 +290,7 @@ void givenNoDefaultApiDoc_thenReturnHighestVersion() throws IOException { when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(metadata, true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); @@ -301,13 +307,13 @@ void givenNoDefaultApiDoc_thenReturnHighestVersion() throws IOException { } @Test - void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() throws IOException { + void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() { String responseBody = "api-doc body"; when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getMetadataWithMultipleApiInfoWithDifferentVersionFormat(), true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); @@ -324,13 +330,13 @@ void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() th } @Test - void givenNoApiDocs_thenReturnNull() throws IOException { + void givenNoApiDocs_thenReturnNull() { String responseBody = "api-doc body"; when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) .thenReturn(getStandardInstance(getMetadataWithoutApiInfo(), true)); - when(httpClient.execute(any(), any(HttpClientResponseHandler.class))).thenReturn(Pair.of(HttpStatus.SC_OK, responseBody)); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); @@ -356,9 +362,9 @@ void givenApiVersions_thenReturnThem() { void givenNoApiVersions_thenThrowException() { when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)).thenReturn(null); - Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> { - apiDocRetrievalService.retrieveApiVersions(SERVICE_ID); - }); + Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> + apiDocRetrievalService.retrieveApiVersions(SERVICE_ID) + ); assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); } } @@ -390,9 +396,9 @@ void givenNoDefaultApiVersion_thenReturnHighestVersion() { void givenNoApiInfo_thenThrowException() { when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)).thenReturn(null); - Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> { - apiDocRetrievalService.retrieveDefaultApiVersion(SERVICE_ID); - }); + Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> + apiDocRetrievalService.retrieveDefaultApiVersion(SERVICE_ID) + ); assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); } } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java index fb459f3a67..aad56b809d 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java @@ -13,7 +13,9 @@ import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -23,6 +25,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; +import org.zowe.apiml.util.HttpClientMockHelper; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -162,15 +165,12 @@ void givenNoDiscoveryLocations_whenAttemptRefresh_thenReturn500() { private void mockRestTemplateExchange(String discoveryUrl) throws IOException { HttpPost post = new HttpPost(discoveryUrl.replace("/eureka", "") + REFRESH_ENDPOINT); - when(httpClient.execute(any())).thenAnswer((invocation) -> { + when(httpClient.execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class))).thenAnswer((invocation) -> { HttpPost httpRequest = (HttpPost) invocation.getArguments()[0]; URI uri = httpRequest.getUri(); int i = uri.compareTo(post.getUri()); - if (i == 0) { - return okResponse; - } else { - return notFoundResponse; - } + + return HttpClientMockHelper.invokeResponseHandler(invocation, i == 0 ? okResponse : notFoundResponse); }); } } diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index a3a4f592af..7a9bb5e0b1 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -7,4 +7,4 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.0.21-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO= diff --git a/apiml-common/build.gradle b/apiml-common/build.gradle index caba434f2d..60ab7536b2 100644 --- a/apiml-common/build.gradle +++ b/apiml-common/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "java-test-fixtures" +} + dependencies { api project(':apiml-utility') @@ -17,4 +21,11 @@ dependencies { testCompileOnly libs.lombok annotationProcessor libs.lombok + + testFixturesImplementation libs.spring.boot.starter.test + testFixturesImplementation libs.commons.io + testFixturesImplementation libs.http.client5 + testFixturesImplementation libs.rest.assured + testFixturesImplementation libs.lombok + testFixturesAnnotationProcessor libs.lombok } diff --git a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java new file mode 100644 index 0000000000..f7015e9e04 --- /dev/null +++ b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java @@ -0,0 +1,64 @@ +/* + * 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.util; + +import lombok.SneakyThrows; +import org.apache.commons.io.IOUtils; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.hc.core5.http.io.entity.BasicHttpEntity; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.OngoingStubbing; + +import java.nio.charset.StandardCharsets; + +public class HttpClientMockHelper { + + @SneakyThrows + public static OngoingStubbing whenExecuteThenThrow(CloseableHttpClient httpClientMock, Exception exception) { + return Mockito.when(httpClientMock.execute(ArgumentMatchers.any(ClassicHttpRequest.class), ArgumentMatchers.any(HttpClientResponseHandler.class))) + .thenThrow(exception); + } + + @SneakyThrows + public static OngoingStubbing mockExecuteWithResponse(CloseableHttpClient httpClientMock, ClassicHttpResponse responseMock) { + return Mockito.when(httpClientMock.execute(ArgumentMatchers.any(ClassicHttpRequest.class), ArgumentMatchers.any(HttpClientResponseHandler.class))) + .thenAnswer((InvocationOnMock invocation) -> invokeResponseHandler(invocation, responseMock)); + } + + @SneakyThrows + public static T invokeResponseHandler(InvocationOnMock invocation, ClassicHttpResponse responseMock) { + @SuppressWarnings("unchecked") + HttpClientResponseHandler handler + = (HttpClientResponseHandler) invocation.getArguments()[1]; + return handler.handleResponse(responseMock); + + } + + public static void mockResponse(ClassicHttpResponse responseMock, int statusCode, String responseBody) { + mockResponse(responseMock, statusCode); + mockResponse(responseMock, responseBody); + } + + public static void mockResponse(ClassicHttpResponse responseMock, String responseBody) { + BasicHttpEntity responseEntity = (responseBody == null) ? null : new BasicHttpEntity(IOUtils.toInputStream(responseBody, StandardCharsets.UTF_8), ContentType.APPLICATION_JSON); + Mockito.when(responseMock.getEntity()).thenReturn(responseEntity); + } + + public static void mockResponse(ClassicHttpResponse responseMock, int statusCode) { + Mockito.when(responseMock.getCode()).thenReturn(statusCode); + } +} diff --git a/integration-tests/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java similarity index 100% rename from integration-tests/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java rename to apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java diff --git a/integration-tests/src/testFixtures/java/org/zowe/apiml/util/config/SslContextConfigurer.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContextConfigurer.java similarity index 100% rename from integration-tests/src/testFixtures/java/org/zowe/apiml/util/config/SslContextConfigurer.java rename to apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContextConfigurer.java diff --git a/apiml-security-common/build.gradle b/apiml-security-common/build.gradle index 580a2e2865..7528ad89c8 100644 --- a/apiml-security-common/build.gradle +++ b/apiml-security-common/build.gradle @@ -8,6 +8,7 @@ dependencies { implementation libs.http.client5 testImplementation libs.spring.boot.starter.test + testImplementation(testFixtures(project(":apiml-common"))) compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProvider.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProvider.java index 7f4e1ff14b..fea2bae680 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProvider.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProvider.java @@ -14,9 +14,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.annotation.Cacheable; @@ -75,22 +73,22 @@ public List getTrustedCerts(String certificatesEndpoint) { private String callCertificatesEndpoint(String url) { try { HttpGet httpGet = new HttpGet(new URI(url)); - ClassicHttpResponse httpResponse = httpClient.execute(httpGet); - final int statusCode = httpResponse.getCode(); - String body = ""; - if (httpResponse.getEntity() != null) { - body = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8); - } - if (statusCode != HttpStatus.SC_OK) { - apimlLog.log("org.zowe.apiml.security.common.verify.invalidResponse", url, statusCode, body); - return null; - } - log.debug("Trusted certificates from {}: {}", url, body); - return body; - + return httpClient.execute(httpGet, response -> { + final int statusCode = response.getCode(); + String body = ""; + if (response.getEntity() != null) { + body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + } + if (statusCode != HttpStatus.SC_OK) { + apimlLog.log("org.zowe.apiml.security.common.verify.invalidResponse", url, statusCode, body); + return null; + } + log.debug("Trusted certificates from {}: {}", url, body); + return body; + }); } catch (URISyntaxException e) { apimlLog.log("org.zowe.apiml.security.common.verify.invalidURL", e.getMessage()); - } catch (IOException | ParseException e) { //TODO: Consider a better message + } catch (IOException e) { apimlLog.log("org.zowe.apiml.security.common.verify.httpError", e.getMessage()); } return null; diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProviderTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProviderTest.java index 6cfe1ada7f..029367b5aa 100644 --- a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProviderTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/verify/TrustedCertificatesProviderTest.java @@ -14,24 +14,22 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpStatus; +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.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.util.HttpClientMockHelper; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.reset; @ExtendWith(MockitoExtension.class) class TrustedCertificatesProviderTest { @@ -75,16 +73,23 @@ class TrustedCertificatesProviderTest { private HttpEntity responseEntity; private TrustedCertificatesProvider provider; + @BeforeEach + void setup() { + HttpClientMockHelper.mockExecuteWithResponse(closeableHttpClient, httpResponse); + provider = new TrustedCertificatesProvider(closeableHttpClient); + } + + @AfterEach + void tearDown() { + reset(httpResponse); + } + @Nested class GivenResponseWithValidCertificate { @Test - void whenGetTrustedCerts_thenCertificatesReturned() throws UnsupportedOperationException, IOException { - when(httpResponse.getCode()).thenReturn(HttpStatus.SC_OK); - when(httpResponse.getEntity()).thenReturn(responseEntity); - when(responseEntity.getContent()).thenReturn(new ByteArrayInputStream(VALID_CERTIFICATE.getBytes())); - when(closeableHttpClient.execute(any())).thenReturn(httpResponse); - provider = new TrustedCertificatesProvider(closeableHttpClient); + void whenGetTrustedCerts_thenCertificatesReturned() throws UnsupportedOperationException { + HttpClientMockHelper.mockResponse(httpResponse, HttpStatus.SC_OK, VALID_CERTIFICATE); List result = provider.getTrustedCerts(CERTS_URL); assertNotNull(result); assertEquals(1, result.size()); @@ -95,16 +100,16 @@ void whenGetTrustedCerts_thenCertificatesReturned() throws UnsupportedOperationE @Test void whenInvalidUrl_thenNoCertificatesReturned() { - provider = new TrustedCertificatesProvider(closeableHttpClient); + reset(closeableHttpClient); List result = provider.getTrustedCerts("htpp>\\\\//wrong.url"); assertNotNull(result); assertTrue(result.isEmpty()); } @Test - void whenIOError_thenNoCertificatesReturned() throws IOException { - when(closeableHttpClient.execute(any())).thenThrow(new IOException("communication error")); - provider = new TrustedCertificatesProvider(closeableHttpClient); + void whenIOError_thenNoCertificatesReturned() { + reset(closeableHttpClient); + HttpClientMockHelper.whenExecuteThenThrow(closeableHttpClient, new IOException("communication error")); List result = provider.getTrustedCerts(CERTS_URL); assertNotNull(result); assertTrue(result.isEmpty()); @@ -115,15 +120,11 @@ void whenIOError_thenNoCertificatesReturned() throws IOException { class GivenResponseWithInvalidCertificate { @Test - void whenGetTrustedCerts_thenNoCertificatesReturned() throws IOException { - provider = new TrustedCertificatesProvider(closeableHttpClient); - when(closeableHttpClient.execute(any())).thenReturn(httpResponse); - when(httpResponse.getEntity()).thenReturn(responseEntity); - when(responseEntity.getContent()).thenReturn(new ByteArrayInputStream("invalid_certificate".getBytes())); + void whenGetTrustedCerts_thenNoCertificatesReturned() { + HttpClientMockHelper.mockResponse(httpResponse, HttpStatus.SC_OK, "invalid_response_causing_certificate_parsing_error"); List result = provider.getTrustedCerts(CERTS_URL); assertNotNull(result); assertTrue(result.isEmpty()); - // check for log message } } @@ -131,24 +132,16 @@ void whenGetTrustedCerts_thenNoCertificatesReturned() throws IOException { class GivenEmptyResponse { @Test - void whenGetTrustedCerts_thenNoCertificatesReturned() throws UnsupportedOperationException, IOException { - when(closeableHttpClient.execute(any())).thenReturn(httpResponse); - when(httpResponse.getEntity()).thenReturn(responseEntity); - when(responseEntity.getContent()).thenReturn(new ByteArrayInputStream(new byte[0])); - - provider = new TrustedCertificatesProvider(closeableHttpClient); + void whenGetTrustedCerts_thenNoCertificatesReturned() { + HttpClientMockHelper.mockResponse(httpResponse, HttpStatus.SC_OK, ""); List result = provider.getTrustedCerts(CERTS_URL); assertNotNull(result); assertTrue(result.isEmpty()); } @Test - void whenNoHttpEntity_thenNoCertificatesReturned() throws IOException { - when(closeableHttpClient.execute(any())).thenReturn(httpResponse); - when(httpResponse.getEntity()).thenReturn(responseEntity); - when(httpResponse.getEntity()).thenReturn(null); - - provider = new TrustedCertificatesProvider(closeableHttpClient); + void whenNoHttpEntity_thenNoCertificatesReturned() { + HttpClientMockHelper.mockResponse(httpResponse, HttpStatus.SC_OK, null); List result = provider.getTrustedCerts(CERTS_URL); assertNotNull(result); assertTrue(result.isEmpty()); @@ -158,29 +151,20 @@ void whenNoHttpEntity_thenNoCertificatesReturned() throws IOException { @Nested class GivenErrorResponseCode { - @BeforeEach - void setup() throws IOException { - when(httpResponse.getCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); - when(closeableHttpClient.execute(any())).thenReturn(httpResponse); - when(httpResponse.getEntity()).thenReturn(responseEntity); - } - @Test - void whenGetTrustedCerts_thenNoCertificatesReturned() throws IOException { - provider = new TrustedCertificatesProvider(closeableHttpClient); + void whenGetTrustedCerts_thenNoCertificatesReturned() { + HttpClientMockHelper.mockResponse(httpResponse, HttpStatus.SC_BAD_REQUEST); List result = provider.getTrustedCerts(CERTS_URL); assertNotNull(result); assertTrue(result.isEmpty()); - //check for log message } @Test void whenNoStatusLine_thenNoCertificatesReturned() { - provider = new TrustedCertificatesProvider(closeableHttpClient); + HttpClientMockHelper.mockResponse(httpResponse, HttpStatus.SC_BAD_REQUEST); List result = provider.getTrustedCerts(CERTS_URL); assertNotNull(result); assertTrue(result.isEmpty()); - //check for log message } } } diff --git a/caching-service/build.gradle b/caching-service/build.gradle index e8c38f3f47..c6464b20db 100644 --- a/caching-service/build.gradle +++ b/caching-service/build.gradle @@ -48,6 +48,11 @@ gitProperties { gitPropertiesDir = new File("${project.rootDir}/${name}/build/resources/main/META-INF") } +configurations.all { + // Remove Spring Cloud enabler + exclude group: "org.springframework.cloud", module: "spring-cloud-starter-netflix-eureka-client" +} + dependencies { api project(':apiml-tomcat-common') api project(':onboarding-enabler-spring') @@ -64,11 +69,13 @@ dependencies { implementation libs.jakarta.servlet.api implementation libs.lettuce - testImplementation(testFixtures(project(":integration-tests"))) + testImplementation(testFixtures(project(":apiml-common"))) testImplementation libs.spring.boot.starter.test + testImplementation libs.spring.boot.starter testImplementation libs.rest.assured + compileOnly libs.lombok annotationProcessor libs.lombok } diff --git a/discovery-service/build.gradle b/discovery-service/build.gradle index c5197bea5d..1173f1073d 100644 --- a/discovery-service/build.gradle +++ b/discovery-service/build.gradle @@ -62,7 +62,7 @@ dependencies { implementation libs.apache.commons.lang3 implementation libs.jackson.dataformat.yaml - testImplementation(testFixtures(project(":integration-tests"))) + testImplementation(testFixtures(project(":apiml-common"))) testImplementation libs.spring.boot.starter.test testImplementation libs.rest.assured diff --git a/gradle/versions.gradle b/gradle/versions.gradle index fea3e7d3f7..388496eabb 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -127,6 +127,7 @@ dependencyResolutionManagement { library('spring_tx', 'org.springframework', 'spring-tx').versionRef('springFramework') library('spring_webmvc', 'org.springframework', 'spring-webmvc').versionRef('springFramework') library('spring_context_support', 'org.springframework', 'spring-context-support').versionRef('springFramework') + library('spring_beans', 'org.springframework', 'spring-beans').versionRef('springFramework') library('apache_commons_lang3', 'org.apache.commons', 'commons-lang3').versionRef('commonsLang3') library('apache_commons_text', 'org.apache.commons', 'commons-text').versionRef('commonsText') diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index cd2765dea4..c0089cd121 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -11,7 +11,6 @@ buildscript { plugins { alias(libs.plugins.test.logger) - id "java-test-fixtures" } repositories { @@ -42,18 +41,13 @@ dependencies { testImplementation libs.nimbus.jose.jwt testImplementation libs.reactor.test testImplementation libs.rest.assured + testImplementation(testFixtures(project(":apiml-common"))) runtimeOnly libs.jjwt.impl runtimeOnly libs.jjwt.jackson - testFixturesImplementation libs.spring.boot.starter - testFixturesImplementation libs.rest.assured - testCompileOnly libs.lombok testAnnotationProcessor libs.lombok - - testFixturesImplementation libs.lombok - testFixturesAnnotationProcessor libs.lombok } jar { diff --git a/security-service-client-spring/build.gradle b/security-service-client-spring/build.gradle index 4fc68c4fc0..dba84ed78a 100644 --- a/security-service-client-spring/build.gradle +++ b/security-service-client-spring/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation libs.apache.commons.lang3 testImplementation libs.spring.boot.starter.test + testImplementation(testFixtures(project(":apiml-common"))) compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java index a99f07ee33..6c7155d418 100644 --- a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java @@ -11,7 +11,7 @@ package org.zowe.apiml.security.client.handler; import lombok.extern.slf4j.Slf4j; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.HttpResponse; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; @@ -34,7 +34,7 @@ @Component public class RestResponseHandler { - public void handleErrorType(CloseableHttpResponse response, ErrorType errorType, Object... logParameters) { + public void handleErrorType(HttpResponse response, ErrorType errorType, Object... logParameters) { switch (response.getCode()) { case 401: if (errorType != null) { diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java index e220eef620..94387046aa 100644 --- a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java @@ -16,14 +16,12 @@ import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; @@ -72,21 +70,21 @@ public Optional login(String username, char[] password, char[] newPasswo HttpPost post = new HttpPost(uri); String json = objectMapper.writeValueAsString(loginRequest); post.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON)); - CloseableHttpResponse response = closeableHttpClient.execute(post); - final int statusCode = response.getCode(); - if (statusCode < HttpStatus.SC_OK || statusCode >= HttpStatus.SC_MULTIPLE_CHOICES) { - final HttpEntity responseEntity = response.getEntity(); - String responseBody = null; - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); + return closeableHttpClient.execute(post, response -> { + if (!HttpStatus.valueOf(response.getCode()).is2xxSuccessful()) { + final HttpEntity responseEntity = response.getEntity(); + String responseBody = null; + if (responseEntity != null) { + responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); + } + ErrorType errorType = getErrorType(responseBody); + responseHandler.handleErrorType(response, errorType, + "Cannot access Gateway service. Uri '{}' returned: {}", uri); + return Optional.empty(); } - ErrorType errorType = getErrorType(responseBody); - responseHandler.handleErrorType(response, errorType, - "Cannot access Gateway service. Uri '{}' returned: {}", uri); - return Optional.empty(); - } - return extractToken(response.getFirstHeader(HttpHeaders.SET_COOKIE).getValue()); - } catch (IOException | ParseException e) { //TODO: Consider different error + return extractToken(response.getFirstHeader(HttpHeaders.SET_COOKIE).getValue()); + }); + } catch (IOException e) { responseHandler.handleException(e); } finally { // TODO: remove once fixed directly in Spring - org.springframework.security.core.CredentialsContainer#eraseCredentials @@ -107,25 +105,25 @@ public QueryResponse query(String token) { gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayQueryEndpoint()); String cookie = String.format("%s=%s", authConfigurationProperties.getCookieProperties().getCookieName(), token); - try { HttpGet get = new HttpGet(uri); get.addHeader(HttpHeaders.COOKIE, cookie); - CloseableHttpResponse response = closeableHttpClient.execute(get); - final HttpEntity responseEntity = response.getEntity(); - String responseBody = null; - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); - } - final int statusCode = response.getCode(); - if (statusCode < HttpStatus.SC_OK || statusCode >= HttpStatus.SC_MULTIPLE_CHOICES) { - ErrorType errorType = getErrorType(responseBody); - responseHandler.handleErrorType(response, errorType, - "Cannot access Gateway service. Uri '{}' returned: {}", uri); - return null; - } - return objectMapper.readValue(responseBody, QueryResponse.class); - } catch (IOException | ParseException e) { + + return closeableHttpClient.execute(get, response -> { + final HttpEntity responseEntity = response.getEntity(); + String responseBody = null; + if (responseEntity != null) { + responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); + } + if (!HttpStatus.valueOf(response.getCode()).is2xxSuccessful()) { + ErrorType errorType = getErrorType(responseBody); + responseHandler.handleErrorType(response, errorType, + "Cannot access Gateway service. Uri '{}' returned: {}", uri); + return null; + } + return objectMapper.readValue(responseBody, QueryResponse.class); + }); + } catch (IOException e) { responseHandler.handleException(e); } return null; diff --git a/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/service/GatewaySecurityServiceTest.java b/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/service/GatewaySecurityServiceTest.java index fdc8181136..458ab60679 100644 --- a/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/service/GatewaySecurityServiceTest.java +++ b/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/service/GatewaySecurityServiceTest.java @@ -14,7 +14,8 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -22,8 +23,6 @@ import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.security.authentication.BadCredentialsException; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; @@ -32,8 +31,8 @@ import org.zowe.apiml.security.common.error.ErrorType; import org.zowe.apiml.security.common.token.QueryResponse; import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.util.HttpClientMockHelper; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Date; import java.util.Optional; @@ -41,7 +40,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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.Mockito.verify; import static org.mockito.Mockito.when; @@ -60,6 +58,11 @@ class GatewaySecurityServiceTest { @Mock private CloseableHttpClient closeableHttpClient; + @Mock + private Header header; + @Mock + CloseableHttpResponse response; + private ServiceAddress gatewayConfigProperties; private AuthConfigurationProperties authConfigurationProperties; private GatewaySecurityService securityService; @@ -85,25 +88,20 @@ void setup() { cookie = String.format("%s=%s", authConfigurationProperties.getCookieProperties().getCookieName(), TOKEN); + + HttpClientMockHelper.mockExecuteWithResponse(closeableHttpClient, response); } @Nested class GivenNoContent { - @Mock - private Header header; - @Mock - CloseableHttpResponse response; - @Nested class WhenDoLogin { @BeforeEach - void setUp() throws IOException { - when(response.getCode()).thenReturn(HttpStatus.NO_CONTENT.value()); - when(closeableHttpClient.execute(any())) - .thenReturn(response); + void setUp() { + when(response.getCode()).thenReturn(HttpStatus.SC_NO_CONTENT); when(response.getFirstHeader(HttpHeaders.SET_COOKIE)).thenReturn(header); when(header.getValue()).thenReturn(cookie); } @@ -128,16 +126,6 @@ void givenValidUpdatePasswordRequest_thenGetToken() { @Nested class WhenDoQuery { - @Mock - private HttpEntity entity; - - @BeforeEach - void setUp() throws IOException { - when(response.getCode()).thenReturn(HttpStatus.NO_CONTENT.value()); - when(closeableHttpClient.execute(any())) - .thenReturn(response); - } - @Test void givenValidAuth_thenSuccessfulResponse() throws IOException { Date issued = new Date(); @@ -145,8 +133,7 @@ void givenValidAuth_thenSuccessfulResponse() throws IOException { QueryResponse expectedQueryResponse = new QueryResponse("domain", "user", issued, exp, null, null, null); String responseBody = objectMapper.writeValueAsString(expectedQueryResponse); - when(entity.getContent()).thenReturn(new ByteArrayInputStream(responseBody.getBytes())); - when(response.getEntity()).thenReturn(entity); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_NO_CONTENT, responseBody); QueryResponse query = securityService.query("token"); assertEquals(expectedQueryResponse, query); } @@ -154,9 +141,7 @@ void givenValidAuth_thenSuccessfulResponse() throws IOException { @Test void givenGatewayUnauthorized_thenThrowException() throws IOException { String responseBody = MESSAGE_KEY_STRING + "org.zowe.apiml.security.query.invalidToken\""; - when(response.getCode()).thenReturn(HttpStatus.UNAUTHORIZED.value()); - when(entity.getContent()).thenReturn(new ByteArrayInputStream(responseBody.getBytes())); - when(response.getEntity()).thenReturn(entity); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_UNAUTHORIZED, responseBody); Exception exception = assertThrows(TokenNotValidException.class, () -> securityService.query("token")); assertEquals("Token is not valid.", exception.getMessage()); } @@ -169,21 +154,13 @@ class WhenHandleBadResponse { private static final String LOG_PARAMETER_STRING = "Cannot access Gateway service. Uri '{}' returned: {}"; - @Mock - private CloseableHttpResponse response; - @Mock - private Header header; - @Mock - private HttpEntity entity; - private String uri; @BeforeEach void setup() { uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayLoginEndpoint()); - when(response.getCode()).thenReturn(HttpStatus.UNAUTHORIZED.value()); - when(response.getEntity()).thenReturn(entity); + //when(response.getCode()).thenReturn(HttpStatus.UNAUTHORIZED.value()); } @Nested @@ -192,19 +169,14 @@ class ThenHandleAuthGeneralError { @Test void givenInvalidMessageKey() throws IOException { String errorMessage = MESSAGE_KEY_STRING + "badKey\""; - when(entity.getContent()).thenReturn(new ByteArrayInputStream(errorMessage.getBytes())); - when(closeableHttpClient.execute(any())) - .thenReturn(response); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_UNAUTHORIZED, errorMessage); assertThrows(BadCredentialsException.class, () -> securityService.login(USERNAME, PASSWORD, null)); verify(responseHandler).handleErrorType(response, ErrorType.AUTH_GENERAL, LOG_PARAMETER_STRING, uri); } @Test void givenGatewayUnauthorized_thenThrowException() throws IOException { - - when(entity.getContent()).thenReturn(new ByteArrayInputStream("message".getBytes())); - when(closeableHttpClient.execute(any())) - .thenReturn(response); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_UNAUTHORIZED, "message"); Exception exception = assertThrows(BadCredentialsException.class, () -> securityService.login(USERNAME, PASSWORD, null)); assertEquals("Invalid Credentials", exception.getMessage()); } @@ -212,9 +184,7 @@ void givenGatewayUnauthorized_thenThrowException() throws IOException { @Test void givenValidMessageKey_thenHandleErrorTypeForThatMessageKey() throws IOException { String errorMessage = MESSAGE_KEY_STRING + "org.zowe.apiml.security.login.invalidCredentials\""; - when(entity.getContent()).thenReturn(new ByteArrayInputStream(errorMessage.getBytes())); - when(closeableHttpClient.execute(any())) - .thenReturn(response); + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_UNAUTHORIZED, errorMessage); assertThrows(BadCredentialsException.class, () -> securityService.login(USERNAME, PASSWORD, null)); verify(responseHandler).handleErrorType(response, ErrorType.BAD_CREDENTIALS, LOG_PARAMETER_STRING, uri); } diff --git a/zaas-client/build.gradle b/zaas-client/build.gradle index 5c92e80e0f..3ef5e061b6 100644 --- a/zaas-client/build.gradle +++ b/zaas-client/build.gradle @@ -1,5 +1,6 @@ dependencies { - implementation libs.spring.boot.starter + compileOnly libs.spring.beans + compileOnly libs.spring.context implementation libs.jackson.databind implementation libs.http.client5 @@ -9,6 +10,7 @@ dependencies { testImplementation libs.spring.boot.starter.test testImplementation libs.jakarta.servlet.api testImplementation libs.jjwt + testImplementation(testFixtures(project(":apiml-common"))) testRuntimeOnly libs.jjwt.impl testRuntimeOnly libs.jjwt.jackson diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/exception/ZaasClientErrorCodes.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/exception/ZaasClientErrorCodes.java index 3797e817f3..9c0d537445 100644 --- a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/exception/ZaasClientErrorCodes.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/exception/ZaasClientErrorCodes.java @@ -20,13 +20,13 @@ public enum ZaasClientErrorCodes { EMPTY_NULL_USERNAME_PASSWORD("ZWEAS121E", "Empty or null username or password values provided", 400), EMPTY_NULL_AUTHORIZATION_HEADER("ZWEAS122E", "Empty or null authorization header provided", 400), INVALID_JWT_TOKEN("ZWEAS130E", "Invalid token provided", 400), - GENERIC_EXCEPTION("ZWEAS170E", "An exception occurred while trying to get the token", 400), + GENERIC_EXCEPTION("ZWEAS170E", "An exception occurred while trying to get the token", 500), BAD_REQUEST("ZWEAS400E", "Unable to generate PassTicket. Verify that the secured signon (PassTicket) function " + "and application ID is configured properly by referring to Using PassTickets in the guide for your security provider", 400), TOKEN_NOT_PROVIDED("ZWEAS401E", "Token is not provided", 401), - SERVICE_UNAVAILABLE("ZWEAS404E", "Gateway service is unavailable", 404), + SERVICE_UNAVAILABLE("ZWEAS404E", "Gateway service is unavailable", 503), EXPIRED_PASSWORD("ZWEAT412E", "The specified password is expired", 401), - APPLICATION_NAME_NOT_FOUND("ZWEAS417E", "The application name wasn't found", 404); + APPLICATION_NAME_NOT_FOUND("ZWEAS417E", "The application name wasn't found", 400); private final String id; private final String message; diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/ZaasClient.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/ZaasClient.java index e221775cb2..e4d59aa536 100644 --- a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/ZaasClient.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/ZaasClient.java @@ -10,11 +10,10 @@ package org.zowe.apiml.zaasclient.service; +import jakarta.servlet.http.HttpServletRequest; import org.zowe.apiml.zaasclient.exception.ZaasClientException; import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; -import jakarta.servlet.http.HttpServletRequest; - /** * Get JWT tokens, PaasTickets and details about the Tokens. * Facade covering all operations related to the security API exposed via API Mediation Layer diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/PassTicketServiceImpl.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/PassTicketServiceImpl.java index cf5b022bf3..e56e426d49 100644 --- a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/PassTicketServiceImpl.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/PassTicketServiceImpl.java @@ -14,42 +14,37 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.zowe.apiml.zaasclient.config.ConfigProperties; import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes; import org.zowe.apiml.zaasclient.exception.ZaasClientException; -import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; import org.zowe.apiml.zaasclient.passticket.ZaasClientTicketRequest; import org.zowe.apiml.zaasclient.passticket.ZaasPassTicketResponse; +import org.zowe.apiml.zaasclient.util.SimpleHttpResponse; import java.io.IOException; class PassTicketServiceImpl implements PassTicketService { - private final CloseableClientProvider httpClientProvider; + private final CloseableHttpClient httpClient; private final String ticketUrl; ConfigProperties passConfigProperties; - public PassTicketServiceImpl(CloseableClientProvider client, String baseUrl, ConfigProperties configProperties) { - httpClientProvider = client; + public PassTicketServiceImpl(CloseableHttpClient client, String baseUrl, ConfigProperties configProperties) { + httpClient = client; ticketUrl = baseUrl + "/ticket"; passConfigProperties = configProperties; } @Override - public String passTicket(String jwtToken, String applicationId) throws ZaasClientException, ZaasConfigurationException { + public String passTicket(String jwtToken, String applicationId) throws ZaasClientException { try { - CloseableHttpClient closeableHttpsClient = httpClientProvider.getHttpClient(); HttpPost httpPost = getHttpPost(jwtToken, applicationId); - - var response = closeableHttpsClient.execute(httpPost); - return extractPassTicket(response); - } catch (ZaasConfigurationException e) { + var passTicketResponse = httpClient.execute(httpPost, SimpleHttpResponse::fromResponseWithBytesBodyOnSuccess); + return extractPassTicket(passTicketResponse); + } catch (ZaasClientException e) { throw e; } catch (Exception e) { throw new ZaasClientException(ZaasClientErrorCodes.SERVICE_UNAVAILABLE, e); @@ -68,15 +63,15 @@ private HttpPost getHttpPost(String jwtToken, String applicationId) throws JsonP return httpPost; } - private String extractPassTicket(ClassicHttpResponse response) throws IOException, ZaasClientException, ParseException { + private String extractPassTicket(SimpleHttpResponse response) throws IOException, ZaasClientException { int statusCode = response.getCode(); if (statusCode == 200) { ObjectMapper mapper = new ObjectMapper(); ZaasPassTicketResponse zaasPassTicketResponse = mapper - .readValue(response.getEntity().getContent(), ZaasPassTicketResponse.class); + .readValue(response.getByteBody(), ZaasPassTicketResponse.class); return zaasPassTicketResponse.getTicket(); } else { - String obtainedMessage = EntityUtils.toString(response.getEntity()); + String obtainedMessage = response.getStringBody(); if (statusCode == 401) { throw new ZaasClientException(ZaasClientErrorCodes.INVALID_AUTHENTICATION, obtainedMessage); } else if (statusCode == 400) { @@ -88,4 +83,5 @@ private String extractPassTicket(ClassicHttpResponse response) throws IOExceptio } } } + } diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/TokenService.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/TokenService.java index 1e6dd28354..f675e3951c 100644 --- a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/TokenService.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/TokenService.java @@ -10,12 +10,11 @@ package org.zowe.apiml.zaasclient.service.internal; +import jakarta.servlet.http.HttpServletRequest; import org.zowe.apiml.zaasclient.exception.ZaasClientException; import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; import org.zowe.apiml.zaasclient.service.ZaasToken; -import jakarta.servlet.http.HttpServletRequest; - /** * Operations related to the tokens. Mainly JWT token. */ diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImpl.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImpl.java index 9a057a68f2..97f1737c9f 100644 --- a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImpl.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImpl.java @@ -10,6 +10,8 @@ package org.zowe.apiml.zaasclient.service.internal; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.zowe.apiml.zaasclient.config.ConfigProperties; import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes; import org.zowe.apiml.zaasclient.exception.ZaasClientException; @@ -18,44 +20,34 @@ import org.zowe.apiml.zaasclient.service.ZaasClient; import org.zowe.apiml.zaasclient.service.ZaasToken; -import jakarta.servlet.http.HttpServletRequest; +import java.io.Closeable; +import java.io.IOException; import java.util.Arrays; import java.util.Objects; -public class ZaasClientImpl implements ZaasClient { +public class ZaasClientImpl implements ZaasClient, Closeable { private final TokenService tokens; private final PassTicketService passTickets; + private final CloseableHttpClient httpClientWithoutCert; + private final CloseableHttpClient httpClient; public ZaasClientImpl(ConfigProperties configProperties) throws ZaasConfigurationException { if (!configProperties.isHttpOnly() && (configProperties.getKeyStorePath() == null)) { throw new ZaasConfigurationException(ZaasConfigurationErrorCodes.KEY_STORE_NOT_PROVIDED); } - CloseableClientProvider httpClientProvider = getTokenProvider(configProperties); - CloseableClientProvider httpClientProviderWithoutCert = getTokenProviderWithoutCert(configProperties, httpClientProvider); - - String baseUrl = String.format("%s://%s:%s%s", getScheme(configProperties.isHttpOnly()), configProperties.getApimlHost(), configProperties.getApimlPort(), - configProperties.getApimlBaseUrl()); - tokens = new ZaasJwtService(httpClientProviderWithoutCert, baseUrl, configProperties); - passTickets = new PassTicketServiceImpl(httpClientProvider, baseUrl, configProperties); - } - - private CloseableClientProvider getTokenProvider(ConfigProperties configProperties) throws ZaasConfigurationException { if (configProperties.isHttpOnly()) { - return new ZaasHttpClientProvider(); + httpClient = new ZaasHttpClientProvider().getHttpClient(); + httpClientWithoutCert = new ZaasHttpClientProvider().getHttpClient(); } else { - return new ZaasHttpsClientProvider(configProperties); + httpClient = new ZaasHttpsClientProvider(configProperties).getHttpClient(); + httpClientWithoutCert = new ZaasHttpsClientProvider(configProperties.withoutKeyStore()).getHttpClient(); } - } - private CloseableClientProvider getTokenProviderWithoutCert ( - ConfigProperties configProperties, - CloseableClientProvider defaultCloseableClientProvider) throws ZaasConfigurationException - { - if (configProperties.isHttpOnly()) { - return defaultCloseableClientProvider; - } - return getTokenProvider(configProperties.withoutKeyStore()); + String baseUrl = String.format("%s://%s:%s%s", getScheme(configProperties.isHttpOnly()), configProperties.getApimlHost(), configProperties.getApimlPort(), + configProperties.getApimlBaseUrl()); + tokens = new ZaasJwtService(httpClientWithoutCert, baseUrl, configProperties); + passTickets = new PassTicketServiceImpl(httpClient, baseUrl, configProperties); } private Object getScheme(boolean httpOnly) { @@ -69,6 +61,8 @@ private Object getScheme(boolean httpOnly) { ZaasClientImpl(TokenService tokens, PassTicketService passTickets) { this.tokens = tokens; this.passTickets = passTickets; + this.httpClientWithoutCert = null; + this.httpClient = null; } @Override @@ -135,7 +129,6 @@ public ZaasToken query(HttpServletRequest request) throws ZaasClientException { } @Override - @SuppressWarnings("squid:S2147") public String passTicket(String jwtToken, String applicationId) throws ZaasClientException, ZaasConfigurationException { if (Objects.isNull(applicationId) || applicationId.isEmpty()) { throw new ZaasClientException(ZaasClientErrorCodes.APPLICATION_NAME_NOT_FOUND); @@ -150,4 +143,10 @@ public String passTicket(String jwtToken, String applicationId) throws ZaasClien public void logout(String jwtToken) throws ZaasConfigurationException, ZaasClientException { tokens.logout(jwtToken); } + + @Override + public void close() throws IOException { + httpClient.close(); + httpClientWithoutCert.close(); + } } diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProvider.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProvider.java index aaa05866cf..b959bebaa5 100644 --- a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProvider.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProvider.java @@ -13,8 +13,6 @@ import lombok.AllArgsConstructor; import org.apache.hc.client5.http.UserTokenHandler; import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.cookie.BasicCookieStore; -import org.apache.hc.client5.http.cookie.CookieStore; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; @@ -54,8 +52,6 @@ class ZaasHttpsClientProvider implements CloseableClientProvider { private final String keyStoreType; private final String keyStorePath; - private final CookieStore cookieStore = new BasicCookieStore(); - private CloseableHttpClient httpsClient; public ZaasHttpsClientProvider(ConfigProperties configProperties) throws ZaasConfigurationException { @@ -86,10 +82,6 @@ static String formatKeyringUrl(String input) { return input; } - public void clearCookieStore() { - this.cookieStore.clear(); - } - @Override public synchronized CloseableHttpClient getHttpClient() throws ZaasConfigurationException { if (httpsClient == null) { diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtService.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtService.java index 2b162d19db..c8756d3c96 100644 --- a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtService.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtService.java @@ -18,25 +18,27 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.zowe.apiml.zaasclient.config.ConfigProperties; import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes; import org.zowe.apiml.zaasclient.exception.ZaasClientException; -import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; import org.zowe.apiml.zaasclient.service.ZaasToken; +import org.zowe.apiml.zaasclient.util.SimpleHttpResponse; import java.io.IOException; import java.util.Arrays; +import java.util.Map; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -49,15 +51,14 @@ class ZaasJwtService implements TokenService { private final String loginEndpoint; private final String queryEndpoint; private final String logoutEndpoint; - private final CloseableClientProvider httpClientProvider; + private final CloseableHttpClient httpClient; private final ObjectMapper objectMapper = new ObjectMapper(); ConfigProperties zassConfigProperties; - public ZaasJwtService(CloseableClientProvider client, String baseUrl, ConfigProperties configProperties) { - this.httpClientProvider = client; - + public ZaasJwtService(CloseableHttpClient client, String baseUrl, ConfigProperties configProperties) { + httpClient = client; loginEndpoint = baseUrl + "/login"; queryEndpoint = baseUrl + "/query"; logoutEndpoint = baseUrl + "/logout"; @@ -68,6 +69,7 @@ public ZaasJwtService(CloseableClientProvider client, String baseUrl, ConfigProp public String login(String userId, char[] password, char[] newPassword) throws ZaasClientException { return (String) doRequest( () -> loginWithCredentials(userId, password, newPassword), + this::processJwtTokenResponse, this::extractToken); } @@ -75,31 +77,44 @@ public String login(String userId, char[] password, char[] newPassword) throws Z public String login(String userId, char[] password) throws ZaasClientException { return (String) doRequest( () -> loginWithCredentials(userId, password, null), + this::processJwtTokenResponse, this::extractToken); } - private ClientWithResponse loginWithCredentials(String userId, char[] password, char[] newPassword) throws ZaasConfigurationException, IOException { - var client = httpClientProvider.getHttpClient(); + private ClassicHttpRequest loginWithCredentials(String userId, char[] password, char[] newPassword) throws IOException { + var httpPost = new HttpPost(loginEndpoint); String json = objectMapper.writeValueAsString(new Credentials(userId, password, newPassword)); var entity = new StringEntity(json); httpPost.setEntity(entity); httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/json"); - return new ClientWithResponse(client, client.execute(httpPost)); + return httpPost; + } + + private SimpleHttpResponse processJwtTokenResponse(ClassicHttpResponse response) throws ParseException, IOException { + var headers = Arrays.stream(response.getHeaders(HttpHeaders.SET_COOKIE)) + .findFirst() + .map(header -> Map.of(HttpHeaders.SET_COOKIE, header)) + .orElse(Map.of()); + if (response.getEntity() != null) { + return new SimpleHttpResponse(response.getCode(), EntityUtils.toString(response.getEntity()), headers); + } else { + return new SimpleHttpResponse(response.getCode(), headers); + } } @Override public String login(String authorizationHeader) throws ZaasClientException { return (String) doRequest( () -> loginWithHeader(authorizationHeader), + this::processJwtTokenResponse, this::extractToken); } - private ClientWithResponse loginWithHeader(String authorizationHeader) throws ZaasConfigurationException, IOException { - var client = httpClientProvider.getHttpClient(); + private ClassicHttpRequest loginWithHeader(String authorizationHeader) { HttpPost httpPost = new HttpPost(loginEndpoint); httpPost.setHeader(HttpHeaders.AUTHORIZATION, authorizationHeader); - return new ClientWithResponse(client, client.execute(httpPost)); + return httpPost; } @Override @@ -108,7 +123,10 @@ public ZaasToken query(String jwtToken) throws ZaasClientException { throw new ZaasClientException(ZaasClientErrorCodes.TOKEN_NOT_PROVIDED, "No token provided"); } - return (ZaasToken) doRequest(() -> queryWithJwtToken(jwtToken), this::extractZaasToken); + return (ZaasToken) doRequest( + () -> queryWithJwtToken(jwtToken), + SimpleHttpResponse::fromResponseWithBytesBodyOnSuccess, + this::extractZaasToken); } @Override @@ -119,7 +137,7 @@ public ZaasToken query(@NonNull HttpServletRequest request) throws ZaasClientExc @Override public void logout(String jwtToken) throws ZaasClientException { - doRequest(() -> logoutJwtToken(jwtToken)); + doLogoutRequest(() -> logoutJwtToken(jwtToken)); } /** @@ -161,59 +179,20 @@ private Optional extractJwtTokenFromAuthorizationHeader(String header) { return Optional.empty(); } - private ClientWithResponse queryWithJwtToken(String jwtToken) throws ZaasConfigurationException, IOException { - var client = httpClientProvider.getHttpClient(); + private ClassicHttpRequest queryWithJwtToken(String jwtToken) { var httpGet = new HttpGet(queryEndpoint); httpGet.addHeader(HttpHeaders.COOKIE, zassConfigProperties.getTokenPrefix() + "=" + jwtToken); - return new ClientWithResponse(client, client.execute(httpGet)); + return httpGet; } - private ClientWithResponse logoutJwtToken(String jwtToken) throws ZaasConfigurationException, IOException, ZaasClientException { - var client = httpClientProvider.getHttpClient(); - clearZaasClientCookies(); + private ClassicHttpRequest logoutJwtToken(String jwtToken) { HttpPost httpPost = new HttpPost(logoutEndpoint); if (jwtToken.startsWith(BEARER_AUTHENTICATION_PREFIX)) { httpPost.addHeader(HttpHeaders.AUTHORIZATION, jwtToken); } else { httpPost.addHeader(HttpHeaders.COOKIE, zassConfigProperties.getTokenPrefix() + "=" + jwtToken); } - return getClientWithResponse(client, httpPost); - } - - private void clearZaasClientCookies() { - if (httpClientProvider instanceof ZaasHttpsClientProvider) { - ((ZaasHttpsClientProvider) httpClientProvider).clearCookieStore(); - } - } - - private ClientWithResponse getClientWithResponse(CloseableHttpClient client, HttpPost httpPost) throws IOException, ZaasClientException { - ClientWithResponse clientWithResponse = new ClientWithResponse(client, client.execute(httpPost)); - int httpResponseCode = clientWithResponse.getResponse().getCode(); - if (httpResponseCode == 204) { - return clientWithResponse; - } else { - String obtainedMessage; - try { - obtainedMessage = EntityUtils.toString(clientWithResponse.getResponse().getEntity()); - } catch (ParseException e) { - throw new ZaasClientException(ZaasClientErrorCodes.GENERIC_EXCEPTION, e.getMessage()); - } - if (httpResponseCode == 401) { - throw new ZaasClientException(ZaasClientErrorCodes.EXPIRED_JWT_EXCEPTION, obtainedMessage); - } else { - throw new ZaasClientException(ZaasClientErrorCodes.INVALID_JWT_TOKEN, obtainedMessage); - } - } - } - - private void finallyClose(ClassicHttpResponse response) { - try { - if (response != null) { - response.close(); - } - } catch (IOException e) { - log.warn("It wasn't possible to close the resources. " + e.getMessage()); - } + return httpPost; } private void handleErrorMessage(JsonNode message, Predicate condition) throws ZaasClientException { @@ -240,10 +219,10 @@ private void handleErrorMessage(String errorMessage, Predicate v.startsWith(zassConfigProperties.getTokenPrefix())).map(v -> v.substring(v.indexOf("=") + 1)).findFirst(); if (apimlAuthCookie.isPresent()) { token = apimlAuthCookie.get(); @@ -280,49 +253,50 @@ private String extractToken(ClassicHttpResponse response) throws ZaasClientExcep return token; } - String obtainedMessage; - try { - obtainedMessage = EntityUtils.toString(response.getEntity()); - } catch (ParseException e) { - throw new ZaasClientException(ZaasClientErrorCodes.GENERIC_EXCEPTION, e.getMessage()); - } if (httpResponseCode == 401) { - handleErrorMessage(obtainedMessage, ZaasClientErrorCodes.EXPIRED_PASSWORD::equals); - throw new ZaasClientException(ZaasClientErrorCodes.INVALID_AUTHENTICATION, obtainedMessage); + handleErrorMessage(response.getStringBody(), ZaasClientErrorCodes.EXPIRED_PASSWORD::equals); + throw new ZaasClientException(ZaasClientErrorCodes.INVALID_AUTHENTICATION, response.getStringBody()); } if (httpResponseCode == 400) { - throw new ZaasClientException(ZaasClientErrorCodes.EMPTY_NULL_USERNAME_PASSWORD, obtainedMessage); + throw new ZaasClientException(ZaasClientErrorCodes.EMPTY_NULL_USERNAME_PASSWORD, response.getStringBody()); } - throw new ZaasClientException(ZaasClientErrorCodes.GENERIC_EXCEPTION, obtainedMessage); + throw new ZaasClientException(ZaasClientErrorCodes.GENERIC_EXCEPTION, response.getStringBody()); } - private void doRequest(Operation request) throws ZaasClientException { - ClientWithResponse clientWithResponse = new ClientWithResponse(); - try { - clientWithResponse = request.request(); - } catch (IOException | ZaasConfigurationException e) { - throw new ZaasClientException(ZaasClientErrorCodes.SERVICE_UNAVAILABLE, e); - } finally { - finallyClose(clientWithResponse.getResponse()); + private void doLogoutRequest(OperationGenerator requestGenerator) throws ZaasClientException { + var response = getSimpleResponse(requestGenerator, SimpleHttpResponse::fromResponseWithBytesBodyOnSuccess); + + if (response.getCode() == 401) { + throw new ZaasClientException(ZaasClientErrorCodes.EXPIRED_JWT_EXCEPTION, response.getStringBody()); + } else if (!response.isSuccess()) { + throw new ZaasClientException(ZaasClientErrorCodes.INVALID_JWT_TOKEN, response.getStringBody()); } } - private Object doRequest(Operation request, Token token) throws ZaasClientException { - ClientWithResponse clientWithResponse = new ClientWithResponse(); + private Object doRequest( + OperationGenerator requestGenerator, + HttpClientResponseHandler responseHandler, + TokenExtractor token) throws ZaasClientException { try { - - clientWithResponse = request.request(); - - return token.extract(clientWithResponse.getResponse()); + return token.extract(getSimpleResponse(requestGenerator, responseHandler)); } catch (ZaasClientException e) { throw e; } catch (IOException e) { throw new ZaasClientException(ZaasClientErrorCodes.SERVICE_UNAVAILABLE, e); } catch (Exception e) { throw new ZaasClientException(ZaasClientErrorCodes.GENERIC_EXCEPTION, e); - } finally { - finallyClose(clientWithResponse.getResponse()); + + } + } + + private SimpleHttpResponse getSimpleResponse(OperationGenerator operationGenerator, HttpClientResponseHandler responseHandler) + throws ZaasClientException { + + try { + return httpClient.execute(operationGenerator.request(), responseHandler); + } catch (IOException e) { + throw new ZaasClientException(ZaasClientErrorCodes.SERVICE_UNAVAILABLE, e); } } @@ -334,19 +308,11 @@ static class Credentials { char[] newPassword; } - @Data - @AllArgsConstructor - @NoArgsConstructor - static class ClientWithResponse { - CloseableHttpClient client; - ClassicHttpResponse response; - } - - interface Token { - Object extract(ClassicHttpResponse response) throws IOException, ZaasClientException; + interface TokenExtractor { + Object extract(SimpleHttpResponse response) throws IOException, ZaasClientException; } - interface Operation { - ClientWithResponse request() throws ZaasConfigurationException, IOException, ZaasClientException; + interface OperationGenerator { + ClassicHttpRequest request() throws IOException; } } diff --git a/zaas-client/src/main/java/org/zowe/apiml/zaasclient/util/SimpleHttpResponse.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/util/SimpleHttpResponse.java new file mode 100644 index 0000000000..37032502c2 --- /dev/null +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/util/SimpleHttpResponse.java @@ -0,0 +1,93 @@ +/* + * 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.zaasclient.util; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +import java.io.IOException; +import java.util.Map; + +/** + * Simple data class to hold data extracted from {@link org.apache.hc.core5.http.ClassicHttpResponse} so the underlying + * resources can be closed and the data processed further. + * Intended to be used with {@link org.apache.http.impl.client.CloseableHttpClient} execute methods as return + * value of {@link org.apache.http.client.ResponseHandler} in case of custom exceptions needs to be thrown during response processing. + * + * @param code http status code + * @param byteBody response body as byte[] + * @param stringBody response body as String + * @param headers http response headers + */ + +@Data +@RequiredArgsConstructor +public class SimpleHttpResponse { + + private final int code; + private final byte[] byteBody; + private final String stringBody; + private final Map headers; + + public SimpleHttpResponse(int code, byte[] byteBody) { + this(code, byteBody, null, null); + } + + public SimpleHttpResponse(int code, String stringBody) { + this(code, null, stringBody, null); + } + + public SimpleHttpResponse(int code, String stringBody, Map headers) { + this(code, null, stringBody, headers); + } + + public SimpleHttpResponse(int code) { + this(code, null, null, null); + } + + public SimpleHttpResponse(int code, Map headers) { + this(code, null, null, headers); + } + + /** + * Extract data from http response so the underlying resources of {@link org.apache.hc.core5.http.ClassicHttpResponse} can be closed. + * On success http status (2XX), data are fetched as byte[] into byteBody, otherwise as String into stringBody. + * + * @param response http response to fetch data from + * @return {@link SimpleHttpResponse} + * @throws IOException in case of data cannot be fetched from the response stream + * @throws ParseException in case of data cannot be converted into String + */ + public static SimpleHttpResponse fromResponseWithBytesBodyOnSuccess(ClassicHttpResponse response) throws IOException, ParseException { + if (response.getEntity() != null) { + if (isSuccessInternal(response.getCode())) { + return new SimpleHttpResponse(response.getCode(), response.getEntity().getContent().readAllBytes()); + } else { + return new SimpleHttpResponse(response.getCode(), EntityUtils.toString(response.getEntity())); + } + } else { + return new SimpleHttpResponse(response.getCode()); + } + } + + public boolean isSuccess() { + return isSuccessInternal(code); + } + + private static boolean isSuccessInternal(int code) { + return code >= HttpStatus.SC_OK && code < HttpStatus.SC_MULTIPLE_CHOICES; + } +} diff --git a/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImplHttpsTests.java b/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImplHttpsTests.java index 807932d8ff..ec622ef350 100644 --- a/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImplHttpsTests.java +++ b/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasClientImplHttpsTests.java @@ -14,15 +14,14 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.utils.Base64; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HeaderElement; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.message.StatusLine; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,26 +30,17 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.util.HttpClientMockHelper; import org.zowe.apiml.zaasclient.config.ConfigProperties; import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes; import org.zowe.apiml.zaasclient.exception.ZaasClientException; -import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; import org.zowe.apiml.zaasclient.passticket.ZaasPassTicketResponse; import org.zowe.apiml.zaasclient.service.ZaasToken; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; -import java.security.Key; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; +import java.security.*; import java.security.cert.CertificateException; import java.util.Date; import java.util.Properties; @@ -59,16 +49,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ZaasClientImplHttpsTests { @@ -78,17 +61,9 @@ class ZaasClientImplHttpsTests { private static final String CONFIG_FILE_PATH = "src/test/resources/configFile.properties"; - @Mock - private ZaasHttpsClientProvider zaasHttpsClientProvider; - @Mock - private StatusLine statusLine; - @Mock - private HeaderElement headerElement; @Mock private Header header; @Mock - private CloseableHttpResponse closeableHttpResponse; - @Mock private CloseableHttpClient closeableHttpClient; @Mock private HttpEntity httpsEntity; @@ -116,11 +91,9 @@ void setupMethod() throws Exception { expiredToken = getToken(now, expirationForExpiredToken, jwtSecretKey); invalidToken = token + "DUMMY TEXT"; - when(zaasHttpsClientProvider.getHttpClient()).thenReturn(closeableHttpClient); - String baseUrl = "/gateway/api/v1/auth"; - tokenService = new ZaasJwtService(zaasHttpsClientProvider, baseUrl, configProperties); - passTicketService = new PassTicketServiceImpl(zaasHttpsClientProvider, baseUrl, configProperties); + tokenService = new ZaasJwtService(closeableHttpClient, baseUrl, configProperties); + passTicketService = new PassTicketServiceImpl(closeableHttpClient, baseUrl, configProperties); } private String getToken(long now, long expiration, Key jwtSecretKey) { @@ -181,39 +154,28 @@ private static String getAuthHeader(String userName, char[] password) { } private CloseableHttpResponse prepareResponse(int httpResponseCode, boolean withResponseHeaders) { - try { - CloseableHttpResponse response = mock(CloseableHttpResponse.class); - doReturn(httpResponseCode).when(response).getCode(); - when(zaasHttpsClientProvider.getHttpClient()).thenReturn(closeableHttpClient); - doReturn(response).when(closeableHttpClient).execute(any(HttpUriRequestBase.class)); - if (withResponseHeaders) { - Header[] headers = new Header[]{ header }; - when(response.getHeaders("Set-Cookie")).thenReturn(headers); - when(header.getValue()).thenReturn("apimlAuthenticationToken=token"); - } - return response; - } catch (ZaasConfigurationException | IOException e) { - e.printStackTrace(); - throw new RuntimeException(e); + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + doReturn(httpResponseCode).when(response).getCode(); + HttpClientMockHelper.mockExecuteWithResponse(closeableHttpClient, response); + if (withResponseHeaders) { + Header[] headers = new Header[]{header}; + when(response.getHeaders(HttpHeaders.SET_COOKIE)).thenReturn(headers); + when(header.getValue()).thenReturn("apimlAuthenticationToken=token"); } + return response; } private void prepareResponseForServerUnavailable() { - try { - when(zaasHttpsClientProvider.getHttpClient()).thenReturn(closeableHttpClient); - when(closeableHttpClient.execute(any(HttpPost.class))).thenThrow(IOException.class); - } catch (IOException | ZaasConfigurationException e) { - e.printStackTrace(); - } + HttpClientMockHelper.whenExecuteThenThrow(closeableHttpClient, new IOException("An IO Exception")); } private void prepareResponseForUnexpectedException() { try { - when(zaasHttpsClientProvider.getHttpClient()).thenReturn(closeableHttpClient); - when(closeableHttpClient.execute(any(HttpPost.class))).thenAnswer(invocation -> { - throw new Exception(); - }); - } catch (IOException | ZaasConfigurationException e) { + when(closeableHttpClient.execute(any(HttpPost.class), any(HttpClientResponseHandler.class))) + .thenAnswer(invocation -> { + throw new Exception(); + }); + } catch (IOException e) { e.printStackTrace(); } } @@ -250,6 +212,7 @@ void giveInvalidCredentials_whenLoginIsRequested_thenProperExceptionIsRaised(int ZaasClientErrorCodes expectedCode) { var response = prepareResponse(statusCode, false); when(response.getEntity()).thenReturn(httpsEntity); + when(response.getHeaders(HttpHeaders.SET_COOKIE)).thenReturn(new Header[0]); ZaasClientException exception = assertThrows(ZaasClientException.class, () -> tokenService.login(username, password)); @@ -295,6 +258,7 @@ private static Stream provideInvalidAuthHeaders() { void doLoginWithAuthHeaderInvalidUsername(int statusCode, String authHeader, ZaasClientErrorCodes expectedCode) { var response = prepareResponse(statusCode, false); when(response.getEntity()).thenReturn(httpsEntity); + when(response.getHeaders(HttpHeaders.SET_COOKIE)).thenReturn(new Header[0]); ZaasClientException exception = assertThrows(ZaasClientException.class, () -> tokenService.login(authHeader)); @@ -349,10 +313,10 @@ void testQueryWithToken_WhenResponseCodeIs404_ZaasClientException() { } @Test - void testLoginWithToken_WhenResponseCodeIs400_ZaasClientException() throws IOException { + void testLoginWithToken_WhenResponseCodeIs400_ZaasClientException() { var response = prepareResponse(400, false); when(response.getEntity()).thenReturn(httpsEntity); - + when(response.getHeaders(HttpHeaders.SET_COOKIE)).thenReturn(new Header[0]); ZaasClientException exception = assertThrows(ZaasClientException.class, () -> tokenService.login(token)); assertTrue(exception.getMessage().contains("'ZWEAS121E', message='Empty or null username or password values provided'"), "Message was: " + exception.getMessage()); @@ -365,7 +329,6 @@ void testPassTicketWithToken_ValidToken_ValidPassTicket() throws Exception { ZaasPassTicketResponse zaasPassTicketResponse = new ZaasPassTicketResponse(); zaasPassTicketResponse.setTicket("ticket"); - when(zaasHttpsClientProvider.getHttpClient()).thenReturn(closeableHttpClient); when(httpsEntity.getContent()).thenReturn(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(zaasPassTicketResponse))); assertEquals("ticket", passTicketService.passTicket(token, "ZOWEAPPL")); @@ -388,7 +351,7 @@ void givenInvalidToken_whenLogout_thenThrowException() { } @Test - void givenValidTokenInBearer_whenLogout_thenSuccess() throws ZaasClientException, IOException { + void givenValidTokenInBearer_whenLogout_thenSuccess() throws ZaasClientException { prepareResponse(HttpStatus.SC_NO_CONTENT, true); String token = tokenService.login(getAuthHeader(VALID_USER, VALID_PASSWORD)); token = "Bearer " + token; diff --git a/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProviderTests.java b/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProviderTests.java index eb4d926a7f..b41ed8979f 100644 --- a/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProviderTests.java +++ b/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasHttpsClientProviderTests.java @@ -27,9 +27,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; class ZaasHttpsClientProviderTests { diff --git a/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtServiceTest.java b/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtServiceTest.java index 3e71ff091b..aa974e2cd4 100644 --- a/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtServiceTest.java +++ b/zaas-client/src/test/java/org/zowe/apiml/zaasclient/service/internal/ZaasJwtServiceTest.java @@ -16,8 +16,10 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.entity.StringEntity; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,22 +32,17 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.mock.web.MockHttpServletRequest; +import org.zowe.apiml.util.HttpClientMockHelper; import org.zowe.apiml.zaasclient.config.ConfigProperties; import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes; import org.zowe.apiml.zaasclient.exception.ZaasClientException; -import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; import org.zowe.apiml.zaasclient.service.ZaasToken; import java.io.IOException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -63,37 +60,34 @@ class ZaasJwtServiceTest { ArgumentCaptor requestCaptor; private static final String EXPIRED_PASSWORD_RESPONSE = - "{\n" + - " \"messages\": [\n" + - " {\n" + - " \"messageType\": \"ERROR\",\n" + - " \"messageNumber\": \"ZWEAT412E\",\n" + - " \"messageContent\": \"The password for the specified identity has expired\",\n" + - " \"messageKey\": \"org.zowe.apiml.security.platform.errno.EMVSEXPIRE\"\n" + - " }\n" + - " ]\n" + - "}"; + //language=JSON + """ + { + "messages": [ + { + "messageType": "ERROR", + "messageNumber": "ZWEAT412E", + "messageContent": "The password for the specified identity has expired", + "messageKey": "org.zowe.apiml.security.platform.errno.EMVSEXPIRE" + } + ] + }"""; @Mock private CloseableHttpClient closeableHttpClient; - @Mock - private CloseableClientProvider closeableClientProvider; - private ZaasJwtService zaasJwtService; @BeforeEach - void setUp() throws ZaasConfigurationException { - doReturn(closeableHttpClient).when(closeableClientProvider).getHttpClient(); - - zaasJwtService = new ZaasJwtService(closeableClientProvider, BASE_URL, configProperties); + void setUp() { + zaasJwtService = new ZaasJwtService(closeableHttpClient, BASE_URL, configProperties); } @Test void givenJwtToken_whenLogout_thenSetCookie() throws ZaasClientException, IOException { - mockHttpClient(204); + mockHttpClientResponse(204); zaasJwtService.logout(JWT_TOKEN); - verify(closeableHttpClient, times(1)).execute(requestCaptor.capture()); + verify(closeableHttpClient, times(1)).execute(requestCaptor.capture(), any(HttpClientResponseHandler.class)); var capturedRequest = requestCaptor.getValue(); assertTrue((capturedRequest.getHeaders(HttpHeaders.COOKIE) != null) && (capturedRequest.getHeaders(HttpHeaders.COOKIE).length == 1) && @@ -102,10 +96,10 @@ void givenJwtToken_whenLogout_thenSetCookie() throws ZaasClientException, IOExce @Test void givenAuthorizationHeaderWithJwtToken_whenLogout_thenAuthorizationHeader() throws ZaasClientException, IOException { - mockHttpClient(204); + mockHttpClientResponse(204); zaasJwtService.logout(HEADER_AUTHORIZATION); - verify(closeableHttpClient, times(1)).execute(requestCaptor.capture()); + verify(closeableHttpClient, times(1)).execute(requestCaptor.capture(), any(HttpClientResponseHandler.class)); var capturedRequest = requestCaptor.getValue(); assertTrue((capturedRequest.getHeaders(HttpHeaders.AUTHORIZATION) != null) && (capturedRequest.getHeaders(HttpHeaders.AUTHORIZATION).length == 1) && @@ -115,7 +109,7 @@ void givenAuthorizationHeaderWithJwtToken_whenLogout_thenAuthorizationHeader() t @Test void givenValidJwtToken_whenQueryToken_thenReturnToken() throws ZaasClientException, IOException { ZaasToken expectedToken = new ZaasToken(); - mockHttpClient(200, mapper.writeValueAsString(expectedToken)); + mockHttpClientResponse(200, mapper.writeValueAsString(expectedToken)); ZaasToken actualToken = zaasJwtService.query("token"); assertEquals(expectedToken, actualToken); @@ -132,8 +126,8 @@ void givenEmptyJwtToken_whenQueryToken_thenThrowException() { } @Test - void givenInvalidJwtToken_whenQueryToken_thenThrowException() throws IOException { - mockHttpClient(401); + void givenInvalidJwtToken_whenQueryToken_thenThrowException() { + mockHttpClientResponse(401); zaasClientTestAssertThrows(ZaasClientErrorCodes.INVALID_JWT_TOKEN, "Queried token is invalid or expired", () -> zaasJwtService.query("bad token")); } @@ -142,7 +136,7 @@ void givenExpiredToken_whenQueryToken_thenThrowException() throws IOException { ZaasToken expiredToken = new ZaasToken(); expiredToken.setExpired(true); - mockHttpClient(200, mapper.writeValueAsString(expiredToken)); + mockHttpClientResponse(200, mapper.writeValueAsString(expiredToken)); zaasClientTestAssertThrows(ZaasClientErrorCodes.EXPIRED_JWT_EXCEPTION, "Queried token is expired", () -> zaasJwtService.query("expired token")); } @@ -153,7 +147,7 @@ void givenJwtTokenInCookie_whenQueryRequest_thenReturnToken() throws ZaasClientE mockRequest.setCookies(cookies); ZaasToken expectedToken = new ZaasToken(); - mockHttpClient(200, mapper.writeValueAsString(expectedToken)); + mockHttpClientResponse(200, mapper.writeValueAsString(expectedToken)); ZaasToken actualToken = zaasJwtService.query(mockRequest); assertEquals(expectedToken, actualToken); @@ -165,7 +159,7 @@ void givenJwtTokenInHeader_whenQueryRequest_thenReturnToken() throws ZaasClientE mockRequest.addHeader(HttpHeaders.AUTHORIZATION, HEADER_AUTHORIZATION); ZaasToken expectedToken = new ZaasToken(); - mockHttpClient(200, mapper.writeValueAsString(expectedToken)); + mockHttpClientResponse(200, mapper.writeValueAsString(expectedToken)); ZaasToken actualToken = zaasJwtService.query(mockRequest); assertEquals(expectedToken, actualToken); @@ -195,29 +189,31 @@ void givenHeaderWithEmptyAuthorization_whenQueryRequest_thenThrowException() { } @Test - void givenExpiredPassword_whenLogin_thenThrowException() throws IOException { - mockHttpClient(401, EXPIRED_PASSWORD_RESPONSE); + void givenExpiredPassword_whenLogin_thenThrowException() { + var responseMock = mockHttpClientResponse(401, EXPIRED_PASSWORD_RESPONSE); + when(responseMock.getHeaders(HttpHeaders.SET_COOKIE)).thenReturn(new Header[0]); zaasClientTestAssertThrows(ZaasClientErrorCodes.EXPIRED_PASSWORD, "The specified password is expired", () -> zaasJwtService.login("user", "password".toCharArray())); } @Test - void givenExpiredPassword_whenQuery_thenThrowException() throws IOException { - mockHttpClient(401, EXPIRED_PASSWORD_RESPONSE); + void givenExpiredPassword_whenQuery_thenThrowException() { + mockHttpClientResponse(401, EXPIRED_PASSWORD_RESPONSE); zaasClientTestAssertThrows(ZaasClientErrorCodes.EXPIRED_PASSWORD, "The specified password is expired", () -> zaasJwtService.query("jwt")); } - private void mockHttpClient(int statusCode) throws IOException { - mockHttpClient(statusCode, "null"); + private void mockHttpClientResponse(int statusCode) { + mockHttpClientResponse(statusCode, "null"); } - private void mockHttpClient(int statusCode, String content) throws IOException { + private CloseableHttpResponse mockHttpClientResponse(int statusCode, String content) { CloseableHttpResponse response = mock(CloseableHttpResponse.class); doReturn(statusCode).when(response).getCode(); HttpEntity entity = new StringEntity(content, ContentType.TEXT_PLAIN); doReturn(entity).when(response).getEntity(); - doReturn(response).when(closeableHttpClient).execute(any(HttpUriRequestBase.class)); + HttpClientMockHelper.mockExecuteWithResponse(closeableHttpClient, response); + return response; } private void zaasClientTestAssertThrows(ZaasClientErrorCodes code, String message, Executable executable) { diff --git a/zaas-service/build.gradle b/zaas-service/build.gradle index e35a89ac1b..a6da20b46d 100644 --- a/zaas-service/build.gradle +++ b/zaas-service/build.gradle @@ -111,7 +111,7 @@ dependencies { testCompileOnly libs.lombok testAnnotationProcessor libs.lombok - testImplementation(testFixtures(project(":integration-tests"))) + testImplementation(testFixtures(project(":apiml-common"))) } bootJar { diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/ExternalMapper.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/ExternalMapper.java index 31de24f599..9cc5f36edf 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/ExternalMapper.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/ExternalMapper.java @@ -11,27 +11,26 @@ package org.zowe.apiml.zaas.security.mapping; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.message.BasicHeader; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.zowe.apiml.zaas.security.mapping.model.MapperResponse; -import org.zowe.apiml.zaas.security.service.TokenCreationService; 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.zaas.security.mapping.model.MapperResponse; +import org.zowe.apiml.zaas.security.service.TokenCreationService; -import jakarta.validation.constraints.NotNull; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -74,31 +73,28 @@ MapperResponse callExternalMapper(@NotNull HttpEntity payload) { httpPost.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); log.debug("Executing request against external identity mapper API: {}", httpPost); + var response = secureHttpClientWithoutKeystore.execute(httpPost, httpResponse -> { + final int statusCode = httpResponse.getCode(); + String responseBody = ""; + if (httpResponse.getEntity() != null) { + responseBody = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8); + } - - CloseableHttpResponse httpResponse = secureHttpClientWithoutKeystore.execute(httpPost); - - final int statusCode = httpResponse.getCode(); - String response = ""; - if (httpResponse.getEntity() != null) { - response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8); - } - if (statusCode == 0) { - return null; - } - if (!org.springframework.http.HttpStatus.valueOf(statusCode).is2xxSuccessful()) { - if (org.springframework.http.HttpStatus.valueOf(statusCode).is5xxServerError()) { - apimlLog.log("org.zowe.apiml.zaas.security.unexpectedMappingResponse", statusCode, response); + log.debug("External identity mapper API returned: {}", responseBody); + if (HttpStatus.valueOf(statusCode).is2xxSuccessful()) { + return responseBody; + } else if (HttpStatus.valueOf(statusCode).is5xxServerError()) { + apimlLog.log("org.zowe.apiml.zaas.security.unexpectedMappingResponse", statusCode, httpResponse); } else { - log.debug("Unexpected response from the external identity mapper. Status: {} body: {}", statusCode, response); + log.debug("Unexpected response from the external identity mapper. Status: {} body: {}", statusCode, httpResponse); } return null; - } - log.debug("External identity mapper API returned: {}", response); + }); + if (StringUtils.isNotEmpty(response)) { return objectMapper.readValue(response, MapperResponse.class); } - } catch (IOException | ParseException e) { + } catch (IOException e) { apimlLog.log("org.zowe.apiml.zaas.security.InvalidMappingResponse", e); } catch (URISyntaxException e) { apimlLog.log("org.zowe.apiml.zaas.security.InvalidMapperUrl", e); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/ExternalMapperTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/ExternalMapperTest.java index 27ce3fd834..95471bb868 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/ExternalMapperTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/ExternalMapperTest.java @@ -23,21 +23,16 @@ import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.util.HttpClientMockHelper; import org.zowe.apiml.zaas.security.mapping.model.MapperResponse; import org.zowe.apiml.zaas.security.service.TokenCreationService; import java.io.ByteArrayInputStream; import java.io.IOException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.ArgumentMatchers.any; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class ExternalMapperTest { @@ -57,11 +52,11 @@ public TestExternalMapper(String mapperUrl, String mapperUser, CloseableHttpClie @BeforeEach - void setup() throws IOException { + void setup() { closeableHttpClient = mock(CloseableHttpClient.class); httpResponse = mock(CloseableHttpResponse.class); when(httpResponse.getCode()).thenReturn(HttpStatus.SC_OK); - when(closeableHttpClient.execute(any())).thenReturn(httpResponse); + HttpClientMockHelper.mockExecuteWithResponse(closeableHttpClient, httpResponse); tokenCreationService = mock(TokenCreationService.class); when(tokenCreationService.createJwtTokenWithoutCredentials(anyString())).thenReturn("validJwtToken"); responseEntity = mock(HttpEntity.class); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java index 7bf00dfb7f..51843c2b94 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.util.HttpClientMockHelper; import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.zaas.security.service.schema.source.JwtAuthSource; import org.zowe.apiml.zaas.security.service.schema.source.OIDCAuthSource; @@ -97,15 +98,15 @@ void setup() { class GivenIdentityMappingExists { @BeforeEach - void setup() throws IOException { + void setup() { CloseableHttpResponse response = mock(CloseableHttpResponse.class); when(response.getCode()).thenReturn(HttpStatus.SC_OK); when(response.getEntity()).thenReturn(responseEntity); - when(httpClient.execute(any())).thenReturn(response); + HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); } @Test - void thenZosUserIsReturned() throws Exception { + void thenZosUserIsReturned() { String userId = oidcExternalMapper.mapToMainframeUserId(authSource); assertEquals(ZOSUSER, userId); } @@ -122,7 +123,7 @@ void setup() throws IOException { CloseableHttpResponse response = mock(CloseableHttpResponse.class); when(response.getCode()).thenReturn(HttpStatus.SC_OK); when(response.getEntity()).thenReturn(responseEntity); - when(httpClient.execute(any())).thenReturn(response); + HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); } @Test