diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 222ae185fe..b3e25359d8 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -468,7 +468,7 @@ jobs: - name: Dump DC jacoco data run: > - java -jar ./scripts/jacococli.jar dump --address gateway-service --port 6300 --destfile ./results/gateway-service.exec + java -jar ./scripts/jacococli.jar dump --address zaas-service --port 6301 --destfile ./results/zaas-service.exec - name: Store results uses: actions/upload-artifact@v4 @@ -1485,6 +1485,10 @@ jobs: with: name: GatewayProxy-${{ env.JOB_ID }} path: GatewayProxy + - uses: actions/download-artifact@v4 + with: + name: ContainerCITestsZaas-${{ env.JOB_ID }} + path: ContainerCITestsZaas - uses: actions/download-artifact@v4 with: name: CITestsWebSocketChaoticHA-${{ env.JOB_ID }} @@ -1496,7 +1500,7 @@ jobs: - name: Code coverage and publish results run: > - ./gradlew --info coverage sonar -Dresults="containercitests/results,citestswithinfinispan/results,GatewayProxy/results,citestswebsocketchaoticha/results,GatewayServiceRouting/results,containercitestszaas/results" + ./gradlew --info coverage sonar -Dresults="containercitests/results,citestswithinfinispan/results,GatewayProxy/results,citestswebsocketchaoticha/results,GatewayServiceRouting/results,ContainerCITestsZaas/results" -Psonar.host.url=$SONAR_HOST_URL -Dsonar.token=$SONAR_TOKEN -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java index cc2b0e8937..830f5e331b 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java @@ -15,7 +15,6 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import org.zowe.apiml.product.compatibility.ApimlErrorController; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.http.HttpServletRequest; @@ -26,11 +25,7 @@ */ @Controller @Order(Ordered.HIGHEST_PRECEDENCE) -public class NotFoundErrorController implements ApimlErrorController { - - - private static final String PATH = "/not_found"; // NOSONAR - +public class NotFoundErrorController { @GetMapping(value = "/not_found") public String handleError(HttpServletRequest request) { @@ -48,10 +43,6 @@ public String handleError(HttpServletRequest request) { return "error"; } - @Override - public String getErrorPath() { - return PATH; - } } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java index 40136ea7bc..581bad4fb9 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java @@ -130,8 +130,8 @@ private UserDetailsService x509UserDetailsService() { private CategorizeCertsFilter reversedCategorizeCertFilter() { CategorizeCertsFilter out = new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator); - out.setCertificateForClientAuth(crt -> out.getPublicKeyCertificatesBase64().contains(out.base64EncodePublicKey(crt))); - out.setApimlCertificate(crt -> !out.getPublicKeyCertificatesBase64().contains(out.base64EncodePublicKey(crt))); + out.setCertificateForClientAuth(crt -> out.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); + out.setApimlCertificate(crt -> !out.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); return out; } } diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index c1ef09968f..dc40a2223b 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -36,7 +36,7 @@ describe('>>> Service version compare Test', () => { ); cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div .find('a.MuiTab-root') // Find all the anchor elements within the div - .should('have.length', 13); // Check if there are 13 anchor elements within the div -- changed from 12 to 13 after adding Discoverable client with GraphQL + .should('have.length', 15); // Check if there are 15 anchor elements within the div cy.get('.version-text').should('exist'); cy.get('.version-text').should('contain.text', 'Compare'); }); diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/ApimlErrorController.java b/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/ApimlErrorController.java deleted file mode 100644 index 5b32b5821e..0000000000 --- a/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/ApimlErrorController.java +++ /dev/null @@ -1,22 +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.product.compatibility; - -import org.springframework.boot.web.servlet.error.ErrorController; - -/** - * This class is used to reconcile the breaking change between Spring Boot 2.5 and Zuul. The breaking change - * is due to ErrorController.getErrorPath being removed in Spring Boot 2.5. A BeanPostProcessor is used - * to proxy ZuulHandlerMapping, intercepting the code execution that leads to the NoSuchMethodError. - */ -public interface ApimlErrorController extends ErrorController { - String getErrorPath(); -} diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/EndpointImproprietyConfigureException.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/EndpointImproperlyConfigureException.java similarity index 71% rename from apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/EndpointImproprietyConfigureException.java rename to apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/EndpointImproperlyConfigureException.java index 9af8241e03..edca4281a1 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/EndpointImproprietyConfigureException.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/EndpointImproperlyConfigureException.java @@ -12,19 +12,19 @@ import lombok.Getter; -public class EndpointImproprietyConfigureException extends RuntimeException { +public class EndpointImproperlyConfigureException extends RuntimeException { private static final long serialVersionUID = -4582785501782402751L; @Getter private final String endpoint; - public EndpointImproprietyConfigureException(String message, String endpoint) { + public EndpointImproperlyConfigureException(String message, String endpoint) { super(message); this.endpoint = endpoint; } - public EndpointImproprietyConfigureException(String message, String endpoint, Throwable cause) { + public EndpointImproperlyConfigureException(String message, String endpoint, Throwable cause) { super(message, cause); this.endpoint = endpoint; } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpoint.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpoint.java index 61bbba9f9b..e4efc6979a 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpoint.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpoint.java @@ -61,13 +61,13 @@ public boolean hasSafResourceAccess(Authentication authentication, String resour ); Response response = responseEntity.getBody(); if (response != null && response.isError()) { - throw new EndpointImproprietyConfigureException("Endpoint " + endpointUrl + " is not properly configured: " + response.getMessage(), endpointUrl); + throw new EndpointImproperlyConfigureException("Endpoint " + endpointUrl + " is not properly configured: " + response.getMessage(), endpointUrl); } return response != null && !response.isError() && response.isAuthorized(); - } catch (EndpointImproprietyConfigureException e) { + } catch (EndpointImproperlyConfigureException e) { throw e; } catch (Exception e) { - throw new EndpointImproprietyConfigureException("Endpoint " + endpointUrl + " is not properly configured.", endpointUrl, e); + throw new EndpointImproperlyConfigureException("Endpoint " + endpointUrl + " is not properly configured.", endpointUrl, e); } } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java index cf7db4f4a1..9e93b800a6 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java @@ -79,7 +79,7 @@ protected void writeErrorResponse(ApiMessageView message, HttpStatus status, Htt mapper.writeValue(response.getWriter(), message); } catch (IOException e) { apimlLog.log("org.zowe.apiml.security.errorWrittingResponse", e.getMessage()); - throw new ServletException("Error writting response", e); + throw new ServletException("Error writing response", e); } } } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java index 9729c1518e..159110cab3 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java @@ -19,6 +19,7 @@ import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -67,20 +68,18 @@ public class CategorizeCertsFilter extends OncePerRequestFilter { private void categorizeCerts(ServletRequest request) { X509Certificate[] certs = (X509Certificate[]) request.getAttribute(ATTRNAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE); if (certs != null) { - if (certificateValidator.isForwardingEnabled() && certificateValidator.isTrusted(certs)) { + Optional clientCert = getClientCertFromHeader((HttpServletRequest) request); + if (certificateValidator.isForwardingEnabled() && certificateValidator.isTrusted(certs) && clientCert.isPresent()) { certificateValidator.updateAPIMLPublicKeyCertificates(certs); - Optional clientCert = getClientCertFromHeader((HttpServletRequest) request); - if (clientCert.isPresent()) { - // add the client certificate to the certs array - String subjectDN = ((X509Certificate) clientCert.get()).getSubjectX500Principal().getName(); - log.debug("Found client certificate in header, adding it to the request. Subject DN: {}", subjectDN); - certs = Arrays.copyOf(certs, certs.length + 1); - certs[certs.length - 1] = (X509Certificate) clientCert.get(); - } + // add the client certificate to the certs array + String subjectDN = ((X509Certificate) clientCert.get()).getSubjectX500Principal().getName(); + log.debug("Found client certificate in header, adding it to the request. Subject DN: {}", subjectDN); + request.setAttribute(ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(new X509Certificate[]{(X509Certificate) clientCert.get()}, certificateForClientAuth)); + } else { + request.setAttribute(ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth)); + request.setAttribute(ATTRNAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, apimlCertificate)); } - request.setAttribute(ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth)); - request.setAttribute(ATTRNAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, apimlCertificate)); log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE, request.getAttribute(ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE)); } } @@ -144,24 +143,19 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht } private X509Certificate[] selectCerts(X509Certificate[] certs, Predicate test) { - return Arrays.stream(certs) - .filter(test) - .toList().toArray(new X509Certificate[0]); + if (test.test(certs[0])) { + return certs; + } + return new X509Certificate[0]; } - public String base64EncodePublicKey(X509Certificate cert) { + public static String base64EncodePublicKey(X509Certificate cert) { return Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded()); } - public void setCertificateForClientAuth(Predicate certificateForClientAuth) { - this.certificateForClientAuth = certificateForClientAuth; - } - - public void setApimlCertificate(Predicate apimlCertificate) { - this.apimlCertificate = apimlCertificate; - } - + @Setter Predicate certificateForClientAuth = crt -> !getPublicKeyCertificatesBase64().contains(base64EncodePublicKey(crt)); + @Setter Predicate apimlCertificate = crt -> getPublicKeyCertificatesBase64().contains(base64EncodePublicKey(crt)); } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java index 00ccab6380..f5e716b164 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java @@ -22,11 +22,10 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.Base64; import java.util.List; import java.util.Set; -import static java.util.Collections.emptyList; +import static org.zowe.apiml.security.common.filter.CategorizeCertsFilter.base64EncodePublicKey; /** * Service to verify if given certificate chain can be trusted. @@ -63,7 +62,11 @@ public CertificateValidator(TrustedCertificatesProvider trustedCertificatesProvi * @return true if all given certificates are known false otherwise */ public boolean isTrusted(X509Certificate[] certs) { - List trustedCerts = StringUtils.isBlank(proxyCertificatesEndpoint) ? emptyList() : trustedCertificatesProvider.getTrustedCerts(proxyCertificatesEndpoint); + if (StringUtils.isBlank(proxyCertificatesEndpoint)) { + log.debug("No endpoint configured to retrieve trusted certificates. Provide URL via apiml.security.x509.certificatesUrl"); + return false; + } + List trustedCerts = trustedCertificatesProvider.getTrustedCerts(proxyCertificatesEndpoint); for (X509Certificate cert : certs) { if (!trustedCerts.contains(cert)) { apimlLog.log("org.zowe.apiml.security.common.verify.untrustedCert"); @@ -71,7 +74,7 @@ public boolean isTrusted(X509Certificate[] certs) { return false; } } - log.debug("All certificates are trusted."); + log.debug("The whole certificate chain is trusted."); return true; } @@ -82,8 +85,10 @@ public boolean isTrusted(X509Certificate[] certs) { */ public void updateAPIMLPublicKeyCertificates(X509Certificate[] certs) { for (X509Certificate cert : certs) { - String publicKey = Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded()); + String publicKey = base64EncodePublicKey(cert); publicKeyCertificatesBase64.add(publicKey); } } + + } diff --git a/apiml-security-common/src/main/resources/security-common-log-messages.yml b/apiml-security-common/src/main/resources/security-common-log-messages.yml index 7e1b569180..b8291f204e 100644 --- a/apiml-security-common/src/main/resources/security-common-log-messages.yml +++ b/apiml-security-common/src/main/resources/security-common-log-messages.yml @@ -147,7 +147,7 @@ messages: reason: "The parameter `apiml.security.authorization.provider` is set to `endpoint`" action: "Change the SAF provider to another one to use this endpoint" - - key: org.zowe.apiml.security.common.auth.saf.endpoint.endpointImproprietyConfigure + - key: org.zowe.apiml.security.common.auth.saf.endpoint.endpointImproperlyConfigure number: ZWEAT603 type: ERROR text: "Endpoint `%s` is not properly configured" diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpointTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpointTest.java index 1ac1e72ff1..91f568a498 100644 --- a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpointTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessEndpointTest.java @@ -82,7 +82,7 @@ void testHasSafResourceAccess_whenErrorHappened_thenFalse(boolean authorized) { ).when(restTemplate).exchange( eq(TEST_URI_ARGS), eq(HttpMethod.GET), any(), eq(SafResourceAccessEndpoint.Response.class), eq(RESOURCE), eq(LEVEL) ); - assertThrows(EndpointImproprietyConfigureException.class, () -> safResourceAccessEndpoint.hasSafResourceAccess(authentication, SUPPORTED_CLASS, RESOURCE, LEVEL)); + assertThrows(EndpointImproperlyConfigureException.class, () -> safResourceAccessEndpoint.hasSafResourceAccess(authentication, SUPPORTED_CLASS, RESOURCE, LEVEL)); } @Test @@ -96,18 +96,18 @@ void givenFaultyResponse_whenRestTemplateMethodReturnsNull_thenFalse() { } @Test - void givenUnsupportedResouceClass_whenVerify_thenEndpointImproprietyConfigureException() { + void givenUnsupportedResouceClass_whenVerify_thenendpointImproperlyConfigureException() { assertThrows(UnsupportedResourceClassException.class, () -> safResourceAccessEndpoint.hasSafResourceAccess(authentication, UNSUPPORTED_CLASS, RESOURCE, LEVEL)); } @Test - void givenExceptionOnRestCall_whenVerifying_thenEndpointImproprietyConfigureException() { + void givenExceptionOnRestCall_whenVerifying_thenendpointImproperlyConfigureException() { doThrow( new RuntimeException() ).when(restTemplate).exchange( anyString(), any(), any(), eq(SafResourceAccessEndpoint.Response.class), anyString(), anyString() ); - assertThrows(EndpointImproprietyConfigureException.class, () -> safResourceAccessEndpoint.hasSafResourceAccess(authentication, SUPPORTED_CLASS, RESOURCE, LEVEL)); + assertThrows(EndpointImproperlyConfigureException.class, () -> safResourceAccessEndpoint.hasSafResourceAccess(authentication, SUPPORTED_CLASS, RESOURCE, LEVEL)); } } diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java index 45dd090795..c71467b3c4 100644 --- a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java @@ -68,6 +68,8 @@ class CategorizeCertsFilterTest { private MockHttpServletResponse response; private MockFilterChain chain; private X509Certificate[] certificates; + private X509Certificate[] clientCerts; + private CertificateValidator certificateValidator; @BeforeAll @@ -138,11 +140,10 @@ class WhenCertificatesInRequest { @BeforeEach void setUp() { + certificates = new X509Certificate[]{ X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1")), - X509Utils.getCertificate(X509Utils.correctBase64("apimlCert1")), - X509Utils.getCertificate(X509Utils.correctBase64("foreignCert2")), - X509Utils.getCertificate(X509Utils.correctBase64("apimlCert2")) + X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1CA")) }; request.setAttribute("jakarta.servlet.request.X509Certificate", certificates); } @@ -159,7 +160,7 @@ void thenAllClientCertificates() throws IOException, ServletException { X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); assertNotNull(clientCerts); - assertEquals(4, clientCerts.length); + assertEquals(2, clientCerts.length); assertArrayEquals(certificates, clientCerts); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); @@ -168,8 +169,8 @@ void thenAllClientCertificates() throws IOException, ServletException { @Test void thenAllApimlCertificatesWithReversedLogic() throws IOException, ServletException { - filter.setCertificateForClientAuth(crt -> filter.getPublicKeyCertificatesBase64().contains(filter.base64EncodePublicKey(crt))); - filter.setApimlCertificate(crt -> !filter.getPublicKeyCertificatesBase64().contains(filter.base64EncodePublicKey(crt))); + filter.setCertificateForClientAuth(crt -> filter.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); + filter.setApimlCertificate(crt -> !filter.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); filter.doFilter(request, response, chain); HttpServletRequest nextRequest = (HttpServletRequest) chain.getRequest(); @@ -181,7 +182,7 @@ void thenAllApimlCertificatesWithReversedLogic() throws IOException, ServletExce X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); assertNotNull(apimlCerts); - assertEquals(4, apimlCerts.length); + assertEquals(2, apimlCerts.length); assertArrayEquals(certificates, apimlCerts); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); @@ -201,10 +202,8 @@ public void setUp() { void givenTrustedCerts_thenClientCertHeaderAccepted() throws ServletException, IOException { when(certificateValidator.isTrusted(certificates)).thenReturn(true); // when incoming certs are all trusted means that all their public keys are added to the filter - filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("foreignCert1")); - filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("foreignCert2")); filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("apimlCert1")); - filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("apimlCert2")); + filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("apimlCertCA")); filter.doFilter(request, response, chain); HttpServletRequest nextRequest = (HttpServletRequest) chain.getRequest(); @@ -212,7 +211,7 @@ void givenTrustedCerts_thenClientCertHeaderAccepted() throws ServletException, I X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); assertNotNull(apimlCerts); - assertEquals(4, apimlCerts.length); + assertEquals(2, apimlCerts.length); assertArrayEquals(certificates, apimlCerts); X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); @@ -237,7 +236,7 @@ void givenNotTrustedCerts_thenClientCertHeaderIgnored() throws ServletException, X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); assertNotNull(clientCerts); - assertEquals(4, clientCerts.length); + assertEquals(2, clientCerts.length); assertArrayEquals(certificates, clientCerts); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); @@ -266,7 +265,7 @@ void thenClientCertHeaderIgnored() throws ServletException, IOException { X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); assertNotNull(clientCerts); - assertEquals(4, clientCerts.length); + assertEquals(2, clientCerts.length); assertArrayEquals(certificates, clientCerts); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); @@ -296,7 +295,7 @@ void thenCertificateInHeaderIgnored() throws ServletException, IOException { X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); assertNotNull(clientCerts); - assertEquals(4, clientCerts.length); + assertEquals(2, clientCerts.length); assertArrayEquals(certificates, clientCerts); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); @@ -333,10 +332,11 @@ class GivenPublicKeysInFilter { @BeforeEach void setUp() { - filter = new CategorizeCertsFilter(new HashSet<>(Arrays.asList( + var serverCertChain = new HashSet<>(Arrays.asList( X509Utils.correctBase64("apimlCert1"), - X509Utils.correctBase64("apimlCert2") - )), certificateValidator); + X509Utils.correctBase64("apimlCertCA") + )); + filter = new CategorizeCertsFilter(serverCertChain, certificateValidator); } @Nested @@ -345,31 +345,33 @@ class WhenCertificatesInRequest { @BeforeEach void setUp() { certificates = new X509Certificate[]{ - X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1")), X509Utils.getCertificate(X509Utils.correctBase64("apimlCert1")), - X509Utils.getCertificate(X509Utils.correctBase64("foreignCert2")), - X509Utils.getCertificate(X509Utils.correctBase64("apimlCert2")) + X509Utils.getCertificate(X509Utils.correctBase64("apimlCertCA")), + }; + + clientCerts = new X509Certificate[]{ + X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1")), + X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1CA")) }; + request.setAttribute("jakarta.servlet.request.X509Certificate", certificates); } @Test void thenCategorizedCerts() throws IOException, ServletException { + request.setAttribute("jakarta.servlet.request.X509Certificate", clientCerts); filter.doFilter(request, response, chain); HttpServletRequest nextRequest = (HttpServletRequest) chain.getRequest(); assertNotNull(nextRequest); X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); - assertNotNull(apimlCerts); - assertEquals(2, apimlCerts.length); - assertSame(certificates[1], apimlCerts[0]); - assertSame(certificates[3], apimlCerts[1]); + assertEquals(0, apimlCerts.length); - X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); - assertNotNull(clientCerts); - assertEquals(2, clientCerts.length); - assertSame(certificates[0], clientCerts[0]); - assertSame(certificates[2], clientCerts[1]); + X509Certificate[] clientCertsFromAttr = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); + assertNotNull(clientCertsFromAttr); + assertEquals(2, clientCertsFromAttr.length); + assertSame(clientCerts[0], clientCertsFromAttr[0]); + assertSame(clientCerts[1], clientCertsFromAttr[1]); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); assertFalse(nextRequest.getHeaders(CLIENT_CERT_HEADER).hasMoreElements()); @@ -377,24 +379,21 @@ void thenCategorizedCerts() throws IOException, ServletException { @Test void thenCategorizedCertsWithReversedLogic() throws IOException, ServletException { - filter.setCertificateForClientAuth(crt -> filter.getPublicKeyCertificatesBase64().contains(filter.base64EncodePublicKey(crt))); - filter.setApimlCertificate(crt -> !filter.getPublicKeyCertificatesBase64().contains(filter.base64EncodePublicKey(crt))); - + filter.setCertificateForClientAuth(crt -> filter.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); + filter.setApimlCertificate(crt -> !filter.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); + request.setAttribute("jakarta.servlet.request.X509Certificate", clientCerts); filter.doFilter(request, response, chain); HttpServletRequest nextRequest = (HttpServletRequest) chain.getRequest(); assertNotNull(nextRequest); - X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); - assertNotNull(clientCerts); - assertEquals(2, clientCerts.length); - assertSame(certificates[1], clientCerts[0]); - assertSame(certificates[3], clientCerts[1]); + X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); + assertEquals(0, apimlCerts.length); - X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); - assertNotNull(apimlCerts); - assertEquals(2, apimlCerts.length); - assertSame(certificates[0], apimlCerts[0]); - assertSame(certificates[2], apimlCerts[1]); + X509Certificate[] apimlCertificate = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); + assertNotNull(apimlCertificate); + assertEquals(2, apimlCertificate.length); + assertSame(clientCerts[0], apimlCertificate[0]); + assertSame(clientCerts[1], apimlCertificate[1]); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); assertFalse(nextRequest.getHeaders(CLIENT_CERT_HEADER).hasMoreElements()); @@ -413,10 +412,8 @@ public void setUp() { void givenTrustedCerts_thenClientCertHeaderAccepted() throws ServletException, IOException { when(certificateValidator.isTrusted(certificates)).thenReturn(true); // when incoming certs are all trusted means that all their public keys are added to the filter - filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("foreignCert1")); - filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("foreignCert2")); filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("apimlCert1")); - filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("apimlCert2")); + filter.getPublicKeyCertificatesBase64().add(X509Utils.correctBase64("apimlCertCA")); filter.doFilter(request, response, chain); HttpServletRequest nextRequest = (HttpServletRequest) chain.getRequest(); @@ -424,7 +421,7 @@ void givenTrustedCerts_thenClientCertHeaderAccepted() throws ServletException, I X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); assertNotNull(apimlCerts); - assertEquals(4, apimlCerts.length); + assertEquals(2, apimlCerts.length); assertArrayEquals(certificates, apimlCerts); X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); @@ -446,14 +443,12 @@ void givenNotTrustedCerts_thenClientCertHeaderIgnored() throws ServletException, X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); assertNotNull(apimlCerts); assertEquals(2, apimlCerts.length); - assertSame(certificates[1], apimlCerts[0]); - assertSame(certificates[3], apimlCerts[1]); + assertSame(certificates[0], apimlCerts[0]); + assertSame(certificates[1], apimlCerts[1]); X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); assertNotNull(clientCerts); - assertEquals(2, clientCerts.length); - assertSame(certificates[0], clientCerts[0]); - assertSame(certificates[2], clientCerts[1]); + assertEquals(0, clientCerts.length); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); assertFalse(nextRequest.getHeaders(CLIENT_CERT_HEADER).hasMoreElements()); @@ -478,14 +473,12 @@ void thenClientCertHeaderIgnored() throws ServletException, IOException { X509Certificate[] apimlCerts = (X509Certificate[]) nextRequest.getAttribute("jakarta.servlet.request.X509Certificate"); assertNotNull(apimlCerts); assertEquals(2, apimlCerts.length); - assertSame(certificates[1], apimlCerts[0]); - assertSame(certificates[3], apimlCerts[1]); + assertSame(certificates[0], apimlCerts[0]); + assertSame(certificates[1], apimlCerts[1]); X509Certificate[] clientCerts = (X509Certificate[]) nextRequest.getAttribute("client.auth.X509Certificate"); assertNotNull(clientCerts); - assertEquals(2, clientCerts.length); - assertSame(certificates[0], clientCerts[0]); - assertSame(certificates[2], clientCerts[1]); + assertEquals(0, clientCerts.length); assertNull(nextRequest.getHeader(CLIENT_CERT_HEADER)); assertFalse(nextRequest.getHeaders(CLIENT_CERT_HEADER).hasMoreElements()); diff --git a/config/docker/api-defs/staticclient.yml b/config/docker/api-defs/staticclient.yml index 563a202660..ed5f57eaa4 100644 --- a/config/docker/api-defs/staticclient.yml +++ b/config/docker/api-defs/staticclient.yml @@ -137,6 +137,45 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: dcpassticketxbadappl # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Discoverable client with passTicket authentication scheme (Misconfiguration - access rights) # Title of the service in the API catalog + description: Define service to test passTicket authentication schema for integration tests. + instanceBaseUrls: # list of base URLs for each instance + - https://discoverable-client:10012/discoverableclient # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: api/v1 # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: /api/v1 # relativePath that is added to baseUrl of an instance + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + applid: XBADAPPL # APPLID to generate PassTickets for this service + apiInfo: + - apiId: zowe.apiml.discoverableclient + gatewayUrl: api/v1 + version: 1.0.0 + + - serviceId: dcnopassticket # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Discoverable client with passTicket authentication scheme (Misconfiguration - no APPLID) # Title of the service in the API catalog + description: Define service to test passTicket authentication schema for integration tests. + instanceBaseUrls: # list of base URLs for each instance + - https://discoverable-client:10012/discoverableclient # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: api/v1 # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: /api/v1 # relativePath that is added to baseUrl of an instance + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + apiInfo: + - apiId: zowe.apiml.discoverableclient + gatewayUrl: api/v1 + version: 1.0.0 + - serviceId: dcsafidt # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) title: Discoverable client with safIdt authentication scheme # Title of the service in the API catalog diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 5b92ed228a..15047cbce0 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -111,6 +111,7 @@ services: serviceRelativeUrl: /api/v3 # relativePath that is added to baseUrl of an instance authentication: scheme: httpBasicPassTicket + applid: ZOWEAPPL apiInfo: - apiId: zowe.apiml.discoverableclient gatewayUrl: api/v3 @@ -138,6 +139,45 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: dcpassticketxbadappl # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Discoverable client with passTicket authentication scheme (Misconfiguration - access rights) # Title of the service in the API catalog + description: Define service to test passTicket authentication schema for integration tests. + instanceBaseUrls: # list of base URLs for each instance + - https://localhost:10012/discoverableclient # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: api/v1 # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: /api/v1 # relativePath that is added to baseUrl of an instance + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + applid: XBADAPPL # APPLID to generate PassTickets for this service + apiInfo: + - apiId: zowe.apiml.discoverableclient + gatewayUrl: api/v1 + version: 1.0.0 + + - serviceId: dcnopassticket # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Discoverable client with passTicket authentication scheme (Misconfiguration - no APPLID) # Title of the service in the API catalog + description: Define service to test passTicket authentication schema for integration tests. + instanceBaseUrls: # list of base URLs for each instance + - https://localhost:10012/discoverableclient # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: api/v1 # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: /api/v1 # relativePath that is added to baseUrl of an instance + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + apiInfo: + - apiId: zowe.apiml.discoverableclient + gatewayUrl: api/v1 + version: 1.0.0 + - serviceId: dcsafidt # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) title: Discoverable client with safIdt authentication scheme # Title of the service in the API catalog diff --git a/config/local/mock-services.yml b/config/local/mock-services.yml index 8a8a8bd073..dac99cdec0 100644 --- a/config/local/mock-services.yml +++ b/config/local/mock-services.yml @@ -1,2 +1,14 @@ zosmf: timeout: 1800 + +server: + ssl: + keyAlias: localhost + keyPassword: password + keyStore: keystore/localhost/localhost.keystore.p12 + keyStorePassword: password + keyStoreType: PKCS12 + trustStore: keystore/localhost/localhost.truststore.p12 + trustStorePassword: password + trustStoreType: PKCS12 + diff --git a/config/local/zaas-service.yml b/config/local/zaas-service.yml index e0742d851a..65fecdb1b2 100644 --- a/config/local/zaas-service.yml +++ b/config/local/zaas-service.yml @@ -47,12 +47,6 @@ spring: enabled: always server: - internal: - enabled: true - port: 10017 - ssl: - keyAlias: localhost-multi - keyStore: keystore/localhost/localhost-multi.keystore.p12 ssl: keyAlias: localhost keyPassword: password 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 6032b5334d..53ad09503c 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 @@ -173,13 +173,10 @@ private Mono> requestWithHa( ) { return requestCreator.apply(serviceInstanceIterator.next()) .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 -> Mono.empty(); - }) - .switchIfEmpty(serviceInstanceIterator.hasNext() ? - requestWithHa(serviceInstanceIterator, requestCreator) : Mono.empty() - ); + 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)); + }); } protected Mono invoke( @@ -192,7 +189,7 @@ protected Mono invoke( throw new ServiceNotAccessibleException("There are no instance of ZAAS available"); } - return requestWithHa(i, requestCreator).flatMap(responseProcessor); + return requestWithHa(i, requestCreator).switchIfEmpty(Mono.just(new AuthorizationResponse<>(null,null))).flatMap(responseProcessor); } /** diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticket.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticket.java index d4c49aac76..6e143fcb2d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticket.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticket.java @@ -10,6 +10,7 @@ package org.zowe.apiml.gateway.service.scheme; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.gateway.filter.FilterDefinition; @@ -18,6 +19,7 @@ import org.zowe.apiml.auth.Authentication; import org.zowe.apiml.auth.AuthenticationScheme; +@Slf4j @Component public class HttpBasicPassticket implements SchemeHandler { @@ -28,6 +30,11 @@ public AuthenticationScheme getAuthenticationScheme() { @Override public void apply(ServiceInstance serviceInstance, RouteDefinition routeDefinition, Authentication auth) { + if (StringUtils.isEmpty(auth.getApplid())) { + log.debug("Service {} does not have configured APPLID. The authorization scheme will be ignored", serviceInstance.getServiceId()); + return; + } + FilterDefinition filterDef = new FilterDefinition(); filterDef.setName("PassticketFilterFactory"); filterDef.addArg("applicationName", auth.getApplid()); 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 2c769b2ece..5bff86f9b8 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 @@ -10,11 +10,13 @@ package org.zowe.apiml.gateway.acceptance; +import lombok.AllArgsConstructor; +import lombok.Data; import org.apache.http.HttpHeaders; import org.hamcrest.Matchers; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTest; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; @@ -27,7 +29,9 @@ 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; @AcceptanceTest public class PassticketTest extends AcceptanceTestWithMockServices { @@ -38,8 +42,9 @@ 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"; - @BeforeEach - void setup() throws IOException { + + @Test + void whenRequestingPassticketForAllowedAPPLID_thenTranslate() throws IOException { TicketResponse response = new TicketResponse(); response.setToken(JWT); response.setUserId(USER_ID); @@ -48,34 +53,60 @@ void setup() throws IOException { mockService("zaas").scope(MockService.Scope.CLASS) .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"))) - .bodyJson(response) + .assertion(he -> assertEquals(SERVICE_ID, he.getRequestHeaders().getFirst("X-Service-Id"))) + .assertion(he -> assertEquals(COOKIE_NAME + "=" + JWT, he.getRequestHeaders().getFirst("Cookie"))) + .bodyJson(response) .and().start(); - } - @Nested - class GivenValidAuthentication { - - @Test - void whenRequestingPassticketForAllowedAPPLID_thenTranslate() throws IOException { - String expectedAuthHeader = "Basic " + Base64.getEncoder().encodeToString((USER_ID + ":" + PASSTICKET).getBytes(StandardCharsets.UTF_8)); - var mockService = mockService(SERVICE_ID) - .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid("IZUDFLT") - .addEndpoint("/" + SERVICE_ID + "/test") - .assertion(he -> assertEquals(expectedAuthHeader, he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) - .and().start(); + String expectedAuthHeader = "Basic " + Base64.getEncoder().encodeToString((USER_ID + ":" + PASSTICKET).getBytes(StandardCharsets.UTF_8)); + var mockService = mockService(SERVICE_ID) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid("IZUDFLT") + .addEndpoint("/" + SERVICE_ID + "/test") + .assertion(he -> assertEquals(expectedAuthHeader, he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) + .and().start(); - given() - .cookie(COOKIE_NAME, JWT) + given() + .cookie(COOKIE_NAME, JWT) .when() - .get(basePath + "/" + SERVICE_ID + "/api/v1/test") + .get(basePath + "/" + SERVICE_ID + "/api/v1/test") .then() - .statusCode(Matchers.is(SC_OK)); + .statusCode(Matchers.is(SC_OK)); + assertEquals(1, mockService.getEndpoint().getCounter()); + } - assertEquals(1, mockService.getEndpoint().getCounter()); - } + @ParameterizedTest + @ValueSource(ints = {400, 401, 403, 404, 405, 500}) + void whenCannotGeneratePassticket_thenIgnoreTransformation(int responseCode) throws IOException { + mockService("zaas").scope(MockService.Scope.TEST) + .addEndpoint("/zaas/scheme/ticket") + .responseCode(responseCode) + .and().start(); + var mockService = mockService(SERVICE_ID).scope(MockService.Scope.TEST) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET).applid("IZUDFLT") + .addEndpoint("/" + SERVICE_ID + "/test") + .responseCode(401) + .bodyJson(new ResponseDto("ok")) + .assertion(he -> assertFalse(he.getRequestHeaders().containsKey(HttpHeaders.AUTHORIZATION))) + .and().start(); + given() + .cookie(COOKIE_NAME, JWT) + .when() + .get(basePath + "/" + SERVICE_ID + "/api/v1/test") + .then() + .statusCode(Matchers.is(SC_UNAUTHORIZED)) + .body("status", Matchers.is("ok")); + assertEquals(1, mockService.getEndpoint().getCounter()); } + @Data + @AllArgsConstructor + static class ResponseDto { + + private String status; + + } } + + + diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticketTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticketTest.java index 4c711a6883..e85e53c6b7 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticketTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/scheme/HttpBasicPassticketTest.java @@ -18,6 +18,7 @@ import org.zowe.apiml.auth.AuthenticationScheme; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -44,4 +45,18 @@ void givenRouteDefinition_whenApply_thenFulfillFilterFactorArgs() { assertEquals("PassticketFilterFactory", filterDefinition.getName()); } + @Test + void givenNoApplid_whenApply_thenSkipConfiguration() { + RouteDefinition routeDefinition = new RouteDefinition(); + new HttpBasicPassticket().apply(mock(ServiceInstance.class), routeDefinition, new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, null)); + assertTrue(routeDefinition.getFilters().isEmpty()); + } + + @Test + void givenEmptyApplid_whenApply_thenSkipConfiguration() { + RouteDefinition routeDefinition = new RouteDefinition(); + new HttpBasicPassticket().apply(mock(ServiceInstance.class), routeDefinition, new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "")); + assertTrue(routeDefinition.getFilters().isEmpty()); + } + } diff --git a/gradle/jib.gradle b/gradle/jib.gradle index a9e5b7eb00..b28c29faf1 100644 --- a/gradle/jib.gradle +++ b/gradle/jib.gradle @@ -2,12 +2,12 @@ def setJib(componentName, javaAgentPort, debugPort) { def imageTag = project.hasProperty("zowe.docker.tag") ? project.getProperty("zowe.docker.tag"): "latest" def imageName = project.hasProperty("zowe.docker.container") ? "${project.getProperty("zowe.docker.container")}${componentName}:${imageTag}" : "ghcr.io/zowe/${componentName}:${imageTag}" def javaAgentOptions = project.hasProperty("zowe.docker.debug") ? ['-javaagent:/jacocoagent.jar=output=tcpserver,address=*,port=' + javaAgentPort, '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=' + debugPort]: ['-javaagent:/jacocoagent.jar=output=tcpserver,address=*,port=' + javaAgentPort] - def addOpensOptions = ['--add-opens=java.base/java.nio.channels.spi=ALL-UNNAMED', '--add-opens=java.base/java.util=ALL-UNNAMED', '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', '--add-opens=java.base/java.lang=ALL-UNNAMED', '--add-opens=java.base/java.lang.invoke=ALL-UNNAMED', '--add-opens=java.base/javax.net.ssl=ALL-UNNAMED'] + def addOpensOptions = ['--add-opens=java.base/java.nio.channels.spi=ALL-UNNAMED', '--add-opens=java.base/java.util=ALL-UNNAMED', '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', '--add-opens=java.base/java.lang=ALL-UNNAMED', '--add-opens=java.base/java.lang.invoke=ALL-UNNAMED', '--add-opens=java.base/javax.net.ssl=ALL-UNNAMED', '-Dspring.profiles.include=dev,debug'] jib.to.image = imageName jib.to.auth.username = project.hasProperty("zowe.docker.username") ? project.getProperty("zowe.docker.username") : "" jib.to.auth.password = project.hasProperty("zowe.docker.password") ? project.getProperty("zowe.docker.password") : "" - jib.container.args = ['--spring.config.additional-location=file:/docker/' + componentName + '.yml', '--spring.profiles.include=dev,debug'] + jib.container.args = ['--spring.config.additional-location=file:/docker/' + componentName + '.yml'] jib.container.jvmFlags = javaAgentOptions + addOpensOptions jib.extraDirectories.paths = ['../config', '../keystore', '../scripts'] } diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index c0089cd121..43bed0e7d8 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -436,6 +436,7 @@ task runZaasTest(type: Test) { description "Run Zaas tests only" outputs.cacheIf { false } + systemProperty "environment.offPlatform", true systemProperties System.getProperties() useJUnitPlatform { diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java index e4150dc94f..678523a687 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java @@ -13,12 +13,13 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.zowe.apiml.util.TestWithStartedInstances; import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; -import org.zowe.apiml.util.config.GatewayServiceConfiguration; import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.config.GatewayServiceConfiguration; import java.net.URI; import java.net.URISyntaxException; @@ -104,4 +105,9 @@ void testWrongRoutingWithBasePath(String basePath) throws URISyntaxException { given().get(new URI(scgUrl)).then().statusCode(404); } + @Test + void givenEndpointDoesNotExistOnRegisteredService() throws URISyntaxException { + String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), "/dcpassticket/api/v1/unknown"); + given().get(new URI(scgUrl)).then().statusCode(404); + } } 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 5f33e04d9a..7c9f9d142a 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 @@ -26,6 +26,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; 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.*; @@ -275,6 +276,26 @@ void givenCustomHeader() { } + @Nested + class PassticketMisconfiguration { + + @ParameterizedTest + @ValueSource(strings = { + "/dcpassticketxbadappl/api/v1/request", + "/dcnopassticket/api/v1/request" + }) + void givenJwt(String url) { + given() + .cookie(COOKIE_NAME, jwt) + .when() + .get(HttpRequestUtils.getUriFromGateway(url)) + .then() + .body("headers.authorization", Matchers.nullValue()) + .statusCode(200); + } + + } + } private , R extends ResponseBody & ResponseOptions> diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java index a7a9b365e6..c44dad7735 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java @@ -73,9 +73,9 @@ void givenRequestTimeoutIsReached_thenDropConnection() { assertTimeout(Duration.ofMillis(DEFAULT_TIMEOUT * 3), () -> { given() .header(HEADER_X_FORWARD_TO, "discoverableclient") - .when() + .when() .get(scgUrl) - .then() + .then() .statusCode(HttpStatus.SC_GATEWAY_TIMEOUT); }); } @@ -89,9 +89,9 @@ void givenRequestHeader_thenCertPassedToDomainGateway() { given() .config(SslContext.clientCertValid) .header(HEADER_X_FORWARD_TO, "apiml1") - .when() + .when() .get(scgUrl) - .then() + .then() .statusCode(HttpStatus.SC_OK) .body("dn", startsWith("CN=APIMTST")) .body("cn", is("APIMTST")) @@ -103,9 +103,9 @@ void givenBasePath_thenCertPassedToDomainGateway() { String scgUrl = String.format("%s://%s:%s/%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), "apiml1", X509_ENDPOINT); given() .config(SslContext.clientCertValid) - .when() + .when() .get(scgUrl) - .then() + .then() .statusCode(HttpStatus.SC_OK) .body("dn", startsWith("CN=APIMTST")) .body("cn", is("APIMTST")) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java index 6cd47ea8bd..6c719c78e3 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java @@ -19,7 +19,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.zowe.apiml.passticket.PassTicketService; import org.zowe.apiml.ticket.TicketRequest; -import org.zowe.apiml.util.TestWithStartedInstances; import org.zowe.apiml.util.categories.ZaasTest; import org.zowe.apiml.util.config.ConfigReader; @@ -33,9 +32,7 @@ import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.XML; -import static org.apache.http.HttpStatus.SC_BAD_REQUEST; -import static org.apache.http.HttpStatus.SC_NOT_FOUND; -import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.*; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.isEmptyOrNullString; @@ -43,19 +40,13 @@ import static org.hamcrest.core.IsNot.not; import static org.zowe.apiml.integration.zaas.ZaasTestUtil.COOKIE; import static org.zowe.apiml.integration.zaas.ZaasTestUtil.ZAAS_TICKET_URI; -import static org.zowe.apiml.util.SecurityUtils.USERNAME; -import static org.zowe.apiml.util.SecurityUtils.generateZoweJwtWithLtpa; -import static org.zowe.apiml.util.SecurityUtils.getConfiguredSslConfig; -import static org.zowe.apiml.util.SecurityUtils.getZosmfJwtTokenFromGw; -import static org.zowe.apiml.util.SecurityUtils.getZosmfLtpaToken; -import static org.zowe.apiml.util.SecurityUtils.personalAccessToken; -import static org.zowe.apiml.util.SecurityUtils.validOktaAccessToken; +import static org.zowe.apiml.util.SecurityUtils.*; /** * Verify integration of the API ML PassTicket support with the zOS provider of the PassTicket. */ @ZaasTest -class PassTicketTest implements TestWithStartedInstances { +class PassTicketTest { private final static String APPLICATION_NAME = ConfigReader.environmentConfiguration().getDiscoverableClientConfiguration().getApplId(); @@ -189,6 +180,23 @@ void givenNoApplicationName() { //@formatter:on } + @Test + void givenIncorrectHTTPMethod_thenReturnNotAllowed() { + String expectedMessage = "Authentication method 'GET' is not supported for URL '/zaas/scheme/ticket'"; + + //@formatter:off + given() + .contentType(JSON) + .body(new TicketRequest()) + .cookie(COOKIE, jwt) + .when() + .get(ZAAS_TICKET_URI) + .then() + .statusCode(is(SC_METHOD_NOT_ALLOWED)) + .body("messages.find { it.messageNumber == 'ZWEAG101E' }.messageContent", equalTo(expectedMessage)); + //@formatter:on + } + @Test void givenInvalidApplicationName() { String expectedMessage = "The generation of the PassTicket failed. Reason:"; @@ -202,7 +210,7 @@ void givenInvalidApplicationName() { .when() .post(ZAAS_TICKET_URI) .then() - .statusCode(is(SC_BAD_REQUEST)) + .statusCode(is(SC_INTERNAL_SERVER_ERROR)) .body("messages.find { it.messageNumber == 'ZWEAG141E' }.messageContent", containsString(expectedMessage)); //@formatter:on } @@ -225,13 +233,14 @@ void givenLongApplicationName() { @Test void givenNoContentType() { //@formatter:off - given() - .body(new TicketRequest(APPLICATION_NAME)) + given() + .body(new TicketRequest(APPLICATION_NAME).toString().getBytes()) .cookie(COOKIE, jwt) - .when() + .noContentType() + .when() .post(ZAAS_TICKET_URI) - .then() - .statusCode(is(SC_NOT_FOUND)); + .then() + .statusCode(is(SC_BAD_REQUEST)); //@formatter:on } @@ -245,7 +254,7 @@ void givenInvalidContentType() { .when() .post(ZAAS_TICKET_URI) .then() - .statusCode(is(SC_NOT_FOUND)); + .statusCode(is(SC_UNSUPPORTED_MEDIA_TYPE)); //@formatter:on } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java index 889e2b851a..a86bb4226c 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java @@ -184,7 +184,7 @@ void givenInvalidApplicationName() { .when() .post(ZAAS_SAFIDT_URI) .then() - .statusCode(is(SC_BAD_REQUEST)) + .statusCode(is(SC_INTERNAL_SERVER_ERROR)) .body("messages.find { it.messageNumber == 'ZWEAG141E' }.messageContent", containsString(expectedMessage)); //@formatter:on } @@ -211,19 +211,6 @@ class WhenGeneratingSafIdToken_returnNotFound { private final String jwt = getZosmfJwtToken(); - @Test - void givenNoContentType() { - //@formatter:off - given() - .body(new TicketRequest(APPLICATION_NAME)) - .cookie(COOKIE, jwt) - .when() - .post(ZAAS_SAFIDT_URI) - .then() - .statusCode(is(SC_NOT_FOUND)); - //@formatter:on - } - @Test void givenInvalidContentType() { //@formatter:off @@ -234,7 +221,7 @@ void givenInvalidContentType() { .when() .post(ZAAS_SAFIDT_URI) .then() - .statusCode(is(SC_NOT_FOUND)); + .statusCode(is(SC_UNSUPPORTED_MEDIA_TYPE)); //@formatter:on } @@ -243,10 +230,11 @@ void givenNoBody() { //@formatter:off given() .cookie(COOKIE, jwt) + .noContentType() .when() .post(ZAAS_SAFIDT_URI) .then() - .statusCode(is(SC_NOT_FOUND)); + .statusCode(is(SC_BAD_REQUEST)); //@formatter:on } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/ErrorUtils.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/ErrorUtils.java index dde138b87d..bb5d60639c 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/ErrorUtils.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/ErrorUtils.java @@ -17,8 +17,8 @@ public class ErrorUtils { static final String UNEXPECTED_ERROR_OCCURRED = "Unexpected error occurred"; - static final String ATTR_ERROR_STATUS_CODE = "javax.servlet.error.status_code"; - public static final String ATTR_ERROR_EXCEPTION = "javax.servlet.error.exception"; + static final String ATTR_ERROR_STATUS_CODE = "jakarta.servlet.error.status_code"; + public static final String ATTR_ERROR_EXCEPTION = "jakarta.servlet.error.exception"; private ErrorUtils() {} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/check/SafEndpointCheck.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/check/SafEndpointCheck.java deleted file mode 100644 index 94e1daae57..0000000000 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/check/SafEndpointCheck.java +++ /dev/null @@ -1,62 +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.zaas.error.check; - -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.zowe.apiml.message.api.ApiMessageView; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.security.common.auth.saf.EndpointImproprietyConfigureException; -import org.zowe.apiml.security.common.auth.saf.UnsupportedResourceClassException; - -import jakarta.servlet.http.HttpServletRequest; - -@RequiredArgsConstructor -public class SafEndpointCheck implements ErrorCheck { - - private final MessageService messageService; - - private ResponseEntity createResponse(EndpointImproprietyConfigureException eice) { - ApiMessageView apiMessage = messageService.createMessage( - "org.zowe.apiml.security.common.auth.saf.endpoint.endpointImproprietyConfigure", - eice.getEndpoint() - ).mapToView(); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiMessage); - } - - private ResponseEntity createResponse(UnsupportedResourceClassException urce) { - ApiMessageView apiMessage = messageService.createMessage( - "org.zowe.apiml.security.common.auth.saf.endpoint.nonZoweClass", - urce.getResourceClass() - ).mapToView(); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiMessage); - } - - @Override - public ResponseEntity checkError(HttpServletRequest request, Throwable exc) { - int exceptionIndex; - - exceptionIndex = ExceptionUtils.indexOfType(exc, EndpointImproprietyConfigureException.class); - if (exceptionIndex != -1) { - return createResponse((EndpointImproprietyConfigureException) ExceptionUtils.getThrowables(exc)[exceptionIndex]); - } - - exceptionIndex = ExceptionUtils.indexOfType(exc, UnsupportedResourceClassException.class); - if (exceptionIndex != -1) { - return createResponse((UnsupportedResourceClassException) ExceptionUtils.getThrowables(exc)[exceptionIndex]); - } - - return null; - } - -} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/InternalServerErrorController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/InternalServerErrorController.java deleted file mode 100644 index 0568bfd3e9..0000000000 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/InternalServerErrorController.java +++ /dev/null @@ -1,96 +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.zaas.error.controllers; - -import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Primary; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.zowe.apiml.message.api.ApiMessageView; -import org.zowe.apiml.message.core.Message; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.product.compatibility.ApimlErrorController; -import org.zowe.apiml.zaas.error.ErrorUtils; -import org.zowe.apiml.zaas.error.check.ErrorCheck; -import org.zowe.apiml.zaas.error.check.SafEndpointCheck; - -import java.util.ArrayList; -import java.util.List; - -/** - * Handles errors in REST API processing. - */ -@RestController -@Order(Ordered.HIGHEST_PRECEDENCE) -@Primary -public class InternalServerErrorController implements ApimlErrorController { - public static final String ERROR_ENDPOINT = "/internal_error"; - - private final MessageService messageService; - private final List errorChecks = new ArrayList<>(); - - @Autowired - public InternalServerErrorController(MessageService messageService) { - this.messageService = messageService; - - errorChecks.add(new SafEndpointCheck(messageService)); - } - - @Override - public String getErrorPath() { - return ERROR_ENDPOINT; - } - - /** - * Error endpoint controller - * Creates response and logs the error - * - * @param request Http request - * @return Http response entity - */ - @SuppressWarnings("squid:S3752") - @RequestMapping(value = ERROR_ENDPOINT, produces = "application/json") - public ResponseEntity error(HttpServletRequest request) { - final Throwable exc = (Throwable) request.getAttribute(ErrorUtils.ATTR_ERROR_EXCEPTION); - - ResponseEntity entity = checkForSpecificErrors(request, exc); - if (entity != null) { - return entity; - } - - return createResponseForInternalError(request, exc); - } - - private ResponseEntity createResponseForInternalError(HttpServletRequest request, Throwable exc) { - final int status = ErrorUtils.getErrorStatus(request); - Message message = messageService.createMessage("org.zowe.apiml.common.internalRequestError", - ErrorUtils.getForwardUri(request), - ExceptionUtils.getMessage(exc), - ExceptionUtils.getRootCauseMessage(exc)); - - return ResponseEntity.status(status).body(message.mapToView()); - } - - private ResponseEntity checkForSpecificErrors(HttpServletRequest request, Throwable exc) { - for (ErrorCheck check : errorChecks) { - ResponseEntity entity = check.checkError(request, exc); - if (entity != null) { - return entity; - } - } - return null; - } -} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/NotFoundErrorController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/NotFoundErrorController.java deleted file mode 100644 index 63919e86d4..0000000000 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/NotFoundErrorController.java +++ /dev/null @@ -1,55 +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.zaas.error.controllers; - - -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.zowe.apiml.message.api.ApiMessageView; -import org.zowe.apiml.message.core.Message; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.product.compatibility.ApimlErrorController; -import org.zowe.apiml.zaas.error.ErrorUtils; - -/** - * Not found endpoint controller - */ -@RestController -@RequiredArgsConstructor -@Order(Ordered.HIGHEST_PRECEDENCE) -public class NotFoundErrorController implements ApimlErrorController { - private static final String NOT_FOUND_ENDPOINT = "/not_found"; - private final MessageService messageService; - - @Override - public String getErrorPath() { - return NOT_FOUND_ENDPOINT; - } - - /** - * Not found endpoint controller - * Creates response and logs the error - * - * @param request Http request - * @return Http response entity - */ - @GetMapping(value = NOT_FOUND_ENDPOINT, produces = "application/json") - public ResponseEntity notFound400HttpResponse(HttpServletRequest request) { - Message message = messageService.createMessage("org.zowe.apiml.common.endPointNotFound", - ErrorUtils.getForwardUri(request)); - return ResponseEntity.status(ErrorUtils.getErrorStatus(request)).body(message.mapToView()); - } -} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/ZaasErrorController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/ZaasErrorController.java new file mode 100644 index 0000000000..987094c729 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/controllers/ZaasErrorController.java @@ -0,0 +1,112 @@ +/* + * 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.zaas.error.controllers; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.context.annotation.Primary; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zowe.apiml.message.api.ApiMessageView; +import org.zowe.apiml.message.core.Message; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.zaas.error.ErrorUtils; + +import java.util.Optional; + +import static org.apache.hc.core5.http.HttpStatus.*; + +/** + * Handles errors in REST API processing. + */ +@RestController +@Order(Ordered.HIGHEST_PRECEDENCE) +@Primary +@RequiredArgsConstructor +public class ZaasErrorController implements ErrorController { + + private static final String ERROR_ENDPOINT = "/error"; + private static final String NOT_FOUND_ENDPOINT = "/not_found"; + public static final String INTERNAL_ERROR_ENDPOINT = "/internal_error"; + + private final MessageService messageService; + + private Message getMessageByStatus(HttpServletRequest request, int status) { + switch (status) { + case SC_BAD_REQUEST: + return messageService.createMessage("org.zowe.apiml.common.badRequest"); + case SC_NOT_FOUND: + return messageService.createMessage("org.zowe.apiml.common.endPointNotFound", ErrorUtils.getForwardUri(request)); + case SC_INTERNAL_SERVER_ERROR: + final Throwable exc = (Throwable) request.getAttribute(ErrorUtils.ATTR_ERROR_EXCEPTION); + return messageService.createMessage("org.zowe.apiml.common.internalRequestError", + ErrorUtils.getForwardUri(request), + ExceptionUtils.getMessage(exc), + ExceptionUtils.getRootCauseMessage(exc)); + default: + return getMessageByStatus(request, SC_INTERNAL_SERVER_ERROR); + } + } + + private ApiMessageView getBodyByStatus(HttpServletRequest request, int status) { + var message = getMessageByStatus(request, status); + return message == null ? null : message.mapToView(); + } + + /** + * Not found endpoint controller + * Creates response and logs the error + * + * @param request Http request + * @return Http response entity + */ + @GetMapping(value = NOT_FOUND_ENDPOINT, produces = "application/json") + public ResponseEntity notFound404HttpResponse(HttpServletRequest request) { + return ResponseEntity.status(SC_NOT_FOUND).body(getBodyByStatus(request, SC_NOT_FOUND)); + } + /** + * Error endpoint controller + * Creates response and logs the error + * + * @param request Http request + * @return Http response entity + */ + @SuppressWarnings("squid:S3752") + @RequestMapping(value = INTERNAL_ERROR_ENDPOINT, produces = "application/json") + public ResponseEntity internalError(HttpServletRequest request) { + return ResponseEntity.status(SC_INTERNAL_SERVER_ERROR).body(getBodyByStatus(request, SC_INTERNAL_SERVER_ERROR)); + } + + private int getStatus(HttpServletRequest request) { + return Optional + .ofNullable((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)) + .orElse(SC_INTERNAL_SERVER_ERROR); + } + + @SuppressWarnings("squid:S3752") + @RequestMapping(value = ERROR_ENDPOINT, produces = "application/json") + public ResponseEntity error(HttpServletRequest request) { + int status = getStatus(request); + if (status == SC_NO_CONTENT) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return ResponseEntity.status(status).body(getBodyByStatus(request, status)); + } + +} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java index c3bce5def3..791be2595a 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java @@ -57,7 +57,6 @@ import org.zowe.apiml.security.common.verify.CertificateValidator; import org.zowe.apiml.zaas.controllers.AuthController; import org.zowe.apiml.zaas.controllers.SafResourceAccessController; -import org.zowe.apiml.zaas.error.controllers.InternalServerErrorController; import org.zowe.apiml.zaas.security.login.x509.X509AuthenticationProvider; import org.zowe.apiml.zaas.security.query.QueryFilter; import org.zowe.apiml.zaas.security.query.SuccessfulQueryHandler; @@ -575,8 +574,8 @@ private BearerContentFilter bearerContentFilter(AuthenticationManager authentica private CategorizeCertsFilter reversedCategorizeCertFilter() { CategorizeCertsFilter out = new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator); - out.setCertificateForClientAuth(crt -> out.getPublicKeyCertificatesBase64().contains(out.base64EncodePublicKey(crt))); - out.setApimlCertificate(crt -> !out.getPublicKeyCertificatesBase64().contains(out.base64EncodePublicKey(crt))); + out.setCertificateForClientAuth(crt -> out.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); + out.setApimlCertificate(crt -> !out.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); return out; } } @@ -602,7 +601,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { // Endpoints that skip Spring Security completely // There is no CORS filter on these endpoints. If you require CORS processing, use a defined filter chain web.ignoring() - .requestMatchers(InternalServerErrorController.ERROR_ENDPOINT, "/error", + .requestMatchers("/error", "/application/info", "/application/version", AuthController.CONTROLLER_PATH + AuthController.ALL_PUBLIC_KEYS_PATH, AuthController.CONTROLLER_PATH + AuthController.CURRENT_PUBLIC_KEYS_PATH); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/zaas/ZaasExceptionHandler.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/zaas/ZaasExceptionHandler.java index a457beba4b..b49a454348 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/zaas/ZaasExceptionHandler.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/zaas/ZaasExceptionHandler.java @@ -10,35 +10,55 @@ package org.zowe.apiml.zaas.zaas; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.zowe.apiml.zaas.security.service.saf.SafIdtAuthException; -import org.zowe.apiml.zaas.security.service.saf.SafIdtException; -import org.zowe.apiml.zaas.security.service.schema.source.AuthSchemeException; -import org.zowe.apiml.zaas.security.ticket.ApplicationNameNotFoundException; +import org.springframework.web.servlet.NoHandlerFoundException; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; +import org.zowe.apiml.security.common.auth.saf.EndpointImproperlyConfigureException; +import org.zowe.apiml.security.common.auth.saf.UnsupportedResourceClassException; import org.zowe.apiml.security.common.token.TokenExpireException; import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.zaas.security.service.saf.SafIdtAuthException; +import org.zowe.apiml.zaas.security.service.saf.SafIdtException; +import org.zowe.apiml.zaas.security.service.schema.source.AuthSchemeException; +import org.zowe.apiml.zaas.security.ticket.ApplicationNameNotFoundException; import javax.management.ServiceNotFoundException; +import javax.net.ssl.SSLException; +@Slf4j @ControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) @RequiredArgsConstructor public class ZaasExceptionHandler { + + private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + private static final String BASIC_REALM = "Basic realm=\"Realm\""; + private final MessageService messageService; @ExceptionHandler(value = {IRRPassTicketGenerationException.class}) public ResponseEntity handlePassTicketException(IRRPassTicketGenerationException ex) { + log.error(ex.getMessage()); ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.generateFailed", ex.getErrorCode().getMessage()).mapToView(); return ResponseEntity - .status(ex.getHttpStatus()) + .status(HttpStatus.INTERNAL_SERVER_ERROR) .contentType(MediaType.APPLICATION_JSON) .body(messageView); } @@ -52,8 +72,8 @@ public ResponseEntity handleSafIdtExceptions(RuntimeException ex .body(messageView); } - @ExceptionHandler(value = {ApplicationNameNotFoundException.class}) - public ResponseEntity handleApplIdNotFoundException(ApplicationNameNotFoundException ex) { + @ExceptionHandler(value = {ApplicationNameNotFoundException.class, HttpMessageNotReadableException.class}) + public ResponseEntity handleApplIdNotFoundException() { ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.invalidApplicationName").mapToView(); return ResponseEntity .status(HttpStatus.BAD_REQUEST) @@ -80,7 +100,7 @@ public ResponseEntity handleZoweJwtCreationErrors(IllegalStateEx } @ExceptionHandler(value = {TokenNotValidException.class, AuthSchemeException.class}) - public ResponseEntity handleTokenNotValidException(RuntimeException ex) { + public ResponseEntity handleTokenNotValidException() { ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.zaas.security.invalidToken").mapToView(); return ResponseEntity .status(HttpStatus.UNAUTHORIZED) @@ -89,11 +109,104 @@ public ResponseEntity handleTokenNotValidException(RuntimeExcept } @ExceptionHandler(value = {TokenExpireException.class}) - public ResponseEntity handleTokenExpiredException(TokenExpireException ex) { + public ResponseEntity handleTokenExpiredException() { ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.zaas.security.expiredToken").mapToView(); return ResponseEntity .status(HttpStatus.UNAUTHORIZED) .contentType(MediaType.APPLICATION_JSON) .body(messageView); } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(HttpServletRequest request, AccessDeniedException accessDeniedException) { + log.debug("Unauthenticated access", accessDeniedException); + log.debug("URL: {}", request.getRequestURL()); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.forbidden", request.getRequestURI()).mapToView(); + return ResponseEntity + .status(HttpStatus.FORBIDDEN) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity handleNoResourceFoundException(NoHandlerFoundException e) { + log.debug("Resource not found", e); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.common.notFound").mapToView(); + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handleMethodNotAllowedException(HttpServletRequest request, HttpRequestMethodNotSupportedException notAllowedMethodException) { + log.debug("MethodNotAllowedException exception", notAllowedMethodException); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.invalidMethod", request.getMethod(), request.getRequestURI()).mapToView(); + return ResponseEntity + .status(HttpStatus.METHOD_NOT_ALLOWED) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + @ExceptionHandler(SSLException.class) + public ResponseEntity handleSslException(HttpServletRequest request, SSLException sslException) { + log.debug("SSL exception", sslException); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.common.tlsError", request.getRequestURI(), sslException.getMessage()).mapToView(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + + @ExceptionHandler(UnsupportedResourceClassException.class) + public ResponseEntity handleUnsupportedResourceClassException(UnsupportedResourceClassException unsupportedResourceClassException) { + log.debug("Unsupported resource class", unsupportedResourceClassException); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.common.auth.saf.endpoint.nonZoweClass", unsupportedResourceClassException.getResourceClass()).mapToView(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + @ExceptionHandler(EndpointImproperlyConfigureException.class) + public ResponseEntity handleendpointImproperlyConfigureException(EndpointImproperlyConfigureException improprietyConfigureException) { + log.debug("Endpoint is improperly configured", improprietyConfigureException); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.common.auth.saf.endpoint.endpointImproperlyConfigure", improprietyConfigureException.getEndpoint()).mapToView(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleInternalException(Exception exception) { + log.debug("Unexpected internal error", exception); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.common.internalServerError").mapToView(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + @ExceptionHandler({IllegalArgumentException.class, MissingServletRequestParameterException.class}) + public ResponseEntity handleInternalException(IllegalArgumentException exception) { + log.debug("Client sent illegal arguments", exception); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.common.badRequest").mapToView(); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResponseEntity handleUnsupportedMediaException(HttpMediaTypeNotSupportedException exception) { + log.debug("Requested media type is not supported", exception); + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.common.unsupportedMediaType").mapToView(); + return ResponseEntity + .status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + .contentType(MediaType.APPLICATION_JSON) + .body(messageView); + } + } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java index 98add276da..632c9e2089 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java @@ -30,16 +30,16 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.message.yaml.YamlMessageService; +import org.zowe.apiml.security.common.token.AccessTokenProvider; +import org.zowe.apiml.security.common.token.TokenAuthentication; import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.JwtSecurity; import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import org.zowe.apiml.zaas.security.webfinger.WebFingerProvider; import org.zowe.apiml.zaas.security.webfinger.WebFingerResponse; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; -import org.zowe.apiml.security.common.token.AccessTokenProvider; -import org.zowe.apiml.security.common.token.TokenAuthentication; import java.io.IOException; import java.text.ParseException; @@ -48,25 +48,12 @@ import java.util.List; import java.util.Optional; -import static org.apache.http.HttpStatus.SC_BAD_REQUEST; -import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; -import static org.apache.http.HttpStatus.SC_NOT_FOUND; -import static org.apache.http.HttpStatus.SC_NO_CONTENT; -import static org.apache.http.HttpStatus.SC_OK; -import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; -import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.http.HttpStatus.*; import static org.hamcrest.CoreMatchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; -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.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @ExtendWith(SpringExtension.class) class AuthControllerTest { diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/error/InternalServerErrorControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/error/ZaasErrorControllerTest.java similarity index 82% rename from zaas-service/src/test/java/org/zowe/apiml/zaas/error/InternalServerErrorControllerTest.java rename to zaas-service/src/test/java/org/zowe/apiml/zaas/error/ZaasErrorControllerTest.java index 36c5712820..a3c36fb41a 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/error/InternalServerErrorControllerTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/error/ZaasErrorControllerTest.java @@ -11,24 +11,24 @@ package org.zowe.apiml.zaas.error; +import jakarta.servlet.RequestDispatcher; import org.junit.jupiter.api.Test; -import org.zowe.apiml.zaas.error.controllers.InternalServerErrorController; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; -import org.springframework.http.ResponseEntity; -import org.springframework.mock.web.MockHttpServletRequest; +import org.zowe.apiml.zaas.error.controllers.ZaasErrorController; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import jakarta.servlet.RequestDispatcher; +class ZaasErrorControllerTest { -class InternalServerErrorControllerTest { @Test void testGenericError() { MessageService messageService = new YamlMessageService(); - InternalServerErrorController errorController = new InternalServerErrorController(messageService); + ZaasErrorController errorController = new ZaasErrorController(messageService); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -38,9 +38,10 @@ void testGenericError() { ResponseEntity response = errorController.error(request); - assertEquals(523, response.getStatusCodeValue()); - assertEquals("org.zowe.apiml.common.internalRequestError", response.getBody().getMessages().get(0).getMessageKey()); + assertEquals("org.zowe.apiml.common.internalRequestError", response.getBody().getMessages().get(0).getMessageKey()); + assertEquals(523, response.getStatusCode().value()); assertTrue(response.getBody().getMessages().get(0).getMessageContent().contains("Hello")); assertTrue(response.getBody().getMessages().get(0).getMessageContent().contains("/uri")); } + } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/error/check/SafEndpointCheckTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/error/check/SafEndpointCheckTest.java deleted file mode 100644 index f9e6b91ef9..0000000000 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/error/check/SafEndpointCheckTest.java +++ /dev/null @@ -1,84 +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.zaas.error.check; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.web.util.NestedServletException; -import org.zowe.apiml.message.api.ApiMessage; -import org.zowe.apiml.message.api.ApiMessageView; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; -import org.zowe.apiml.security.common.auth.saf.EndpointImproprietyConfigureException; -import org.zowe.apiml.security.common.auth.saf.UnsupportedResourceClassException; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class SafEndpointCheckTest { - - private static final String ENDPOINT_URL = "http://endpoint/url"; - private static final String UNEXPECTED_RESOURCE_CLASS = "UnexpectedResourceClass"; - - private SafEndpointCheck safEndpointCheck; - - @BeforeAll - void setUp() { - MessageService messageService = new YamlMessageService("/security-common-log-messages.yml"); - safEndpointCheck = new SafEndpointCheck(messageService); - } - - private ApiMessage getApiMessage(ResponseEntity response) { - assertNotNull(response); - assertNotNull(response.getBody()); - List messages = response.getBody().getMessages(); - assertEquals(1, messages.size()); - return messages.get(0); - } - - @Test - void givenCoveredEndpointImproprietyConfigureException_whenCheck_thenReturnMessage() { - EndpointImproprietyConfigureException eice = new EndpointImproprietyConfigureException("An error", ENDPOINT_URL); - NestedServletException coveredException = new NestedServletException("msg", eice); - ResponseEntity response = safEndpointCheck.checkError(new MockHttpServletRequest(), coveredException); - - ApiMessage message = getApiMessage(response); - assertEquals("org.zowe.apiml.security.common.auth.saf.endpoint.endpointImproprietyConfigure", message.getMessageKey()); - assertEquals("ZWEAT603E", message.getMessageNumber()); - assertTrue(message.getMessageContent().contains(ENDPOINT_URL)); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); - } - - @Test - void givenCoveredUnsupportedResourceClassException_whenCheck_thenReturnMessage() { - UnsupportedResourceClassException urce = new UnsupportedResourceClassException(UNEXPECTED_RESOURCE_CLASS, "message"); - NestedServletException coveredException = new NestedServletException("msg", urce); - ResponseEntity response = safEndpointCheck.checkError(new MockHttpServletRequest(), coveredException); - - ApiMessage message = getApiMessage(response); - assertEquals("org.zowe.apiml.security.common.auth.saf.endpoint.nonZoweClass", message.getMessageKey()); - assertEquals("ZWEAT602E", message.getMessageNumber()); - assertTrue(message.getMessageContent().contains(UNEXPECTED_RESOURCE_CLASS)); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); - } - - @Test - void givenNonEndpointException_whenCheck_thenReturnNull() { - assertNull(safEndpointCheck.checkError(new MockHttpServletRequest(), new RuntimeException())); - } - -} \ No newline at end of file diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/error/controllers/NotFoundErrorControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/error/controllers/NotFoundErrorControllerTest.java deleted file mode 100644 index f5cd0d6700..0000000000 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/error/controllers/NotFoundErrorControllerTest.java +++ /dev/null @@ -1,63 +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.zaas.error.controllers; - -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.MockitoAnnotations; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.zowe.apiml.message.core.Message; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.core.MessageType; -import org.zowe.apiml.message.template.MessageTemplate; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(SpringExtension.class) -class NotFoundErrorControllerTest { - - private MockMvc mockMvc; - - @BeforeEach - void init() { - MessageTemplate messageTemplate = new MessageTemplate("org.zowe.apiml.common.endPointNotFound", "number", MessageType.ERROR, "text"); - Message message = Message.of("org.zowe.apiml.common.endPointNotFound", messageTemplate, new Object[0]); - MessageService messageService = mock(MessageService.class); - - when(messageService.createMessage(anyString(), (Object[]) any())).thenReturn(message); - - NotFoundErrorController notFoundErrorController = new NotFoundErrorController(messageService); - MockitoAnnotations.openMocks(this); - - mockMvc = MockMvcBuilders - .standaloneSetup(notFoundErrorController) - .build(); - } - - @Nested - class GivenNotFoundErrorRequest { - @Test - void whenCallingWithRequestAttribute_thenReturnProperErrorStatus() throws Exception { - mockMvc.perform(get("/not_found").requestAttr("javax.servlet.error.status_code", 404)) - .andExpect(status().isNotFound()); - } - } - -} diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/zaas/SchemeControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/zaas/SchemeControllerTest.java index 8c8232d203..07e5746b5f 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/zaas/SchemeControllerTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/zaas/SchemeControllerTest.java @@ -22,15 +22,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.zowe.apiml.constants.ApimlConstants; -import org.zowe.apiml.zaas.security.service.TokenCreationService; -import org.zowe.apiml.zaas.security.service.saf.SafIdtException; -import org.zowe.apiml.zaas.security.service.schema.source.AuthSchemeException; -import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; -import org.zowe.apiml.zaas.security.service.schema.source.AuthSourceService; -import org.zowe.apiml.zaas.security.service.schema.source.JwtAuthSource; -import org.zowe.apiml.zaas.security.service.schema.source.OIDCAuthSource; -import org.zowe.apiml.zaas.security.service.schema.source.ParsedTokenAuthSource; -import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; @@ -39,20 +30,21 @@ import org.zowe.apiml.security.common.token.TokenExpireException; import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.zaas.ZaasTokenResponse; +import org.zowe.apiml.zaas.security.service.TokenCreationService; +import org.zowe.apiml.zaas.security.service.saf.SafIdtException; +import org.zowe.apiml.zaas.security.service.schema.source.*; +import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import javax.management.ServiceNotFoundException; import java.util.Date; -import static org.apache.http.HttpStatus.SC_BAD_REQUEST; -import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; -import static org.apache.http.HttpStatus.SC_OK; -import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; -import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.http.HttpStatus.*; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasSize; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -159,6 +151,58 @@ void whenRequestPassticketAndNoApplNameProvided_thenBadRequest() throws Exceptio .andExpect(jsonPath("$.messages[0].messageContent", is("The 'applicationName' parameter name is missing."))); } + @Test + void givenIncorrectMethod_whenRequestPassticket_thenBadRequest() throws Exception { + ticketBody.put("applicationName", ""); + + mockMvc.perform(get(PASSTICKET_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(ticketBody.toString()) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_METHOD_NOT_ALLOWED)) + .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAG101E")) + .andExpect(jsonPath("$.messages[0].messageContent", is("Authentication method 'GET' is not supported for URL '/zaas/scheme/ticket'"))); + } + + @Test + void givenIncorrectMediaType_whenRequestPassticket_thenUnsupportedMedia() throws Exception { + ticketBody.put("applicationName", ""); + + mockMvc.perform(post(PASSTICKET_URL) + .contentType(MediaType.TEXT_XML) + .content(ticketBody.toString()) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_UNSUPPORTED_MEDIA_TYPE)) + .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAO415E")) + .andExpect(jsonPath("$.messages[0].messageContent", is("The media format of the requested data is not supported by the service, so the service has rejected the request."))); + } + + @Test + void givenInvalidPath_whenRequestPassticket_thenNotFound() throws Exception { + ticketBody.put("applicationName", ""); + + mockMvc.perform(post("/unknown/url") + .contentType(MediaType.TEXT_XML) + .content(ticketBody.toString()) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_NOT_FOUND)) + .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAO404E")) + .andExpect(jsonPath("$.messages[0].messageContent", is("The service can not find the requested resource."))); + } + + @Test + void givenMissingRequestAttribute_whenRequestPassticket_thenInternalError() throws Exception { + ticketBody.put("applicationName", ""); + + mockMvc.perform(post(PASSTICKET_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(ticketBody.toString().getBytes())) + .andExpect(status().is(SC_INTERNAL_SERVER_ERROR)) + .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAO500E")) + .andExpect(jsonPath("$.messages[0].messageContent", is("The service has encountered a situation it doesn't know how to handle. Please contact support for further assistance. More details are available in the log under the provided message instance ID"))); + + } + @Test void whenRequestSafIdtAndApplNameProvided_thenResponseOk() throws Exception { when(tokenCreationService.createSafIdTokenWithoutCredentials(USER, APPLID)).thenReturn(SAFIDT); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/zaas/ZaasExceptionHandlerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/zaas/ZaasExceptionHandlerTest.java new file mode 100644 index 0000000000..fbb3ab57c1 --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/zaas/ZaasExceptionHandlerTest.java @@ -0,0 +1,160 @@ +/* + * 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.zaas.zaas; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zowe.apiml.security.common.auth.saf.EndpointImproperlyConfigureException; +import org.zowe.apiml.security.common.auth.saf.UnsupportedResourceClassException; + +import javax.net.ssl.SSLException; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "apiml.security.filterChainConfiguration=new", + "apiml.health.protected=true" + } +) +class ZaasExceptionHandlerTest { + + @LocalServerPort + private int port; + + @BeforeEach + void init() { + RestAssured.baseURI = "https://localhost"; + RestAssured.port = port; + RestAssured.useRelaxedHTTPSValidation(); + } + + @Test + void givenUnknownEndpoint_whenCallZaas_thenReturns404WithMessage() { + given().when() + .get("/unknown/endpoint") + .then() + .statusCode(404) + .body("messages[0].messageKey", is("org.zowe.apiml.common.endPointNotFound")); + } + + @ParameterizedTest + @ValueSource(strings = { + "/zaas/scheme/ticket", + "/application/health" + }) + void givenNoCredentials_whenCallZaas_thenReturns401WithMessage(String url) { + given().when() + .get(url) + .then() + .statusCode(401) + .body("messages[0].messageKey", is("org.zowe.apiml.security.authRequired")); + } + + @ParameterizedTest + @ValueSource(strings = { + "/zaas/api/v1/auth/login" + }) + void givenNoCredentials_whenLogin_thenReturns401WithMessage(String url) { + given().when() + .auth().preemptive().basic("UNKNOWN", "WRONG") + .post(url) + .then() + .statusCode(401) + .body("messages[0].messageKey", is("org.zowe.apiml.security.login.invalidCredentials")); + } + + @ParameterizedTest + @ValueSource(strings = { + "/zaas/api/v1/auth/oidc/webfinger" // missing ?resource=abc + }) + void givenNoRequiredArgument_whenCallZaas_thenReturns400WithMessage(String url) { + given().when() + .get(url) + .then() + .statusCode(400) + .body("messages[0].messageKey", is("org.zowe.apiml.common.badRequest")); + } + + @Test + void givenNonAuthorizedCredentials_whenCallZaas_thenReturns403WithMessage() { + given().when() + .get("/test/forbidden") + .then() + .statusCode(403) + .body("messages[0].messageKey", is("org.zowe.apiml.security.forbidden")); + } + + @Test + void givenZosmfSslMisconfiguration_whenCallZaas_thenReturns500WithMessage() { + given().when() + .get("/test/sslException") + .then() + .statusCode(500) + .body("messages[0].messageKey", is("org.zowe.apiml.common.tlsError")); + } + + @Test + void givenUnsupportedResourceClass_whenCallZaas_thenReturns500WithMessage() { + given().when() + .get("/test/unsupportedResourceClassException") + .then() + .statusCode(500) + .body("messages[0].messageKey", is("org.zowe.apiml.security.common.auth.saf.endpoint.nonZoweClass")); + } + + @Test + void givenMisconfiguredEndpoint_whenCallZaas_thenReturns500WithMessage() { + given().when() + .get("/test/endpointImproperlyConfigureException") + .then() + .statusCode(500) + .body("messages[0].messageKey", is("org.zowe.apiml.security.common.auth.saf.endpoint.endpointImproperlyConfigure")); + } + + @RestController + @RequestMapping("/test") + static class TestController { + + @PreAuthorize("false") + @GetMapping("/forbidden") + public void forbidden() { + } + + @GetMapping("/sslException") + public void sslException() throws SSLException { + throw new SSLException("Any SSL error during calling z/OSMF"); + } + + @GetMapping("/unsupportedResourceClassException") + public void unsupportedResourceClassException() { + throw new UnsupportedResourceClassException("unknownResourceClass", "non-ZOWE resource class"); + } + + @GetMapping("/endpointImproperlyConfigureException") + public void endpointImproperlyConfigureException() { + throw new EndpointImproperlyConfigureException("misconfigured", "endpoint"); + } + + } + +} diff --git a/zaas-service/src/test/resources/zaas-messages.yml b/zaas-service/src/test/resources/zaas-messages.yml index 3c89ebef90..0e3efa1045 100644 --- a/zaas-service/src/test/resources/zaas-messages.yml +++ b/zaas-service/src/test/resources/zaas-messages.yml @@ -229,3 +229,18 @@ messages: text: "z/OSMF is not available or z/OSMF response does not contain any token. Reason: %s" reason: z/OSMF does not return JWT or LTPA tokens. action: Make sure z/OSMF is available to API ML or review your z/OSMF configuration. + + - key: org.zowe.apiml.common.notFound + number: ZWEAO404 + type: ERROR + text: "The service can not find the requested resource." + + - key: org.zowe.apiml.common.unsupportedMediaType + number: ZWEAO415 + type: ERROR + text: "The media format of the requested data is not supported by the service, so the service has rejected the request." + + - key: org.zowe.apiml.common.internalServerError + number: ZWEAO500 + type: ERROR + text: "The service has encountered a situation it doesn't know how to handle. Please contact support for further assistance. More details are available in the log under the provided message instance ID"