diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 107b1011b5..41d57b3205 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1420,6 +1420,14 @@ jobs: - name: Run startup check run: > ./gradlew runStartUpCheck --info --scan -Denvironment.config=-ha -Ddiscoverableclient.instances=1 + - name: Show status when APIML is not ready yet + if: failure() + shell: bash + run: | + apt update + apt install -y apt-transport-https ca-certificates curl software-properties-common + curl -k -s https://gateway-service:10010/application/health + curl -k -s https://gateway-service-2:10010/application/health - name: Cypress run API Catalog run: | cd api-catalog-ui/frontend diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java index 553bc9a791..9f10b69e53 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java @@ -15,12 +15,16 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.config.HttpClientProperties; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.NettyRoutingFilter; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.gateway.route.Route; import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; +import java.net.ConnectException; import java.time.Duration; import java.util.List; import java.util.Optional; @@ -81,4 +85,14 @@ protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) { .responseTimeout(Duration.ofMillis(requestTimeout)); } + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + return super.filter(exchange, chain).onErrorResume(e -> { + if (e.getCause() instanceof ConnectException) { + var uri = exchange.getRequest().getURI(); + return Mono.error(new ServiceNotAccessibleException(String.format("Service is not available at %s://%s:%d", uri.getScheme(), uri.getHost(), uri.getPort()), e)); + } + return Mono.error(e); + }); + } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java index 55650151c9..2d3a2e6b4c 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java @@ -33,6 +33,7 @@ import org.springframework.web.server.session.DefaultWebSessionManager; import org.zowe.apiml.gateway.filters.ForbidCharacterException; import org.zowe.apiml.gateway.filters.ForbidSlashException; +import org.zowe.apiml.gateway.filters.ZaasInternalErrorException; import org.zowe.apiml.message.core.Message; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.log.ApimlLogger; @@ -163,7 +164,13 @@ public Mono handleStatusError(ServerWebExchange exchange, ResponseStatusEx @ExceptionHandler({ServiceNotAccessibleException.class, WebClientResponseException.ServiceUnavailable.class}) public Mono handleServiceNotAccessibleException(ServerWebExchange exchange, Exception ex) { log.debug("A service is not available at the moment to finish request {}: {}", exchange.getRequest().getURI(), ex.getMessage()); - return setBodyResponse(exchange, SC_SERVICE_UNAVAILABLE, "org.zowe.apiml.common.serviceUnavailable", exchange.getRequest().getURI()); + return setBodyResponse(exchange, SC_SERVICE_UNAVAILABLE, "org.zowe.apiml.common.serviceUnavailable", exchange.getRequest().getPath()); + } + + @ExceptionHandler(ZaasInternalErrorException.class) + public Mono handleZaasInternalErrorException(ServerWebExchange exchange, ZaasInternalErrorException ex) { + log.debug("The ZAAS instance {} return internal server error for request {}: {}", ex.getInstanceId(), exchange.getRequest().getURI(), ex.getMessage()); + return setBodyResponse(exchange, SC_INTERNAL_SERVER_ERROR, "org.zowe.apiml.gateway.zaas.internalServerError", ex.getInstanceId()); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java index 53ad09503c..207e6c1e6c 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java @@ -13,6 +13,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.gateway.filter.GatewayFilter; @@ -35,12 +36,12 @@ import java.net.HttpCookie; import java.security.cert.CertificateEncodingException; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; -import static org.apache.hc.core5.http.HttpStatus.SC_OK; -import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.hc.core5.http.HttpStatus.*; import static org.zowe.apiml.constants.ApimlConstants.PAT_COOKIE_AUTH_NAME; import static org.zowe.apiml.constants.ApimlConstants.PAT_HEADER_NAME; import static org.zowe.apiml.gateway.x509.ForwardClientCertFilterFactory.CLIENT_CERT_HEADER; @@ -111,9 +112,11 @@ * private String token; * } */ +@Slf4j public abstract class AbstractAuthSchemeFactory extends AbstractGatewayFilterFactory { private static final String HEADER_SERVICE_ID = "X-Service-Id"; + private static final String SERVICE_IS_UNAVAILABLE_MESSAGE = "There are no instance of ZAAS available"; private static final String[] CERTIFICATE_HEADERS = { "X-Certificate-Public", @@ -169,14 +172,35 @@ private Mono> getZaasInstances() { private Mono> requestWithHa( Iterator serviceInstanceIterator, - Function> requestCreator + Function> requestCreator, + AtomicReference> mostCriticalException // to be accessible and updatable in all lambdas below ) { - return requestCreator.apply(serviceInstanceIterator.next()) + // selected instance of ZAAS to invoke + var zaasInstance = serviceInstanceIterator.next(); + + // this lambda creates a chain of call over all instances. It also remembers the most critical exception to + // be thrown in case all instances fail + Function>> callNext = exception -> { + // select the most critical exception to remember (ZaasInternalErrorException is more important one) + exception = mostCriticalException.get().filter(ZaasInternalErrorException.class::isInstance).orElse(exception); + mostCriticalException.set(Optional.of(exception)); + + if (serviceInstanceIterator.hasNext()) { + return requestWithHa(serviceInstanceIterator, requestCreator, mostCriticalException); + } else { + return Mono.error(exception); + } + }; + + return requestCreator.apply(zaasInstance) .exchangeToMono(clientResp -> switch (clientResp.statusCode().value()) { case SC_UNAUTHORIZED -> Mono.just(new AuthorizationResponse<>(clientResp.headers(), null)); case SC_OK -> clientResp.bodyToMono(getResponseClass()).map(b -> new AuthorizationResponse<>(clientResp.headers(), b)); - default -> serviceInstanceIterator.hasNext() ? requestWithHa(serviceInstanceIterator, requestCreator) : Mono.just(new AuthorizationResponse<>(clientResp.headers(), null)); - }); + case SC_INTERNAL_SERVER_ERROR -> callNext.apply(new ZaasInternalErrorException(zaasInstance, "An internal exception occurred in ZAAS service. Check its configuration of instance " + zaasInstance.getInstanceId() + ".")); + default -> callNext.apply(new ServiceNotAccessibleException(SERVICE_IS_UNAVAILABLE_MESSAGE)); + }) + .doOnError(t -> log.debug("Error on calling ZAAS service instance {}: {}", zaasInstance.getInstanceId(), t.getMessage())) + .onErrorResume(e -> callNext.apply(new ServiceNotAccessibleException(SERVICE_IS_UNAVAILABLE_MESSAGE))); } protected Mono invoke( @@ -186,10 +210,12 @@ protected Mono invoke( ) { Iterator i = robinRound.getIterator(serviceInstances); if (!i.hasNext()) { - throw new ServiceNotAccessibleException("There are no instance of ZAAS available"); + throw new ServiceNotAccessibleException(SERVICE_IS_UNAVAILABLE_MESSAGE); } - return requestWithHa(i, requestCreator).switchIfEmpty(Mono.just(new AuthorizationResponse<>(null,null))).flatMap(responseProcessor); + return requestWithHa(i, requestCreator, new AtomicReference<>(Optional.empty())) + .switchIfEmpty(Mono.just(new AuthorizationResponse<>(null,null))) + .flatMap(responseProcessor); } /** diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasInternalErrorException.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasInternalErrorException.java new file mode 100644 index 0000000000..57f74f4a56 --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasInternalErrorException.java @@ -0,0 +1,26 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.filters; + +import lombok.Getter; +import org.springframework.cloud.client.ServiceInstance; + +public class ZaasInternalErrorException extends Exception { + + @Getter + private final String instanceId; + + public ZaasInternalErrorException(ServiceInstance zaasInstance, String message) { + super(message); + this.instanceId = zaasInstance.getInstanceId(); + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/ZaasServiceIsNotAvailableException.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/ZaasServiceIsNotAvailableException.java deleted file mode 100644 index 279155eb61..0000000000 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/ZaasServiceIsNotAvailableException.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.gateway.service; - -public class ZaasServiceIsNotAvailableException extends RuntimeException { - - public ZaasServiceIsNotAvailableException(String msg) { - super(msg); - } - -} diff --git a/gateway-service/src/main/resources/gateway-log-messages.yml b/gateway-service/src/main/resources/gateway-log-messages.yml index 1ca608cd1a..a7b7dd3843 100644 --- a/gateway-service/src/main/resources/gateway-log-messages.yml +++ b/gateway-service/src/main/resources/gateway-log-messages.yml @@ -119,6 +119,13 @@ messages: reason: "Cannot connect to the Gateway service." action: "Make sure that the external Gateway service is running and the truststore of the both Gateways contain the corresponding certificate." + - key: org.zowe.apiml.gateway.zaas.internalServerError + number: ZWESG101 + type: ERROR + text: "An internal exception occurred in ZAAS service %s." + reason: "ZAAS cannot process authentication required to finish the request." + action: "Make sure that the ZAAS is configured well and check all security requirements." + - key: org.zowe.apiml.gateway.connectionsLimitApproached number: ZWESG429 type: ERROR diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/PassticketTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/PassticketTest.java index 5bff86f9b8..098abbef18 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/PassticketTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/PassticketTest.java @@ -28,8 +28,8 @@ import java.util.Base64; import static io.restassured.RestAssured.given; -import static org.apache.http.HttpStatus.SC_OK; -import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.http.HttpStatus.*; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -42,7 +42,6 @@ public class PassticketTest extends AcceptanceTestWithMockServices { private static final String JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjcxNDYxNjIzLCJleHAiOjE2NzE0OTA0MjMsImlzcyI6IkFQSU1MIiwianRpIjoiYmFlMTkyZTYtYTYxMi00MThhLWI2ZGMtN2I0NWI5NzM4ODI3IiwiZG9tIjoiRHVtbXkgcHJvdmlkZXIifQ.Vt5UjJUlbmuzmmEIodAACtj_AOxlsWqkFrFyWh4_MQRRPCj_zMIwnzpqRN-NJvKtUg1zxOCzXv2ypYNsglrXc7cH9wU3leK1gjYxK7IJjn2SBEb0dUL5m7-h4tFq2zNhcGH2GOmTpE2gTQGSTvDIdja-TIj_lAvUtbkiorm1RqrNu2MGC0WfgOGiak3tj2tNJLv_Y1ZMxNjzyHgXBMuNPozQrd4Vtnew3x4yy85LrTYF7jJM3U-e3AD2yImftxwycQvbkjNb-lWadejTVH0MgHMr04wVdDd8Nq5q7yrZf7YPzhias8ehNbew5CHiKut9SseZ1sO2WwgfhpEfsN4okg"; private static final String PASSTICKET = "ZOWE_DUMMY_PASS_TICKET"; - @Test void whenRequestingPassticketForAllowedAPPLID_thenTranslate() throws IOException { TicketResponse response = new TicketResponse(); @@ -51,7 +50,7 @@ void whenRequestingPassticketForAllowedAPPLID_thenTranslate() throws IOException response.setApplicationName("IZUDFLT"); response.setTicket(PASSTICKET); - mockService("zaas").scope(MockService.Scope.CLASS) + mockService("zaas").scope(MockService.Scope.TEST) .addEndpoint("/zaas/scheme/ticket") .assertion(he -> assertEquals(SERVICE_ID, he.getRequestHeaders().getFirst("X-Service-Id"))) .assertion(he -> assertEquals(COOKIE_NAME + "=" + JWT, he.getRequestHeaders().getFirst("Cookie"))) @@ -59,7 +58,7 @@ void whenRequestingPassticketForAllowedAPPLID_thenTranslate() throws IOException .and().start(); String expectedAuthHeader = "Basic " + Base64.getEncoder().encodeToString((USER_ID + ":" + PASSTICKET).getBytes(StandardCharsets.UTF_8)); - var mockService = mockService(SERVICE_ID) + var mockService = mockService(SERVICE_ID).scope(MockService.Scope.TEST) .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid("IZUDFLT") .addEndpoint("/" + SERVICE_ID + "/test") .assertion(he -> assertEquals(expectedAuthHeader, he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) @@ -74,29 +73,69 @@ void whenRequestingPassticketForAllowedAPPLID_thenTranslate() throws IOException assertEquals(1, mockService.getEndpoint().getCounter()); } - - @ParameterizedTest - @ValueSource(ints = {400, 401, 403, 404, 405, 500}) - void whenCannotGeneratePassticket_thenIgnoreTransformation(int responseCode) throws IOException { + @Test + void whenCredentialsAreMissingOrInvalid_thenIgnoreTransformation() throws IOException { mockService("zaas").scope(MockService.Scope.TEST) .addEndpoint("/zaas/scheme/ticket") - .responseCode(responseCode) + .responseCode(SC_UNAUTHORIZED) .and().start(); - var mockService = mockService(SERVICE_ID).scope(MockService.Scope.TEST) + var service = mockService(SERVICE_ID).scope(MockService.Scope.TEST) .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid("IZUDFLT") .addEndpoint("/" + SERVICE_ID + "/test") - .responseCode(401) - .bodyJson(new ResponseDto("ok")) + .responseCode(SC_UNAUTHORIZED) + .bodyJson(new ResponseDto("ok")) .assertion(he -> assertFalse(he.getRequestHeaders().containsKey(HttpHeaders.AUTHORIZATION))) .and().start(); given() .cookie(COOKIE_NAME, JWT) - .when() + .when() .get(basePath + "/" + SERVICE_ID + "/api/v1/test") - .then() + .then() .statusCode(Matchers.is(SC_UNAUTHORIZED)) .body("status", Matchers.is("ok")); - assertEquals(1, mockService.getEndpoint().getCounter()); + assertEquals(1, service.getEndpoint().getCounter()); + } + + @Test + void whenZaasIsMisconfigured_thenReturnError() throws IOException { + var zaas = mockService("zaas").scope(MockService.Scope.TEST) + .addEndpoint("/zaas/scheme/ticket") + .responseCode(SC_INTERNAL_SERVER_ERROR) + .and().start(); + var service = mockService(SERVICE_ID).scope(MockService.Scope.TEST) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid("IZUDFLT") + .addEndpoint("/" + SERVICE_ID + "/test") + .and().start(); + given() + .cookie(COOKIE_NAME, JWT) + .when() + .get(basePath + "/" + SERVICE_ID + "/api/v1/test") + .then() + .statusCode(Matchers.is(SC_INTERNAL_SERVER_ERROR)) + .body("messages[0].messageKey", is("org.zowe.apiml.gateway.zaas.internalServerError")) + .body("messages[0].messageContent", is("An internal exception occurred in ZAAS service " + zaas.getInstanceId() + ".")); + assertEquals(0, service.getEndpoint().getCounter()); + } + + @ParameterizedTest(name = "When ZAAS returns {0} the Gateway response with 503") + @ValueSource(ints = {400, 403, 404, 405}) + void whenCannotGeneratePassticket_thenReturn503(int responseCode) throws IOException { + mockService("zaas").scope(MockService.Scope.TEST) + .addEndpoint("/zaas/scheme/ticket") + .responseCode(responseCode) + .and().start(); + var service = mockService(SERVICE_ID).scope(MockService.Scope.TEST) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid("IZUDFLT") + .addEndpoint("/" + SERVICE_ID + "/test") + .and().start(); + given() + .cookie(COOKIE_NAME, JWT) + .when() + .get(basePath + "/" + SERVICE_ID + "/api/v1/test") + .then() + .statusCode(Matchers.is(SC_SERVICE_UNAVAILABLE)) + .body("messages[0].messageKey", is("org.zowe.apiml.common.serviceUnavailable")); + assertEquals(0, service.getEndpoint().getCounter()); } @Data @@ -106,7 +145,5 @@ static class ResponseDto { private String status; } -} - - +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java index fe0013d738..5ba2e460b3 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java @@ -102,7 +102,7 @@ void createAllZaasServices() throws IOException { } @Test - void givenNoInstanceOfZosmf_whenCallingAService_thenReturn503() { + void givenNoInstanceOfZaas_whenCallingAService_thenReturn503() { zaasZombie.stop(); zaasError.stop(); zaasOk.stop(); @@ -112,7 +112,7 @@ void givenNoInstanceOfZosmf_whenCallingAService_thenReturn503() { } @Test - void givenInstanceOfZosmf_whenCallingAService_thenReturn200() throws IOException { + void givenInstanceOfZaas_whenCallingAService_thenReturn200() throws IOException { zaasZombie.stop(); zaasError.stop(); zaasOk.start(); @@ -122,7 +122,7 @@ void givenInstanceOfZosmf_whenCallingAService_thenReturn200() throws IOException } @Test - void givenZombieAndOkInstanceOfZosmf_whenCallingAService_preventZombieOne() throws IOException { + void givenZombieAndOkInstanceOfZaas_whenCallingAService_preventZombieOne() throws IOException { zaasZombie.zombie(); zaasError.stop(); zaasOk.start(); @@ -135,17 +135,17 @@ void givenZombieAndOkInstanceOfZosmf_whenCallingAService_preventZombieOne() thro } @Test - void givenOnlyZombieZosmf_whenCallingAService_return500() { + void givenOnlyZaas_whenCallingAService_return503() { zaasZombie.zombie(); zaasError.stop(); zaasOk.stop(); - given().when().get(getServiceUrl()).then().statusCode(500); + given().when().get(getServiceUrl()).then().statusCode(503); assertEquals(0, service.getCounter()); } @Test - void givenZombieAndErrorZosmf_whenCallingAService_return500() throws IOException { + void givenZombieAndErrorZaas_whenCallingAService_return500() throws IOException { zaasZombie.zombie(); zaasError.start(); zaasOk.stop(); @@ -155,7 +155,7 @@ void givenZombieAndErrorZosmf_whenCallingAService_return500() throws IOException } @Test - void givenZombieFailingAndSuccessZosmf_whenCallingAService_return200() throws IOException { + void givenZombieFailingAndSuccessZaas_whenCallingAService_return200() throws IOException { zaasZombie.zombie(); zaasError.start(); zaasOk.start(); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MockService.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MockService.java index efb1766b77..6b8e23cfbf 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MockService.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MockService.java @@ -304,7 +304,7 @@ private Map getMetadata() { */ public InstanceInfo getInstanceInfo() { return InstanceInfo.Builder.newBuilder() - .setInstanceId(serviceId) + .setInstanceId(getInstanceId()) .setHostName(hostname) .setPort(port) .setAppName(serviceId) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java index e8d1a8e0c5..d68f0c18e6 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java @@ -10,18 +10,28 @@ package org.zowe.apiml.gateway.config; +import com.fasterxml.jackson.core.JsonProcessingException; import io.netty.channel.ChannelOption; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; +import org.hamcrest.Matchers; import org.junit.jupiter.api.*; import org.springframework.cloud.gateway.route.Route; +import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.auth.AuthenticationScheme; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; +import org.zowe.apiml.gateway.acceptance.common.MockService; +import org.zowe.apiml.ticket.TicketResponse; import reactor.netty.http.client.HttpClient; import javax.net.ssl.SSLException; +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static org.springframework.cloud.gateway.support.RouteMetadataUtils.CONNECT_TIMEOUT_ATTR; @@ -137,4 +147,72 @@ void givenTimeoutAndRequirementsForClientCert_whenGetHttpClient_thenCallWithoutC } + @Nested + class UnavailableService extends AcceptanceTestWithMockServices { + + @BeforeAll + void setUp() { + mockService("service").scope(MockService.Scope.CLASS).start().zombie(); + } + + @Test + void allInstancesAreUnavailable() { + given().when().get(basePath + "/service/api/v1/test") + .then() + .statusCode(Matchers.is(SC_SERVICE_UNAVAILABLE)); + } + + @RepeatedTest(5) + void someInstancesAreUnavailable() { + mockService("service").scope(MockService.Scope.TEST) + .addEndpoint("/service/test").responseCode(200) + .and().start(); + given().when().get(basePath + "/service/api/v1/test") + .then() + .statusCode(Matchers.is(SC_OK)); + } + + } + + @Nested + class UnavailableZaas extends AcceptanceTestWithMockServices { + + private static final String USER = "user"; + private static final String PASSTICKET = "password"; + private static final String APPLID = "SRVTST"; + private static final String BASIC_HEADER = "Basic dXNlcjpwYXNzd29yZA=="; + + @BeforeAll + void setUp() { + mockService("service").scope(MockService.Scope.CLASS).authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid(APPLID) + .addEndpoint("/service/test").responseCode(200) + .assertion(ha -> assertEquals(BASIC_HEADER, ha.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) + .and().start(); + } + + @Test + void noZaasServiceIsRegistered() { + given().when().get(basePath + "/service/api/v1/test") + .then().statusCode(Matchers.is(SC_SERVICE_UNAVAILABLE)); + } + + @Test + void noZaasServiceAvailable() { + mockService("zaas").scope(MockService.Scope.TEST).start().zombie(); + given().when().get(basePath + "/service/api/v1/test") + .then().statusCode(Matchers.is(SC_SERVICE_UNAVAILABLE)); + } + + @RepeatedTest(5) + void someZaasServiceIsUnavailable() throws JsonProcessingException { + mockService("zaas").scope(MockService.Scope.TEST).start().zombie(); + mockService("zaas").scope(MockService.Scope.TEST) + .addEndpoint("/zaas/scheme/ticket").bodyJson(new TicketResponse(null, USER, APPLID, PASSTICKET)) + .and().start(); + given().when().get(basePath + "/service/api/v1/test") + .then().statusCode(Matchers.is(SC_OK)); + } + + } + } diff --git a/gateway-service/src/test/resources/gateway-log-messages.yml b/gateway-service/src/test/resources/gateway-log-messages.yml deleted file mode 100644 index 81a2bc7e8c..0000000000 --- a/gateway-service/src/test/resources/gateway-log-messages.yml +++ /dev/null @@ -1,90 +0,0 @@ -messages: - # Info messages - # 000-099 - - # General messages - # 100-199 - - # HTTP,Protocol messages - # 400-499 - - # TLS,Certificate messages - # 500-599 - - # Various messages - # 600-699 - - # Service specific messages - # 700-999 - - - key: org.zowe.apiml.gateway.requestContainEncodedCharacter - number: ZWEAG701 - type: ERROR - text: "Service '%s' does not allow encoded characters in the request path: '%s'." - reason: "The request that was issued to the Gateway contains an encoded character in the URL path. The service that the request was addressing does not allow this pattern." - action: "Contact the system administrator and request enablement of encoded characters in the service." - - - key: org.zowe.apiml.gateway.requestContainEncodedSlash - number: ZWEAG702 - type: ERROR - text: "Gateway does not allow encoded slashes in request: '%s'." - reason: "The request that was issued to the Gateway contains an encoded slash in the URL path. Gateway configuration does not allow this encoding in the URL." - action: "Contact the system administrator and request enablement of encoded slashes in the Gateway." - - - key: org.zowe.apiml.gateway.verifier.wrongServiceId - number: ZWEAG717 - type: ERROR - text: "The service id provided is invalid: '%s'" - reason: "The provided id is not valid under conformance criteria." - action: "Verify the conformance criteria, provide valid service id." - - - key: org.zowe.apiml.gateway.verifier.noMetadata - number: ZWEAG718 - type: ERROR - text: "Cannot retrieve metadata: '%s'" - reason: "Metadata aren't accessible" - action: "Verify that the metadata are accessible and not empty" - - - key: org.zowe.apiml.gateway.verifier.nonConformant - number: ZWEAG719 - type: INFO - text: "The service is not conformant: %s" - reason: "The provided service does not satisfy the conformance criteria and is therefore not valid." - action: "Verify the conformance criteria." - - # Legacy messages - - - key: org.zowe.apiml.security.generic - number: ZWEAG100 - type: ERROR - text: "Authentication exception: '%s' for URL '%s'" - reason: "A generic failure occurred during authentication." - action: "Refer to the specific authentication exception details for troubleshooting." - - - key: org.zowe.apiml.security.invalidMethod - number: ZWEAG101 - type: ERROR - text: "Authentication method '%s' is not supported for URL '%s'" - reason: "The HTTP request method is not supported by the URL." - action: "Use the correct HTTP request method supported by the URL." - - - key: org.zowe.apiml.security.authRequired - number: ZWEAG105 - type: ERROR - text: "Authentication is required for URL '%s'" - reason: "Authentication is required." - action: "Provide valid authentication." - - - key: org.zowe.apiml.security.loginEndpointInDummyMode - number: ZWEAG106 - type: WARNING - text: "Login endpoint is running in dummy mode. Use credentials '%s'/'%s' to log in. Do not use this option in the production environment." - reason: "The authentication is running in dummy mode." - action: "Ensure that this option is not being used in a production environment." - - - key: org.zowe.apiml.gateway.security.schema.missingX509Authentication - number: ZWEAG167 - type: ERROR - text: "No client certificate provided in the request" - reason: "The X509 client certificate was not provided with the request" - action: "Configure your client to provide valid certificate." diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java index 7c9f9d142a..11c84e424a 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java @@ -25,8 +25,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.util.TestWithStartedInstances; import org.zowe.apiml.util.categories.*; @@ -279,19 +279,19 @@ void givenCustomHeader() { @Nested class PassticketMisconfiguration { - @ParameterizedTest - @ValueSource(strings = { - "/dcpassticketxbadappl/api/v1/request", - "/dcnopassticket/api/v1/request" + @ParameterizedTest(name = "{2} ({0})") + @CsvSource({ + "/dcpassticketxbadappl/api/v1/request,500,Passticket is misconfigured then return 500", + "/dcnopassticket/api/v1/request,200,When APPLID is not set then passticket is not set and the Gateway returns 200" }) - void givenJwt(String url) { + void givenJwt(String url, int responseCode, String description) { given() .cookie(COOKIE_NAME, jwt) .when() .get(HttpRequestUtils.getUriFromGateway(url)) .then() .body("headers.authorization", Matchers.nullValue()) - .statusCode(200); + .statusCode(responseCode); } }