From 525825c710176051d330efbeef13e3bda0fac6dd Mon Sep 17 00:00:00 2001 From: Richard Salac Date: Wed, 10 Jun 2026 13:03:47 +0200 Subject: [PATCH 01/21] wip Signed-off-by: Richard Salac --- .../config/ConnectionsConfig.java | 7 ++----- .../java/org/zowe/apiml/util/CorsUtils.java | 16 ++++++---------- .../gateway/security/config/CorsBeans.java | 16 ++++++++++++++-- .../corsTests/CorsPerServiceTest.java | 6 +++++- .../corsTests/GatewayCorsEnabledTest.java | 18 +++++++++++++----- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java index 775b700df6..9205fd80f4 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java @@ -73,10 +73,7 @@ import javax.net.ssl.TrustManagerFactory; import java.security.KeyStore; import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.springframework.cloud.netflix.eureka.EurekaClientConfigBean.DEFAULT_ZONE; @@ -333,7 +330,7 @@ public CorsConfigurationSource corsConfigurationSource(RoutePredicateHandlerMapp @Bean public CorsUtils corsUtils() { - return new CorsUtils(corsEnabled, null); + return new CorsUtils(corsEnabled, Collections.emptyList()); } @Bean diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java index c38b594312..9f95354131 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java @@ -15,10 +15,7 @@ import org.springframework.http.HttpMethod; import org.springframework.web.cors.CorsConfiguration; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.BiConsumer; import java.util.regex.Pattern; @@ -57,14 +54,14 @@ private CorsConfiguration setAllowedOriginsForService(Map metada // Check if the configuration specifies allowed origins for this service final CorsConfiguration config = new CorsConfiguration(); if (isCorsEnabledForService(metadata)) { + allowedOrigins.forEach(config::addAllowedOrigin); String corsAllowedOriginsForService = metadata.get("apiml.corsAllowedOrigins"); - if (corsAllowedOriginsForService == null || corsAllowedOriginsForService.isEmpty()) { - // Origins not specified: allow everything - config.addAllowedOriginPattern(CorsConfiguration.ALL); - } else { + if (corsAllowedOriginsForService != null && !corsAllowedOriginsForService.isEmpty()) { // Origins specified: split by comma, add to whitelist + // apiml.corsAllowedOrigins = https://www.google.com:443,https://foo.bar:1234,* Arrays.stream(corsAllowedOriginsForService.split(",")) - .forEach(config::addAllowedOrigin); + .forEach(config::addAllowedOrigin) + ; } config.setAllowCredentials(true); config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); @@ -82,7 +79,6 @@ public void registerDefaultCorsConfiguration(BiConsumer allowedOrigins; @Value("${apiml.service.ignoredHeadersWhenCorsEnabled}") private String ignoredHeadersWhenCorsEnabled; @@ -65,7 +71,7 @@ List getDefaultAllowedOrigins( // TODO: this method is a hotfix for AT-T ) throws URISyntaxException { boolean isAttls = Arrays.asList(environment.getActiveProfiles()).contains("attls"); if (corsEnabled || !isAttls) { - return null; // NOSONAR + return Collections.emptyList(); } Set gatewayOrigins = new HashSet<>(); @@ -88,6 +94,12 @@ CorsUtils corsUtils( @Value("${server.hostname:${apiml.service.hostname}}") String hostname, @Value("${server.port}") int port ) throws URISyntaxException { - return new CorsUtils(corsEnabled, getDefaultAllowedOrigins(environment, externalUrl, hostname, port)); + if (corsEnabled && allowedOrigins.isEmpty()) { + log.warn("Custom cors configuration is enabled but no cors allowed origins defined. Set gateway.apiml.service.corsAllowedOrigins in your zowe.yml"); + } + + allowedOrigins.addAll(getDefaultAllowedOrigins(environment, externalUrl, hostname, port)); + + return new CorsUtils(corsEnabled, new ArrayList<>(allowedOrigins)); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java index 591d3de458..28df123d5e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -46,6 +47,9 @@ */ @AcceptanceTest @ActiveProfiles("test") +@TestPropertySource(properties = { + "apiml.service.corsAllowedOrigins=https://foo.bar.org" +}) class CorsPerServiceTest extends AcceptanceTestWithTwoServices { @Test // Verify the header to allow CORS isn't set @@ -81,7 +85,7 @@ void givenCorsIsDelegatedToGatewayButServiceDoesntAllowCors_whenSimpleCorsReques .header(new Header("Access-Control-Request-Method", "POST")) .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) .when() - .get(basePath + serviceWithDefaultConfiguration.getPath()) + .post(basePath + serviceWithDefaultConfiguration.getPath()) .then() .statusCode(is(SC_FORBIDDEN)) .header("Access-Control-Allow-Origin", is(nullValue())); diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java index a097ee5398..65ab8a5cb6 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java @@ -12,10 +12,14 @@ import io.restassured.http.Header; import org.apache.http.client.methods.HttpUriRequest; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.springframework.context.annotation.PropertySource; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -28,7 +32,11 @@ @AcceptanceTest @ActiveProfiles("test") +@TestPropertySource(properties = { + "apiml.service.corsAllowedOrigins=https://foo.bar.org" +}) class GatewayCorsEnabledTest extends AcceptanceTestWithTwoServices { + @Test // The CORS headers are properly set on the request void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHeadersAreSet() throws Exception { @@ -37,20 +45,20 @@ void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHe .header(new Header("Origin", "https://foo.bar.org")) .header(new Header("Access-Control-Request-Method", "POST")) .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) - .when() + .when() .options(basePath + "/gateway/version") - .then() + .then() .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin","https://foo.bar.org") + .header("Access-Control-Allow-Origin", "https://foo.bar.org") .header("Access-Control-Allow-Methods", "GET,HEAD,POST,DELETE,PUT,OPTIONS") .header("Access-Control-Allow-Headers", "origin, x-requested-with"); // Actual request given() .header(new Header("Origin", "https://foo.bar.org")) - .when() + .when() .get(basePath + "/gateway/version") - .then() + .then() .statusCode(is(SC_OK)) .header("Access-Control-Allow-Origin", "https://foo.bar.org"); } From f18cb883824dce4c89eec0e0619f4b2190e3a784 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 12 Jun 2026 10:32:52 +0200 Subject: [PATCH 02/21] wip cors Signed-off-by: Pablo Carle --- .github/workflows/integration-tests.yml | 1 + .../src/main/resources/bin/start.sh | 2 - .../service/RouteLocator.java | 3 - .../java/org/zowe/apiml/util/CorsUtils.java | 33 +++-- config/docker/api-defs/staticclient.yml | 10 ++ config/docker/gateway-service.yml | 1 + config/local/api-defs/staticclient.yml | 10 ++ config/local/gateway-service.yml | 1 + .../service/CorsMetadataProcessor.java | 1 + .../gateway/security/config/CorsBeans.java | 20 +-- .../corsTests/GatewayCorsEnabledTest.java | 7 +- .../gateway/security/config/CorsBeanTest.java | 7 +- .../cloudgateway/CentralRegistryTest.java | 1 + .../gateway/CloudGatewayRoutingTest.java | 24 +++- .../proxy/CloudGatewayProxyTest.java | 1 + .../integration/proxy/CorsEnabledTest.java | 119 ++++++++++++++++++ .../zowe/apiml/util/requests/Endpoints.java | 2 + 17 files changed, 211 insertions(+), 32 deletions(-) create mode 100644 integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5467cfad54..8aabb03f1a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -65,6 +65,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMUSERHEADER: customUserHeader APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka + APIML_SERVICE_CORSENABLED: true mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} metrics-service: diff --git a/cloud-gateway-package/src/main/resources/bin/start.sh b/cloud-gateway-package/src/main/resources/bin/start.sh index 651532711f..7006926ba7 100755 --- a/cloud-gateway-package/src/main/resources/bin/start.sh +++ b/cloud-gateway-package/src/main/resources/bin/start.sh @@ -227,8 +227,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CLOUD_GATEWAY_CODE} java \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ -Dapiml.service.port=${ZWE_configs_port:-10023} \ -Dapiml.service.forwardClientCertEnabled=${ZWE_configs_apiml_service_forwardClientCertEnabled:-false} \ - -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-false} \ - -Dapiml.service.corsAllowedMethods=${ZWE_configs_apiml_service_corsAllowedMethods:-} \ -Dapiml.security.x509.registry.allowedUsers=${ZWE_configs_apiml_security_x509_registry_allowedUsers:-} \ -Dapiml.logs.location=${ZWE_zowe_logDirectory} \ -Dapiml.zoweManifest=${ZWE_zowe_runtimeDirectory}/manifest.json \ diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java index 188b5d542c..33ed8dcc71 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java @@ -176,9 +176,6 @@ public Flux getRouteDefinitions() { AtomicInteger order = new AtomicInteger(); // iterate over services return getServiceInstances().flatMap(Flux::fromIterable).map(serviceInstance -> { - // configure CORS for the service (if necessary) - setCors(serviceInstance); - // generate route definition per services and its routing rules return getAuthFilterPerRoute(order, serviceInstance, getPostRoutingFilters(serviceInstance)); }) diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java index da233635e7..ad0cb69a4f 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java @@ -11,6 +11,7 @@ package org.zowe.apiml.util; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.util.TriConsumer; import org.springframework.web.cors.CorsConfiguration; @@ -19,33 +20,39 @@ import java.util.regex.Pattern; @RequiredArgsConstructor +@Slf4j public class CorsUtils { - private final boolean corsEnabled; - private final List allowedCorsHttpMethods; - private final List allowedOrigins; + private static final Pattern gatewayRoutesPattern = Pattern.compile("apiml\\.routes.*.gateway\\S*"); private static final List CORS_ENABLED_ENDPOINTS = Arrays.asList("/*/*/gateway/**", "/gateway/*/*/**", "/gateway/version"); + private final boolean gatewayCorsEnabled; + private final List defaultAllowedCorsHttpMethods; + private final List defaultAllowedOrigins; + // private final List defaultAllowedHeaders; + public boolean isCorsEnabledForService(Map metadata) { String isCorsEnabledForService = metadata.get("apiml.corsEnabled"); return Boolean.parseBoolean(isCorsEnabledForService); } public void setCorsConfiguration(String serviceId, Map metadata, TriConsumer entryMapper) { - if (corsEnabled) { - CorsConfiguration corsConfiguration = setAllowedOriginsForService(metadata); + if (gatewayCorsEnabled) { + CorsConfiguration corsConfiguration = setCorsHeadersForService(metadata); metadata.entrySet().stream() .filter(entry -> gatewayRoutesPattern.matcher(entry.getKey()).find()) .forEach(entry -> entryMapper.accept(entry.getValue(), serviceId, corsConfiguration)); + } else { + log.debug("CORS is not enabled in Gateway"); } } - private CorsConfiguration setAllowedOriginsForService(Map metadata) { + private CorsConfiguration setCorsHeadersForService(Map metadata) { // Check if the configuration specifies allowed origins for this service final CorsConfiguration config = new CorsConfiguration(); if (isCorsEnabledForService(metadata)) { - allowedOrigins.forEach(config::addAllowedOrigin); + defaultAllowedOrigins.forEach(config::addAllowedOrigin); String corsAllowedOriginsForService = metadata.get("apiml.corsAllowedOrigins"); if (corsAllowedOriginsForService != null && !corsAllowedOriginsForService.isEmpty()) { // Origins specified: split by comma, add to whitelist @@ -55,10 +62,11 @@ private CorsConfiguration setAllowedOriginsForService(Map metada ; } config.setAllowCredentials(true); + config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); - config.setAllowedMethods(allowedCorsHttpMethods); + config.setAllowedMethods(defaultAllowedCorsHttpMethods); } else { - config.setAllowedOrigins(allowedOrigins); + config.setAllowedOrigins(defaultAllowedOrigins); } return config; } @@ -67,15 +75,16 @@ public void registerDefaultCorsConfiguration(BiConsumer pathsToEnable; - config.setAllowedOrigins(allowedOrigins); - if (corsEnabled) { + config.setAllowedOrigins(defaultAllowedOrigins); + if (gatewayCorsEnabled) { config.setAllowCredentials(true); config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); - config.setAllowedMethods(allowedCorsHttpMethods); + config.setAllowedMethods(defaultAllowedCorsHttpMethods); pathsToEnable = CORS_ENABLED_ENDPOINTS; } else { pathsToEnable = Collections.singletonList("/**"); } pathsToEnable.forEach(path -> pathMapper.accept(path, config)); } + } diff --git a/config/docker/api-defs/staticclient.yml b/config/docker/api-defs/staticclient.yml index 5a29004c64..02240631c8 100644 --- a/config/docker/api-defs/staticclient.yml +++ b/config/docker/api-defs/staticclient.yml @@ -29,6 +29,13 @@ services: customMetadata: apiml: okToRetryOnAllOperations: true + corsEnabled: true + corsAllowedOrigins: https://discoverable-client:10012/discoverableclient + # corsAllowedMethods: GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS + # corsAllowedHeaders: Content-Type,Authorization + # corsExposedHeaders: Content-Type,Authorization + # corsMaxAge: 3600 + # corsAllowCredentials: true - serviceId: zowejwt # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) @@ -77,6 +84,9 @@ services: - apiId: zowe.apiml.discoverableclient gatewayUrl: api/v1 version: 1.0.0 + customMetadata: + apiml: + corsEnabled: false - serviceId: dcbypass # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) diff --git a/config/docker/gateway-service.yml b/config/docker/gateway-service.yml index 7d42310843..eb07e5b3c1 100644 --- a/config/docker/gateway-service.yml +++ b/config/docker/gateway-service.yml @@ -3,6 +3,7 @@ apiml: apimlId: apiml1 hostname: gateway-service discoveryServiceUrls: https://discovery-service:10011/eureka/ + corsEnabled: true security: allowTokenRefresh: true personalAccessToken: diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 1a01bc94ae..1b65767709 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -29,6 +29,13 @@ services: customMetadata: apiml: okToRetryOnAllOperations: true + corsEnabled: true + corsAllowedOrigins: https://localhost:10010 + # corsAllowedMethods: GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS + # corsAllowedHeaders: Content-Type,Authorization + # corsExposedHeaders: Content-Type,Authorization + # corsMaxAge: 3600 + # corsAllowCredentials: true - serviceId: staticclient2 # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) @@ -54,6 +61,9 @@ services: - apiId: zowe.apiml.discoverableclient gatewayUrl: api/v1 version: 1.0.0 + customMetadata: + apiml: + corsEnabled: false - serviceId: zowejwt # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) diff --git a/config/local/gateway-service.yml b/config/local/gateway-service.yml index 3f6eaeb680..43ba191005 100644 --- a/config/local/gateway-service.yml +++ b/config/local/gateway-service.yml @@ -6,6 +6,7 @@ apiml: ipAddress: 127.0.0.1 port: 10010 discoveryServiceUrls: https://localhost:10011/eureka/ + corsEnabled: true security: allowTokenRefresh: true diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java index 0fa4fbf5e1..cb75d462cf 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java @@ -48,4 +48,5 @@ protected void checkInstanceInfo(InstanceInfo instanceInfo) { corsUtils.setCorsConfiguration(instanceInfo.getVIPAddress().toLowerCase(), metadata, (entry, serviceId, config) -> cors.registerCorsConfiguration("/" + entry + "/" + serviceId + "/**", config)); } } + } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java index f53cc084cf..afaefc0ca0 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java @@ -11,8 +11,6 @@ package org.zowe.apiml.gateway.security.config; import lombok.RequiredArgsConstructor; -import lombok.extern.java.Log; -import lombok.extern.log4j.Log4j; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -65,19 +63,17 @@ private void addCorsRelatedIgnoredHeaders() { List getDefaultAllowedOrigins( // TODO: this method is a hotfix for AT-TLS, but it could be a breaking change, verify no-ATTLS configuration in v3 Environment environment, - String externalUrl, + List externalUrls, String hostname, int port ) throws URISyntaxException { boolean isClientAttlsEnabled = Arrays.asList(environment.getActiveProfiles()).contains("attlsClient"); if (corsEnabled || !isClientAttlsEnabled) { - return null; // NOSONAR + return null; } Set gatewayOrigins = new HashSet<>(); - if (StringUtils.isNotBlank(externalUrl)) { - gatewayOrigins.add(externalUrl); - } + externalUrls.stream().filter(StringUtils::isNotBlank).forEach(gatewayOrigins::add); gatewayOrigins.add(new URIBuilder() .setScheme("https") .setHost(hostname) @@ -90,10 +86,16 @@ List getDefaultAllowedOrigins( // TODO: this method is a hotfix for AT-T @Bean CorsUtils corsUtils( Environment environment, - @Value("${apiml.service.externalUrl:}") String externalUrl, + @Value("${apiml.service.externalUrl:}") String externalUrl, // FIXME Should support multiple external URLs @Value("${server.hostname:${apiml.service.hostname}}") String hostname, @Value("${server.port}") int port ) throws URISyntaxException { - return new CorsUtils(corsEnabled, corsAllowedMethods, getDefaultAllowedOrigins(environment, externalUrl, hostname, port)); + return new CorsUtils( + corsEnabled, + corsAllowedMethods, + getDefaultAllowedOrigins(environment, new ArrayList<>(Arrays.asList(externalUrl)), + hostname, + port)); } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java index 6a2151deef..20de8133bb 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java @@ -12,13 +12,10 @@ import io.restassured.http.Header; import org.apache.http.client.methods.HttpUriRequest; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; -import org.springframework.context.annotation.PropertySource; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.NestedTestConfiguration; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -28,7 +25,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @AcceptanceTest @ActiveProfiles("test") diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java index 8936fc850a..0a48c81402 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java @@ -26,6 +26,8 @@ import java.lang.reflect.Field; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -47,7 +49,7 @@ void whenGetDefaultOrigins_thenAllowHttps() throws URISyntaxException { CorsBeans corsBeans = new CorsBeans(new ZuulProperties()); when(environment.getActiveProfiles()).thenReturn(new String[]{ "attlsServer", "attlsClient" }); - List allowedOrigins = corsBeans.getDefaultAllowedOrigins(environment, "https://dvipahost:10010", "lparhost", 10010); + List allowedOrigins = corsBeans.getDefaultAllowedOrigins(environment, new ArrayList<>(Arrays.asList("https://dvipahost:10010")), "lparhost", 10010); assertEquals(2, allowedOrigins.size()); assertTrue(allowedOrigins.contains("https://dvipahost:10010")); assertTrue(allowedOrigins.contains("https://lparhost:10010")); @@ -100,6 +102,9 @@ void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessExce assertEquals("POST", corsAllowedMethods.get(1)); assertEquals("PATCH", corsAllowedMethods.get(2)); } + } + } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java index 5064eb2220..b974fc3e36 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java @@ -165,4 +165,5 @@ private URI buildRegistryURI(String apimlId, String apiId, String serviceId) { return new URL(conf.getScheme(), conf.getHost(), conf.getPort(), CENTRAL_REGISTRY_PATH) .toURI().resolve(query); } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java index 854fa8f5db..d70eac530f 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java @@ -12,7 +12,11 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.zowe.apiml.util.TestWithStartedInstances; @@ -28,6 +32,7 @@ @DiscoverableClientDependentTest @Tag("CloudGatewayServiceRouting") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class CloudGatewayRoutingTest implements TestWithStartedInstances { private static final String HEADER_X_FORWARD_TO = "X-Forward-To"; @@ -38,7 +43,7 @@ class CloudGatewayRoutingTest implements TestWithStartedInstances { private static final CloudGatewayConfiguration conf = ConfigReader.environmentConfiguration().getCloudGatewayConfiguration(); @BeforeAll - static void setup() { + void setup() { RestAssured.useRelaxedHTTPSValidation(); } @@ -104,4 +109,21 @@ void testWrongRoutingWithBasePath(String basePath) throws URISyntaxException { given().get(new URI(scgUrl)).then().statusCode(404); } + @Nested + class WhenCorsIsEnabled { +// Using staticclient, try preflight request and simple request +// Using staticclient2 + + @BeforeEach + void setUp() { + + } + + @Test + void givenServiceHasCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreSet() { + + } + + } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CloudGatewayProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CloudGatewayProxyTest.java index c756a2a4de..23a4a14343 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CloudGatewayProxyTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CloudGatewayProxyTest.java @@ -155,4 +155,5 @@ void thenCertificatesChainProvided() throws URISyntaxException { } } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java new file mode 100644 index 0000000000..9bad267e31 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java @@ -0,0 +1,119 @@ +/* + * 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.integration.proxy; + +import io.restassured.http.Header; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.zowe.apiml.util.TestWithStartedInstances; +import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; +import org.zowe.apiml.util.http.HttpRequestUtils; +import org.zowe.apiml.util.requests.Endpoints; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; + +@DiscoverableClientDependentTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CorsEnabledTest implements TestWithStartedInstances { + + // // Need to login to get a token and try credentials config. + // private final GatewayServiceConfiguration gatewayServiceConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + + // private String gatewayScheme; + // private String gatewayHost; + // private int gatewayPort; + + @BeforeAll + void init() { + // gatewayScheme = gatewayServiceConfiguration.getScheme(); + // gatewayHost = gatewayServiceConfiguration.getHost(); + // gatewayPort = gatewayServiceConfiguration.getPort(); + } + + @Nested + class WhenCorsIsEnabled { + + @Test + void givenServiceHasCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreSet() { + given() + .header("Origin", "https://foo.bar.org") + .when() + .get(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_1_REQUEST)) + .then() + .log().all() + .statusCode(SC_OK) + .header("Access-Control-Allow-Origin", "https://foo.bar.org"); + } + + @Test + void givenServiceHasCorsConfiguration_whenSimpleRequestArrives_thenCorsHeadersAreSet() { + given() + .header("Origin", "https://foo.bar.org") + .when() + .get(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_1_REQUEST)) + .then() + .log().all() + .statusCode(SC_OK) + .header("Access-Control-Allow-Origin", "https://foo.bar.org"); + } + + @Test + void givenServiceDoesNotHaveCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreNotSet() { + given() + .header("Origin", "https://foo.bar.org") + .when() + .options(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_2_REQUEST)) + .then() + .log().all() + .statusCode(SC_OK) + .header("Access-Control-Allow-Origin", "https://foo.bar.org"); + } + + @Test // TODO check names + void givenServiceHasIncompleteCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreNotSet() { + + } + + } + + @Nested + class WhenCorsIsDisabled { + + // preflight (should not be forwarded to service) + @Test + void thenForwardToServiceAsIs() { + given() + .header(new Header("Origin", "")) + .when() + .options("/gateway/version") + .then() + .statusCode(SC_OK) + .header("Access-Control-Allow-Origin", ""); + } + + // simple request (should be forwarded to service) + @Test + void simpleRequest() { + given() + .header(new Header("Origin", "")) + .when() + .get("/gateway/version") + .then() + .statusCode(SC_OK) + .header("Access-Control-Allow-Origin", ""); + } + + } + +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java index 744a8f3eaf..8ddf48460a 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java @@ -70,6 +70,8 @@ public class Endpoints { public final static String SAF_IDT_REQUEST = "/dcsafidt/api/v1/request"; public final static String ZOSMF_REQUEST = "/dczosmf/api/v1/request"; public final static String ZOWE_JWT_REQUEST = "/zowejwt/api/v1/request"; + public final static String STATIC_CLIENT_2_REQUEST = "/staticclient2/api/v1/request"; + public final static String STATIC_CLIENT_1_REQUEST = "/staticclient/api/v1/request"; public final static String DISCOVERABLE_CLIENT_CONTAINER_ENDPOINT = "/apicatalog/api/v1/containers/cademoapps"; public final static String DISCOVERABLE_CLIENT_API_DOC_ENDPOINT = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0"; From 60b4832597977faa22bd2c9b9df05c8dd071c350 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 15 Jun 2026 13:49:09 +0200 Subject: [PATCH 03/21] wip Signed-off-by: Pablo Carle --- .../config/ConnectionsConfig.java | 5 +- .../java/org/zowe/apiml/util/CorsUtils.java | 7 ++- config/local/api-defs/staticclient.yml | 30 ++++++++- config/local/gateway-service.yml | 2 +- .../service/CorsMetadataProcessor.java | 5 +- .../gateway/security/config/CorsBeans.java | 48 +++++++++------ .../service/CorsMetadataProcessorTest.java | 10 ++- .../integration/proxy/CorsEnabledTest.java | 61 +++++++++---------- .../zowe/apiml/util/config/ConfigReader.java | 3 +- .../util/service/FullApiMediationLayer.java | 1 + 10 files changed, 110 insertions(+), 62 deletions(-) diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java index 9eb55d9c12..2927edf03a 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java @@ -356,7 +356,10 @@ public CorsConfigurationSource corsConfigurationSource(RoutePredicateHandlerMapp @Bean public CorsUtils corsUtils() { - return new CorsUtils(corsEnabled, corsAllowedMethods, null); + return CorsUtils.builder() + .gatewayCorsEnabled(corsEnabled) + .defaultAllowedCorsHttpMethods(corsAllowedMethods) + .build(); } @Bean diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java index ad0cb69a4f..68a91b3830 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java @@ -10,7 +10,7 @@ package org.zowe.apiml.util; -import lombok.RequiredArgsConstructor; +import lombok.Builder; import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.util.TriConsumer; import org.springframework.web.cors.CorsConfiguration; @@ -19,8 +19,8 @@ import java.util.function.BiConsumer; import java.util.regex.Pattern; -@RequiredArgsConstructor @Slf4j +@Builder public class CorsUtils { private static final Pattern gatewayRoutesPattern = Pattern.compile("apiml\\.routes.*.gateway\\S*"); @@ -65,8 +65,11 @@ private CorsConfiguration setCorsHeadersForService(Map metadata) config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); config.setAllowedMethods(defaultAllowedCorsHttpMethods); + + log.debug("CORS enabled for service {}: {}", metadata.get("apiml.service.title"), config); } else { config.setAllowedOrigins(defaultAllowedOrigins); + log.debug("CORS is not enabled for service {}. Using defaults {}", metadata.get("apiml.service.title"), defaultAllowedOrigins); } return config; } diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 1b65767709..03768909a5 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -30,7 +30,7 @@ services: apiml: okToRetryOnAllOperations: true corsEnabled: true - corsAllowedOrigins: https://localhost:10010 + corsAllowedOrigins: https://foo.bar:10010 # corsAllowedMethods: GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS # corsAllowedHeaders: Content-Type,Authorization # corsExposedHeaders: Content-Type,Authorization @@ -65,6 +65,34 @@ services: apiml: corsEnabled: false + - serviceId: staticcors1 # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Staticaly Defined Service for CORS testing # Title of the service in the API catalog + description: Sample to demonstrate how to add an API service without Swagger documentation to API Catalog using a static YAML definition # Description of the service in the API catalog + 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 + - gatewayUrl: ui/v1 + serviceRelativeUrl: / + - gatewayUrl: ws/v1 + serviceRelativeUrl: /ws + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + applid: TSTAPPL # APPLID to generate PassTickets for this service + # List of APIs provided by the service (currently only one is supported): + apiInfo: + - apiId: zowe.apiml.discoverableclient + gatewayUrl: api/v1 + version: 1.0.0 + customMetadata: + apiml: + corsEnabled: true + - serviceId: zowejwt # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) title: Staticaly Defined Service To Test Zowe JWT scheme # Title of the service in the API catalog diff --git a/config/local/gateway-service.yml b/config/local/gateway-service.yml index 43ba191005..bf5623feae 100644 --- a/config/local/gateway-service.yml +++ b/config/local/gateway-service.yml @@ -1,4 +1,4 @@ -spring.profiles.include: diag +spring.profiles.include: diag,debug apiml: service: apimlId: apiml1 diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java index cb75d462cf..6f0c94e91e 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java @@ -29,12 +29,11 @@ public class CorsMetadataProcessor extends MetadataProcessor { @Value("${apiml.service.corsEnabled:false}") - private boolean corsEnabled; + private boolean gatewayCorsEnabled; private final EurekaApplications applications; private final CorsConfigurationSource corsConfigurationSource; private final CorsUtils corsUtils; - @Override List getApplications() { return this.applications.getRegistered(); @@ -43,7 +42,7 @@ List getApplications() { protected void checkInstanceInfo(InstanceInfo instanceInfo) { Map metadata = instanceInfo.getMetadata(); - if (metadata != null && corsEnabled) { + if (metadata != null && gatewayCorsEnabled) { UrlBasedCorsConfigurationSource cors = (UrlBasedCorsConfigurationSource) this.corsConfigurationSource; corsUtils.setCorsConfiguration(instanceInfo.getVIPAddress().toLowerCase(), metadata, (entry, serviceId, config) -> cors.registerCorsConfiguration("/" + entry + "/" + serviceId + "/**", config)); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java index afaefc0ca0..ab85330c2f 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java @@ -25,7 +25,12 @@ import org.zowe.apiml.util.CorsUtils; import java.net.URISyntaxException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * Externalized configuration of CORS behavior @@ -37,18 +42,22 @@ public class CorsBeans { @Value("${apiml.service.corsEnabled:false}") - private boolean corsEnabled; + private boolean gatewayCorsEnabled; + @Value("${apiml.service.corsDefaultAllowedOrigins:#{null}}") + private String corsDefaultAllowedOrigins; @Value("${apiml.service.corsAllowedMethods:GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}") - private List corsAllowedMethods; - @Value("${apiml.service.ignoredHeadersWhenCorsEnabled}") + private List corsDefaultAllowedMethods; + @Value("${apiml.service.ignoredHeadersWhenCorsEnabled}") // Used by cloud gateway? private String ignoredHeadersWhenCorsEnabled; + @Value("${apiml.service.port}") + private String port; private final ZuulProperties zuulProperties; @Bean CorsConfigurationSource corsConfigurationSource(CorsUtils corsUtils) { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - if (corsEnabled) { + if (gatewayCorsEnabled) { addCorsRelatedIgnoredHeaders(); } corsUtils.registerDefaultCorsConfiguration(source::registerCorsConfiguration); @@ -61,19 +70,24 @@ private void addCorsRelatedIgnoredHeaders() { )); } - List getDefaultAllowedOrigins( // TODO: this method is a hotfix for AT-TLS, but it could be a breaking change, verify no-ATTLS configuration in v3 + List getDefaultAllowedOrigins( Environment environment, - List externalUrls, + List externalDomains, String hostname, int port ) throws URISyntaxException { + if (corsDefaultAllowedOrigins != null) { + return Arrays.asList(corsDefaultAllowedOrigins.split(",")); + } boolean isClientAttlsEnabled = Arrays.asList(environment.getActiveProfiles()).contains("attlsClient"); - if (corsEnabled || !isClientAttlsEnabled) { - return null; + if (gatewayCorsEnabled || !isClientAttlsEnabled) { + return externalDomains.stream() + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); } Set gatewayOrigins = new HashSet<>(); - externalUrls.stream().filter(StringUtils::isNotBlank).forEach(gatewayOrigins::add); + externalDomains.stream().filter(StringUtils::isNotBlank).forEach(gatewayOrigins::add); gatewayOrigins.add(new URIBuilder() .setScheme("https") .setHost(hostname) @@ -83,19 +97,19 @@ List getDefaultAllowedOrigins( // TODO: this method is a hotfix for AT-T return new ArrayList<>(gatewayOrigins); } + @Bean CorsUtils corsUtils( Environment environment, - @Value("${apiml.service.externalUrl:}") String externalUrl, // FIXME Should support multiple external URLs + @Value("${apiml.service.externalUrl:}") String externalUrl, @Value("${server.hostname:${apiml.service.hostname}}") String hostname, @Value("${server.port}") int port ) throws URISyntaxException { - return new CorsUtils( - corsEnabled, - corsAllowedMethods, - getDefaultAllowedOrigins(environment, new ArrayList<>(Arrays.asList(externalUrl)), - hostname, - port)); + return CorsUtils.builder() + .gatewayCorsEnabled(gatewayCorsEnabled) + .defaultAllowedCorsHttpMethods(corsDefaultAllowedMethods) + .defaultAllowedOrigins(getDefaultAllowedOrigins(environment, new ArrayList<>(Arrays.asList(externalUrl)), hostname, port)).build(); + } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java index f324d5f48f..6dff62af6c 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java @@ -18,10 +18,10 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.zowe.apiml.util.CorsUtils; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; @@ -38,7 +38,11 @@ class CorsMetadataProcessorTest { @BeforeEach void setUp() { configurationSource = mock(UrlBasedCorsConfigurationSource.class); - corsUtils = new CorsUtils(true, Stream.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS").collect(Collectors.toList()), null); + corsUtils = CorsUtils.builder() + .gatewayCorsEnabled(true) + .defaultAllowedCorsHttpMethods(Arrays.asList("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS")) + .defaultAllowedOrigins(Collections.emptyList()) + .build(); } @Nested diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java index 9bad267e31..06b790b84b 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java @@ -10,13 +10,20 @@ package org.zowe.apiml.integration.proxy; +import io.restassured.RestAssured; import io.restassured.http.Header; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +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.ConfigReader; +import org.zowe.apiml.util.config.GatewayServiceConfiguration; +import org.zowe.apiml.util.config.ItSslConfigFactory; +import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.http.HttpRequestUtils; import org.zowe.apiml.util.requests.Endpoints; @@ -27,53 +34,41 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class CorsEnabledTest implements TestWithStartedInstances { - // // Need to login to get a token and try credentials config. - // private final GatewayServiceConfiguration gatewayServiceConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); - - // private String gatewayScheme; - // private String gatewayHost; - // private int gatewayPort; + private final GatewayServiceConfiguration gatewayServiceConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); @BeforeAll - void init() { - // gatewayScheme = gatewayServiceConfiguration.getScheme(); - // gatewayHost = gatewayServiceConfiguration.getHost(); - // gatewayPort = gatewayServiceConfiguration.getPort(); + void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + SslContext.prepareSslAuthentication(ItSslConfigFactory.integrationTests()); } @Nested - class WhenCorsIsEnabled { - - @Test - void givenServiceHasCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreSet() { + class WhenCorsIsEnabledInService { + + @ParameterizedTest + @CsvSource({ + "https://foo.bar.org, 200", + "https://localhost:10010, 403" + }) + void test1(String origin, int statusCode) { given() - .header("Origin", "https://foo.bar.org") - .when() - .get(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_1_REQUEST)) - .then() .log().all() - .statusCode(SC_OK) - .header("Access-Control-Allow-Origin", "https://foo.bar.org"); - } - - @Test - void givenServiceHasCorsConfiguration_whenSimpleRequestArrives_thenCorsHeadersAreSet() { - given() - .header("Origin", "https://foo.bar.org") + .header("Origin", origin) + .header("Access-Control-Request-Method", "GET") + .header("Access-Control-Request-Headers", "Origin") .when() - .get(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_1_REQUEST)) + .options(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_1_REQUEST)) .then() .log().all() - .statusCode(SC_OK) - .header("Access-Control-Allow-Origin", "https://foo.bar.org"); + .statusCode(statusCode); } @Test - void givenServiceDoesNotHaveCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreNotSet() { + void test3() { given() .header("Origin", "https://foo.bar.org") .when() - .options(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_2_REQUEST)) + .options(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_1_REQUEST)) .then() .log().all() .statusCode(SC_OK) @@ -81,14 +76,14 @@ void givenServiceDoesNotHaveCorsConfiguration_whenPreflightRequestArrives_thenCo } @Test // TODO check names - void givenServiceHasIncompleteCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreNotSet() { + void test4() { } } @Nested - class WhenCorsIsDisabled { + class WhenCorsIsDisabledInService { // preflight (should not be forwarded to service) @Test diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java index 1a9f890f5e..5adfaa2614 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java @@ -50,7 +50,8 @@ public static EnvironmentConfiguration environmentConfiguration() { } catch (URISyntaxException exception) { log.error("Incorrect environment-configuration.yml location: " + exception.getMessage(), exception); configFile = new File(Objects.requireNonNull(classLoader.getResource(configFileName)).getFile()); - + } catch (NullPointerException exception) { + configFile = new File("src/test/resources/" + configFileName); } ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); EnvironmentConfiguration configuration; diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java b/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java index 6adfc94de5..636caa0d3c 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java @@ -27,6 +27,7 @@ @Slf4j public class FullApiMediationLayer { + private RunningService discoveryService; private RunningService gatewayService; private RunningService apiCatalogService; From c572e4b6152ae8c89a95e62e6ac91cccceb338f5 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 15 Jun 2026 14:54:49 +0200 Subject: [PATCH 04/21] wip Signed-off-by: Pablo Carle --- .../service/RouteLocator.java | 5 +-- .../service/RouteLocatorTest.java | 39 +++++++++++++------ .../java/org/zowe/apiml/util/CorsUtils.java | 4 +- .../org/zowe/apiml/util/CorsUtilsTest.java | 8 ++-- config/local/api-defs/staticclient.yml | 2 +- .../service/CorsMetadataProcessor.java | 5 ++- .../src/main/resources/application.yml | 12 +++--- .../service/CorsMetadataProcessorTest.java | 9 +++-- .../integration/proxy/CorsEnabledTest.java | 6 ++- 9 files changed, 56 insertions(+), 34 deletions(-) diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java index 33ed8dcc71..e60a35a7c2 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java @@ -95,10 +95,9 @@ void setAuth(ServiceInstance serviceInstance, RouteDefinition routeDefinition, A void setCors(ServiceInstance serviceInstance) { corsUtils.setCorsConfiguration( - serviceInstance.getServiceId().toLowerCase(), serviceInstance.getMetadata(), - (prefix, serviceId, config) -> { - serviceId = serviceInstance.getMetadata().getOrDefault(APIML_ID, serviceInstance.getServiceId().toLowerCase()); + (prefix, config) -> { + String serviceId = serviceInstance.getMetadata().getOrDefault(APIML_ID, serviceInstance.getServiceId().toLowerCase()); getCorsConfigurationSource().registerCorsConfiguration("/" + serviceId + "/**", config); }); } diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java index cb424e92dc..be9ce3b3d9 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java @@ -10,7 +10,6 @@ package org.zowe.apiml.cloudgatewayservice.service; -import org.apache.logging.log4j.util.TriConsumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -32,12 +31,28 @@ import org.zowe.apiml.util.CorsUtils; import reactor.core.publisher.Flux; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_SUPPORTING_CLIENT_CERT_FORWARDING; @@ -163,31 +178,31 @@ void givenExistingAuthenticationScheme_whenSetAuth_thenCallApply() { verify(SCHEME_HANDLER_FILTERS[0]).apply(MOCK_SERVICE, routeDefinition, authentication); } - private TriConsumer getCorsLambda(Consumer> metadataProcessor) { + private BiConsumer getCorsLambda(Consumer> metadataProcessor) { ServiceInstance serviceInstance = createServiceInstance("myservice", "api/v1"); metadataProcessor.accept(serviceInstance.getMetadata()); routeLocator.setCors(serviceInstance); - ArgumentCaptor> lambdaCaptor = ArgumentCaptor.forClass(TriConsumer.class); - verify(corsUtils).setCorsConfiguration(anyString(), any(), lambdaCaptor.capture()); + ArgumentCaptor> lambdaCaptor = ArgumentCaptor.forClass(BiConsumer.class); + verify(corsUtils).setCorsConfiguration(any(), lambdaCaptor.capture()); return lambdaCaptor.getValue(); } @Test void givenApimlId_whenSetCors_thenServiceIdIsReplacedWithApimlId() { - TriConsumer corsLambda = getCorsLambda(md -> md.put(APIML_ID, "apimlid")); + BiConsumer corsLambda = getCorsLambda(md -> md.put(APIML_ID, "apimlid")); - corsLambda.accept(null, "myservice", null); + corsLambda.accept("myservice", null); verify(urlBasedCorsConfigurationSource).registerCorsConfiguration("/apimlid/**", null); } @Test void givenNoApimlId_whenSetCors_thenServiceIdIsUsed() { - TriConsumer corsLambda = getCorsLambda(md -> {}); + BiConsumer corsLambda = getCorsLambda(md -> {}); - corsLambda.accept(null, "myservice", null); + corsLambda.accept("myservice", null); verify(urlBasedCorsConfigurationSource).registerCorsConfiguration("/myservice/**", null); } @@ -245,7 +260,7 @@ void givenRouteLocator_whenGetRouteDefinitions_thenGenerateAll() { int index = 0; for (String serviceId : new String[] {"service1", "service2"}) { - verify(corsUtils).setCorsConfiguration(eq(serviceId), any(), any()); + verify(corsUtils).setCorsConfiguration(any(), any()); for (String gatewayUrl : new String[] {"a/b", ""}) { for (String producerId : new String[] {"id0", "id5", "id10"}) { diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java index 68a91b3830..0a0f9d6e2d 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java @@ -36,13 +36,13 @@ public boolean isCorsEnabledForService(Map metadata) { return Boolean.parseBoolean(isCorsEnabledForService); } - public void setCorsConfiguration(String serviceId, Map metadata, TriConsumer entryMapper) { + public void setCorsConfiguration(Map metadata, BiConsumer entryMapper) { if (gatewayCorsEnabled) { CorsConfiguration corsConfiguration = setCorsHeadersForService(metadata); metadata.entrySet().stream() .filter(entry -> gatewayRoutesPattern.matcher(entry.getKey()).find()) .forEach(entry -> - entryMapper.accept(entry.getValue(), serviceId, corsConfiguration)); + entryMapper.accept(entry.getValue(), corsConfiguration)); } else { log.debug("CORS is not enabled in Gateway"); } diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java index bfefcbd233..ac92a72cae 100644 --- a/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java +++ b/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java @@ -55,7 +55,7 @@ void registerDefaultConfig() { @Test void registerConfigForService() { - corsUtils.setCorsConfiguration("dclient", metadata, (path, serviceId, configuration) -> { + corsUtils.setCorsConfiguration(metadata, (path, configuration) -> { assertEquals(metadata.get("apiml.routes.v1.gateway"), path); assertNotNull(configuration.getAllowedHeaders()); assertEquals(1, configuration.getAllowedHeaders().size()); @@ -68,7 +68,7 @@ void registerConfigForService() { @Test void registerDefaultConfigForService() { metadata.remove("apiml.corsEnabled"); - corsUtils.setCorsConfiguration("dclient", metadata, (path, serviceId, configuration) -> { + corsUtils.setCorsConfiguration(metadata, (path, configuration) -> { assertEquals(metadata.get("apiml.routes.v1.gateway"), path); assertNull(configuration.getAllowedMethods()); } @@ -79,7 +79,7 @@ void registerDefaultConfigForService() { void registerConfigForServiceWithCustomOrigins() { Map customMetadata = new HashMap<>(metadata); customMetadata.put("apiml.corsAllowedOrigins", "https://localhost:3000,http://hostname.com,https://anothehostname:3040"); - corsUtils.setCorsConfiguration("dclient", customMetadata, (path, serviceId, configuration) -> { + corsUtils.setCorsConfiguration(customMetadata, (path, configuration) -> { assertEquals(metadata.get("apiml.routes.v1.gateway"), path); assertNotNull(configuration.getAllowedHeaders()); assertTrue(configuration.getAllowedOrigins().contains("https://localhost:3000")); @@ -107,7 +107,7 @@ void registerEmptyDefaultConfig() { @Test void registerEmptyConfigForService() { - corsUtils.setCorsConfiguration("dcclient", metadata, (path, serviceId, configuration) -> { + corsUtils.setCorsConfiguration(metadata, (path, configuration) -> { assertNull(configuration.getAllowedHeaders()); assertNull(configuration.getAllowedMethods()); } diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 03768909a5..64ef1ef6d7 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -30,7 +30,7 @@ services: apiml: okToRetryOnAllOperations: true corsEnabled: true - corsAllowedOrigins: https://foo.bar:10010 + corsAllowedOrigins: https://localhost2:10010 # corsAllowedMethods: GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS # corsAllowedHeaders: Content-Type,Authorization # corsExposedHeaders: Content-Type,Authorization diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java index 6f0c94e91e..79e9c6014c 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java @@ -44,8 +44,11 @@ protected void checkInstanceInfo(InstanceInfo instanceInfo) { if (metadata != null && gatewayCorsEnabled) { UrlBasedCorsConfigurationSource cors = (UrlBasedCorsConfigurationSource) this.corsConfigurationSource; - corsUtils.setCorsConfiguration(instanceInfo.getVIPAddress().toLowerCase(), metadata, (entry, serviceId, config) -> cors.registerCorsConfiguration("/" + entry + "/" + serviceId + "/**", config)); + corsUtils.setCorsConfiguration( + metadata, + (gatewayRoute, config) -> cors.registerCorsConfiguration("/" + instanceInfo.getVIPAddress().toLowerCase() + "/" + gatewayRoute + "/**", config)); // i.e. /staticcors1/api/v1/** } + } } diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 9631058a85..4bf9d48f31 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -304,18 +304,18 @@ management: logging: level: ROOT: INFO - org.zowe.apiml: DEBUG - org.springframework: INFO + com.netflix: INFO + com.netflix.discovery.shared.transport.decorator: DEBUG org.apache: INFO org.apache.http: DEBUG org.apache.tomcat.util.net.jsse.JSSESupport: INFO - com.netflix: INFO - org.hibernate: INFO - org.springframework.web.servlet.PageNotFound: WARN org.ehcache: INFO + org.hibernate: INFO + org.springframework: INFO org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter: INFO - com.netflix.discovery.shared.transport.decorator: DEBUG + org.springframework.web.servlet.PageNotFound: WARN org.springframework.web.socket: DEBUG + org.zowe.apiml: DEBUG org.zowe.apiml.gateway.ws: DEBUG --- diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java index 6dff62af6c..57f1cba195 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java @@ -55,7 +55,7 @@ void corsIsEnabledPerService_allowedOriginsAreProvided() { metadata.put("apiml.corsEnabled", "true"); metadata.put("apiml.corsAllowedOrigins", "http://local1,http://local2"); metadata.put("apiml.routes.0.gateway", "gateway"); - corsUtils.setCorsConfiguration("cors-enabled-origins-allowed", metadata, (entry, serviceId, config) -> configurationSource.registerCorsConfiguration("/" + entry + "/" + serviceId + "/**", config)); + corsUtils.setCorsConfiguration(metadata, (entry, config) -> configurationSource.registerCorsConfiguration("/cors-enabled-origins-allowed/" + entry + "/**", config)); verify(configurationSource).registerCorsConfiguration(any(), configurationCaptor.capture()); @@ -72,7 +72,7 @@ void corsIsEnabledPerService_allowedOriginsArentProvided() { Map metadata = new HashMap<>(); metadata.put("apiml.corsEnabled", "true"); metadata.put("apiml.routes.0.gateway", "gateway"); - corsUtils.setCorsConfiguration("cors-enabled-all-origins", metadata, (entry, serviceId, config) -> configurationSource.registerCorsConfiguration("/" + entry + "/" + serviceId + "/**", config)); + corsUtils.setCorsConfiguration(metadata, (entry, config) -> configurationSource.registerCorsConfiguration("/cors-enabled-all-origins/" + entry + "/**", config)); verify(configurationSource).registerCorsConfiguration(any(), configurationCaptor.capture()); @@ -93,15 +93,18 @@ private void assertDefaultConfiguration(CorsConfiguration provided) { @Nested class GivenCorsDisabled { + @Test void corsIsDisabledPerService() { Map metadata = new HashMap<>(); metadata.put("apiml.corsEnabled", "false"); metadata.put("apiml.routes.0.gateway", "gateway"); - corsUtils.setCorsConfiguration("cors-disabled", metadata, (entry, serviceId, config) -> configurationSource.registerCorsConfiguration("/" + entry + "/" + serviceId + "/**", config)); + corsUtils.setCorsConfiguration(metadata, (entry, config) -> configurationSource.registerCorsConfiguration("/cors-disabled/" + entry + "/**", config)); verify(configurationSource).registerCorsConfiguration(any(), configurationCaptor.capture()); } + } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java index 06b790b84b..e5604cdc54 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java @@ -47,8 +47,10 @@ class WhenCorsIsEnabledInService { @ParameterizedTest @CsvSource({ - "https://foo.bar.org, 200", - "https://localhost:10010, 403" + "https://localhost2:10010, 200", + "https://localhost:10010, 200", + "https://foo.bar:10010, 403", + "https://localhost:10011, 403" }) void test1(String origin, int statusCode) { given() From ef67682f3c2d6c93cbadaf4747e304fde442fd07 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 15 Jun 2026 16:54:38 +0200 Subject: [PATCH 05/21] wip fix tests Signed-off-by: Pablo Carle --- .../config/ConnectionsConfigTest.java | 12 +++------ .../java/org/zowe/apiml/util/CorsUtils.java | 6 +++-- .../service/CorsMetadataProcessor.java | 6 ++++- .../gateway/security/config/CorsBeans.java | 14 +++++----- .../corsTests/CorsPerServiceTest.java | 26 +++---------------- .../corsTests/GatewayCorsEnabledTest.java | 10 +++---- .../acceptance/netflix/MetadataBuilder.java | 2 ++ 7 files changed, 31 insertions(+), 45 deletions(-) diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java index 12a5fd16be..317afc5ead 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java @@ -31,7 +31,6 @@ import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.util.CorsUtils; -import java.lang.reflect.Field; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -133,10 +132,8 @@ public class WhenCorsAllowedMethodsIsNotSet { @Test void validateDefaultCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException { CorsUtils corsUtils = connectionsConfig.corsUtils(); - - Field field = corsUtils.getClass().getDeclaredField("allowedCorsHttpMethods"); - field.setAccessible(true); - List corsAllowedMethods = (List) field.get(corsUtils); + @SuppressWarnings("unchecked") + List corsAllowedMethods = (List) ReflectionTestUtils.getField(corsUtils, "defaultAllowedCorsHttpMethods"); assertEquals(7, corsAllowedMethods.size()); } } @@ -155,9 +152,8 @@ public class WhenCorsAllowedMethodsIsSet { void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException { CorsUtils corsUtils = connectionsConfig.corsUtils(); - Field field = corsUtils.getClass().getDeclaredField("allowedCorsHttpMethods"); - field.setAccessible(true); - List corsAllowedMethods = (List) field.get(corsUtils); + @SuppressWarnings("unchecked") + List corsAllowedMethods = (List) ReflectionTestUtils.getField(corsUtils, "defaultAllowedCorsHttpMethods"); assertEquals(3, corsAllowedMethods.size()); assertEquals("GET", corsAllowedMethods.get(0)); assertEquals("POST", corsAllowedMethods.get(1)); diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java index 0a0f9d6e2d..9571389ead 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java @@ -12,10 +12,12 @@ import lombok.Builder; import lombok.extern.slf4j.Slf4j; -import org.apache.logging.log4j.util.TriConsumer; import org.springframework.web.cors.CorsConfiguration; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import java.util.regex.Pattern; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java index 79e9c6014c..f7d10c2827 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessor.java @@ -46,7 +46,11 @@ protected void checkInstanceInfo(InstanceInfo instanceInfo) { UrlBasedCorsConfigurationSource cors = (UrlBasedCorsConfigurationSource) this.corsConfigurationSource; corsUtils.setCorsConfiguration( metadata, - (gatewayRoute, config) -> cors.registerCorsConfiguration("/" + instanceInfo.getVIPAddress().toLowerCase() + "/" + gatewayRoute + "/**", config)); // i.e. /staticcors1/api/v1/** + (gatewayRoute, config) -> { + cors.registerCorsConfiguration("/" + instanceInfo.getVIPAddress().toLowerCase() + "/" + gatewayRoute + "/**", config); // i.e. /staticcors1/api/v1/** + cors.registerCorsConfiguration("/" + gatewayRoute + "/" + instanceInfo.getVIPAddress().toLowerCase() + "/**", config); // i.e. /api/v1/staticcors1/** + } + ); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java index ab85330c2f..013a85cf55 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java @@ -76,26 +76,26 @@ List getDefaultAllowedOrigins( String hostname, int port ) throws URISyntaxException { + Set defaultAllowedOrigins = new HashSet<>(); if (corsDefaultAllowedOrigins != null) { - return Arrays.asList(corsDefaultAllowedOrigins.split(",")); + defaultAllowedOrigins.addAll(Arrays.asList(corsDefaultAllowedOrigins.split(","))); } boolean isClientAttlsEnabled = Arrays.asList(environment.getActiveProfiles()).contains("attlsClient"); if (gatewayCorsEnabled || !isClientAttlsEnabled) { - return externalDomains.stream() + defaultAllowedOrigins.addAll(externalDomains.stream() .filter(StringUtils::isNotBlank) - .collect(Collectors.toList()); + .collect(Collectors.toList())); } - Set gatewayOrigins = new HashSet<>(); - externalDomains.stream().filter(StringUtils::isNotBlank).forEach(gatewayOrigins::add); - gatewayOrigins.add(new URIBuilder() + externalDomains.stream().filter(StringUtils::isNotBlank).forEach(defaultAllowedOrigins::add); + defaultAllowedOrigins.add(new URIBuilder() .setScheme("https") .setHost(hostname) .setPort(port) .build().toString() ); - return new ArrayList<>(gatewayOrigins); + return new ArrayList<>(defaultAllowedOrigins); } @Bean diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java index 84a8afa4c2..d488364474 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java @@ -26,31 +26,13 @@ import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.*; -/* - * Verify default state - * - With pre-flight request - * The No Access Control Allow Origin header - * - Without pre-flight request - * The header for Access Control Allow Origin isn't present - * Verify changed state - * - With pre-flight request - * - Verify that the downstream headers for CORS are ignored - * - Without pre-flight request - * - Verify that the downstream headers for CORS are ignored - * - * The case for downstream headers will be? - * With disabled by default not much. - * Is there case when disabled cors would mean that we will have to change it? - * When allowed the headers are also irelevant as the headers adds no behavior - * What can the headers do? - * If the pre-flight request comes and we - */ @AcceptanceTest @ActiveProfiles("test") @TestPropertySource(properties = { - "apiml.service.corsAllowedOrigins=https://foo.bar.org" + "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org" // TODO This is a defaults list that can be extended in gateway configuration }) class CorsPerServiceTest extends AcceptanceTestWithTwoServices { + @Test // Verify the header to allow CORS isn't set // Verify there was no call to southbound service @@ -93,7 +75,6 @@ void givenCorsIsDelegatedToGatewayButServiceDoesntAllowCors_whenSimpleCorsReques verify(mockClient, never()).execute(ArgumentMatchers.any(HttpUriRequest.class)); } - @Test // There is no request to the southbound server for preflight // There is request to the southbound server for the second request @@ -141,7 +122,7 @@ void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeade // Preflight request given() - .header(new Header("Origin", "https://foo.bar.org")) + .header(new Header("Origin", "https://foo.bar.org")) // This can't work anymore with the defaults (cors enabled on gateway + service with cors enabled + default list of origins) .when() .get(basePath + serviceWithCustomConfiguration.getPath()) .then() @@ -151,4 +132,5 @@ void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeade // The actual request is passed to the southbound service verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java index 20de8133bb..ebbead8fa7 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java @@ -32,7 +32,7 @@ @AcceptanceTest @ActiveProfiles("test") @TestPropertySource(properties = { - "apiml.service.corsAllowedOrigins=https://foo.bar.org" + "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org" }) class GatewayCorsEnabledTest extends AcceptanceTestWithTwoServices { @@ -44,9 +44,9 @@ void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHe .header(new Header("Origin", "https://foo.bar.org")) .header(new Header("Access-Control-Request-Method", "POST")) .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) - .when() + .when() .options(basePath + "/gateway/version") - .then() + .then() .statusCode(is(SC_OK)) .header("Access-Control-Allow-Origin","https://foo.bar.org") .header("Access-Control-Allow-Methods", "GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS") @@ -55,9 +55,9 @@ void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHe // Actual request given() .header(new Header("Origin", "https://foo.bar.org")) - .when() + .when() .get(basePath + "/gateway/version") - .then() + .then() .statusCode(is(SC_OK)) .header("Access-Control-Allow-Origin", "https://foo.bar.org"); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/netflix/MetadataBuilder.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/netflix/MetadataBuilder.java index f910f5049e..6be2065a1d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/netflix/MetadataBuilder.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/netflix/MetadataBuilder.java @@ -17,6 +17,7 @@ import static java.util.stream.Collectors.joining; public class MetadataBuilder { + private Map metadata; public MetadataBuilder() { @@ -126,4 +127,5 @@ public static MetadataBuilder customInstance() { builder.withCompression(true); return builder; } + } From face0e1e0784f302adac3d1660760bc44389a17c Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 16 Jun 2026 10:48:45 +0200 Subject: [PATCH 06/21] try fix timer (cherry pick from v3) Signed-off-by: Pablo Carle --- .../GatewayContextClosedEventListener.java | 29 ++++++++++++ .../apiml/gateway/GatewayStartupListener.java | 18 ++++++- ...GatewayContextClosedEventListenerTest.java | 47 +++++++++++++++++++ .../security/config/AttlsConfigTest.java | 2 + 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayContextClosedEventListener.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayContextClosedEventListener.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayContextClosedEventListener.java new file mode 100644 index 0000000000..35736ae81a --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayContextClosedEventListener.java @@ -0,0 +1,29 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GatewayContextClosedEventListener implements ApplicationListener { + + private final GatewayStartupListener gatewayStartupListener; + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + gatewayStartupListener.onContextClosed(); + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayStartupListener.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayStartupListener.java index 344bbdf301..330e6c7f1d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayStartupListener.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayStartupListener.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicReference; @Component @RequiredArgsConstructor @@ -29,14 +30,21 @@ public class GatewayStartupListener implements ApplicationListener timer = new AtomicReference<>(); + private final Providers providers; public void onApplicationEvent(ApplicationReadyEvent event) { if (providers.isZosfmUsed()) { - new Timer().scheduleAtFixedRate(new TimerTask() { + timer.set(new Timer()); + timer.get().scheduleAtFixedRate(new TimerTask() { @Override public void run() { + if (event.getApplicationContext() != null && !event.getApplicationContext().isActive()) { + cancel(); + return; + } if (providers.isZosmfAvailableAndOnline()) { cancel(); notifyStartup(); @@ -49,8 +57,16 @@ public void run() { } } + void onContextClosed() { + if (timer.get() != null) { + timer.get().cancel(); + timer.set(null); + } + } + private void notifyStartup() { new ServiceStartupEventHandler().onServiceStartup("Gateway Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java new file mode 100644 index 0000000000..5a4844696c --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java @@ -0,0 +1,47 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class GatewayContextClosedEventListenerTest { + + @Mock + private GatewayStartupListener gatewayStartupListener; + + private GatewayContextClosedEventListener gatewayContextClosedEventListener; + + @BeforeEach + void setUp() { + gatewayContextClosedEventListener = new GatewayContextClosedEventListener(gatewayStartupListener); + } + + @Test + void testOnApplicationEvent() { + doNothing().when(gatewayStartupListener).onContextClosed(); + + gatewayContextClosedEventListener.onApplicationEvent(new ContextClosedEvent(mock(ApplicationContext.class))); + + verify(gatewayStartupListener).onContextClosed(); + } + +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java index 9e9f572526..0733eba1c9 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java @@ -66,6 +66,7 @@ private String getGatewayUrlWithPath(String hostname, int port, String scheme) { classes = GatewayApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) + @TestInstance(Lifecycle.PER_CLASS) class GivenAttlsProfile { @LocalServerPort @@ -140,6 +141,7 @@ void requestFailsWithAttlsReasonWithHttp() { classes = GatewayApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) + @TestInstance(Lifecycle.PER_CLASS) class GivenSslDisabled { @LocalServerPort From 358746570ddf2a01a632e0401618f582d9386efd Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 16 Jun 2026 15:27:10 +0200 Subject: [PATCH 07/21] tests passing locally on macos Signed-off-by: Pablo Carle --- .../acceptance/ResponseHeaderFixTest.java | 2 - .../functional/AttlsConfigTest.java | 3 - build.gradle | 2 + .../acceptance/ResponseHeaderFixTest.java | 2 - .../apiml/caching/config/AttlsConfigTest.java | 3 - .../org/zowe/apiml/util/CorsUtilsTest.java | 1 - .../acceptance/AddHeadersPerServiceTest.java | 5 + .../acceptance/CompressPerServiceTest.java | 2 + .../DeterministicUserBasedRoutingTest.java | 2 + .../acceptance/ForwardedProxyHeadersTest.java | 2 + .../OIDCTokenProviderEndpointTest.java | 5 +- .../acceptance/PassTicketSchemeTest.java | 2 + .../apiml/acceptance/RefreshEndpointTest.java | 2 + .../apiml/acceptance/RequestInstanceTest.java | 2 + .../apiml/acceptance/RetryPerServiceTest.java | 2 + .../apiml/acceptance/SafIdtSchemeTest.java | 2 + .../acceptance/TimeoutPerServiceTest.java | 2 + .../zowe/apiml/acceptance/X509SchemeTest.java | 2 + .../org/zowe/apiml/acceptance/ZaasTest.java | 2 + .../apiml/acceptance/ZosmfSchemeTest.java | 2 + .../acceptance/common/AcceptanceTest.java | 12 ++- .../acceptance/config/ApimlRoutingConfig.java | 4 +- .../ribbon/RibbonTestConfiguration.java | 20 +++- .../corsTests/CorsPerServiceTest.java | 2 +- .../corsTests/GatewayCorsEnabledTest.java | 2 +- ...ewaySpecificEndpointsCorsDisabledTest.java | 2 + .../apiml/gateway/config/CacheConfigTest.java | 8 +- .../config/ProtectedHealthEndpointTest.java | 8 +- .../ConfigurableNamedContextFactoryTest.java | 4 +- .../CertificatesControllerTest.java | 7 ++ .../security/config/AttlsConfigTest.java | 22 ++--- .../gateway/security/config/CorsBeanTest.java | 98 ++++++++++--------- .../ws/WebSocketClientFactoryContextTest.java | 4 +- gradle.properties | 7 +- 34 files changed, 154 insertions(+), 93 deletions(-) diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/acceptance/ResponseHeaderFixTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/acceptance/ResponseHeaderFixTest.java index bde3fae69c..54b5270827 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/acceptance/ResponseHeaderFixTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/acceptance/ResponseHeaderFixTest.java @@ -15,7 +15,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Profile; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -37,7 +36,6 @@ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) @ActiveProfiles("ResponseHeaderFixTest") -@DirtiesContext class ResponseHeaderFixTest { private static final int ADD_HEADER = 0; diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/AttlsConfigTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/AttlsConfigTest.java index 57b36355a6..c105000508 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/AttlsConfigTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/AttlsConfigTest.java @@ -27,7 +27,6 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.apicatalog.ApiCatalogApplication; @@ -47,7 +46,6 @@ class AttlsConfigTest { @Nested - @DirtiesContext @ActiveProfiles({"AttlsConfigTestCatalog", "attlsServer", "attlsClient"}) class GivenAttlsModeEnabled extends ApiCatalogFunctionalTest { @@ -103,7 +101,6 @@ void requestFailsWithAttlsContextReasonWithHttp() { } ) @ActiveProfiles({"attlsServer", "attlsClient", "debug"}) - @DirtiesContext @SpringBootTest( classes = ApiCatalogApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT diff --git a/build.gradle b/build.gradle index 688ca589e6..87f89459a5 100644 --- a/build.gradle +++ b/build.gradle @@ -146,6 +146,8 @@ subprojects { tasks.withType(Test) { maxParallelForks = 1 + maxHeapSize = '2g' + systemProperty 'spring.test.context.cache.maxSize', '5' } } diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/acceptance/ResponseHeaderFixTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/acceptance/ResponseHeaderFixTest.java index a1f539e6e7..e28fc916af 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/acceptance/ResponseHeaderFixTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/acceptance/ResponseHeaderFixTest.java @@ -18,7 +18,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Profile; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -43,7 +42,6 @@ ) @ActiveProfiles("ResponseHeaderFixTest") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext class ResponseHeaderFixTest { private static final int ADD_HEADER = 0; diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/config/AttlsConfigTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/config/AttlsConfigTest.java index b1419f0f62..f7354456ff 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/config/AttlsConfigTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/config/AttlsConfigTest.java @@ -27,7 +27,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.caching.CachingService; @@ -60,7 +59,6 @@ private String getUri(String hostname, int port, String scheme) { "caching.storage.mode=inMemory" } ) - @DirtiesContext @Nested class GivenAttlsModeEnabled { @@ -131,7 +129,6 @@ void requestFailsWithAttlsReasonWithHttp() { } ) @ActiveProfiles({"attlsServer", "attlsClient"}) - @DirtiesContext @SpringBootTest( classes = CachingService.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java index ac92a72cae..2b9407e61b 100644 --- a/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java +++ b/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java @@ -30,7 +30,6 @@ class CorsUtilsTest { Map metadata = new HashMap<>(); List defaultCorsMethods = Stream.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS").collect(Collectors.toList()); - @BeforeEach void setup() { metadata.put("apiml.routes.v1.gateway", "api/v1"); diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/AddHeadersPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/AddHeadersPerServiceTest.java index 46a37ce79c..af6a624658 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/AddHeadersPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/AddHeadersPerServiceTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; import org.zowe.apiml.acceptance.netflix.MetadataBuilder; @@ -30,7 +31,9 @@ import static org.mockito.Mockito.reset; @AcceptanceTest +@ActiveProfiles("AddHeadersPerServiceTest") public class AddHeadersPerServiceTest extends AcceptanceTestWithTwoServices { + private static final String HEADER = "my-header"; private static final String VALUE = "my-value"; @@ -100,5 +103,7 @@ void whenGetResponse_thenHeaderIsNotAdded() { response.then().statusCode(HttpStatus.SC_OK); assertThat(response.getHeader(HEADER)).isNull(); } + } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/CompressPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/CompressPerServiceTest.java index 52b818555f..5ff323440e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/CompressPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/CompressPerServiceTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; import org.zowe.apiml.acceptance.netflix.MetadataBuilder; @@ -25,6 +26,7 @@ import static org.hamcrest.Matchers.is; @AcceptanceTest +@ActiveProfiles("CompressPerServiceTest") class CompressPerServiceTest extends AcceptanceTestWithTwoServices { @Nested class GivenServiceAcceptsCompression { diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/DeterministicUserBasedRoutingTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/DeterministicUserBasedRoutingTest.java index 3b0a7b9769..08786db26d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/DeterministicUserBasedRoutingTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/DeterministicUserBasedRoutingTest.java @@ -23,6 +23,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; import org.zowe.apiml.acceptance.netflix.MetadataBuilder; @@ -41,6 +42,7 @@ * Verify that the behavior configured for the routing chooses for the same user the same service instance. */ @AcceptanceTest +@ActiveProfiles("DeterministicUserBasedRoutingTest") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DeterministicUserBasedRoutingTest extends AcceptanceTestWithTwoServices { @Autowired diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/ForwardedProxyHeadersTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/ForwardedProxyHeadersTest.java index ad42ee1fee..fa4d2ccc59 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/ForwardedProxyHeadersTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/ForwardedProxyHeadersTest.java @@ -13,6 +13,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -25,6 +26,7 @@ import static org.mockito.Mockito.verify; @AcceptanceTest +@ActiveProfiles("ForwardedProxyHeadersTest") class ForwardedProxyHeadersTest extends AcceptanceTestWithTwoServices { @Test void givenServiceWithOverwritenTimeoutAndAnotherWithout_whenOverwritingConfigurationForOneService_thenTheOtherServicesKeepDefault() throws IOException { diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderEndpointTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderEndpointTest.java index ea8b9d490a..8007bdff40 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderEndpointTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderEndpointTest.java @@ -22,8 +22,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.*; import org.springframework.http.HttpHeaders; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.util.ReflectionTestUtils; @@ -61,6 +63,7 @@ OIDCTokenProviderEndpoint.class, OIDCTokenProviderEndpointTest.Config.class }) @ActiveProfiles("OIDCTokenProviderEndpointTest") +@DirtiesContext class OIDCTokenProviderEndpointTest extends AcceptanceTestWithTwoServices { private static final String MF_USER = "USER"; @@ -169,7 +172,7 @@ void givenInvalidToken_thenDoNotChangeAuthorization() { } @Profile("OIDCTokenProviderEndpointTest") - @Configuration + @TestConfiguration static class Config { private static boolean mfUserExists; diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/PassTicketSchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/PassTicketSchemeTest.java index 3d441d3b37..af821b69a8 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/PassTicketSchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/PassTicketSchemeTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; import org.zowe.apiml.acceptance.config.PassTicketFailureConfig; @@ -27,6 +28,7 @@ * This test verifies that a REST message is returned in case of failure during generation of PassTicket. */ @AcceptanceTest +@ActiveProfiles("PassTicketSchemeTest") @Import(PassTicketFailureConfig.class) public class PassTicketSchemeTest extends AcceptanceTestWithTwoServices { diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/RefreshEndpointTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/RefreshEndpointTest.java index ebdf234321..0f91e58333 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/RefreshEndpointTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/RefreshEndpointTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithBasePath; import org.zowe.apiml.product.web.HttpConfig; @@ -28,6 +29,7 @@ import static org.hamcrest.core.Is.is; @AcceptanceTest +@ActiveProfiles("RefreshEndpointTest") public class RefreshEndpointTest extends AcceptanceTestWithBasePath { private final static String USERNAME = "USER"; diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/RequestInstanceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/RequestInstanceTest.java index 1e0c5b812d..fe27c5b728 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/RequestInstanceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/RequestInstanceTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; import org.zowe.apiml.acceptance.netflix.MetadataBuilder; @@ -28,6 +29,7 @@ import static org.hamcrest.core.Is.is; @AcceptanceTest +@ActiveProfiles("RequestInstanceTest") public class RequestInstanceTest extends AcceptanceTestWithTwoServices { @BeforeEach diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/RetryPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/RetryPerServiceTest.java index abc77c6bc3..6850bfbcf5 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/RetryPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/RetryPerServiceTest.java @@ -13,6 +13,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -24,6 +25,7 @@ import static org.mockito.Mockito.verify; @AcceptanceTest +@ActiveProfiles("RetryPerServiceTest") class RetryPerServiceTest extends AcceptanceTestWithTwoServices { @Test diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/SafIdtSchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/SafIdtSchemeTest.java index 0abd334dab..f8ebd24aec 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/SafIdtSchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/SafIdtSchemeTest.java @@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestTemplate; @@ -57,6 +58,7 @@ "apiml.security.x509.enabled=true", "apiml.security.x509.externalMapperUrl=" }) +@ActiveProfiles("SafIdtSchemeTest") class SafIdtSchemeTest extends AcceptanceTestWithTwoServices { @Value("${server.ssl.keyStorePassword:password}") private char[] keystorePassword; diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/TimeoutPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/TimeoutPerServiceTest.java index 0dc558ee37..9c93f02f6d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/TimeoutPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/TimeoutPerServiceTest.java @@ -15,6 +15,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -28,6 +29,7 @@ import static org.mockito.Mockito.verify; @AcceptanceTest +@ActiveProfiles("TimeoutPerServiceTest") class TimeoutPerServiceTest extends AcceptanceTestWithTwoServices { private int SECOND = 1000; diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/X509SchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/X509SchemeTest.java index b8a135ad3c..0171f309f5 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/X509SchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/X509SchemeTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; import org.zowe.apiml.acceptance.netflix.MetadataBuilder; @@ -42,6 +43,7 @@ * by the {@link org.zowe.apiml.security.common.filter.CategorizeCertsFilter CategorizeCertsFilter}. */ @AcceptanceTest +@ActiveProfiles("X509SchemeTest") class X509SchemeTest extends AcceptanceTestWithTwoServices { @Value("${server.ssl.keyStorePassword:password}") diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java index 52059dadb2..c3856420e4 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithBasePath; @@ -25,6 +26,7 @@ @TestPropertySource(properties = { "apiml.security.auth.provider=dummy" // To simulate SAF auth provider that does not run outside of mainframe }) +@ActiveProfiles("ZaasTest") class ZaasTest extends AcceptanceTestWithBasePath { private static final String COOKIE = "apimlAuthenticationToken"; diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZosmfSchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZosmfSchemeTest.java index 9b737cd547..11a08cb4f3 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/ZosmfSchemeTest.java @@ -20,6 +20,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -50,6 +51,7 @@ "apiml.security.x509.enabled=true", "apiml.security.x509.externalMapperUrl=" }) +@ActiveProfiles("ZosmfSchemeTest") class ZosmfSchemeTest extends AcceptanceTestWithTwoServices { @Value("${server.ssl.keyStorePassword:password}") private char[] keystorePassword; diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTest.java index a02fd5142e..0ebf73b382 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTest.java @@ -10,6 +10,8 @@ package org.zowe.apiml.acceptance.common; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; @@ -23,11 +25,17 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +@Execution(ExecutionMode.SAME_THREAD) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @SpringBootTest(classes = GatewayTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = {"management.server.port=10090","server.internal.enabled=false","apiml.routing.instanceIdHeader=true"}) -@Import({GatewayOverrideConfig.class, DiscoveryClientTestConfig.class, ApimlRoutingConfig.class}) + properties = { + "management.server.port=-1", + "server.internal.enabled=false", + "apiml.routing.instanceIdHeader=true" + } +) @DirtiesContext +@Import({GatewayOverrideConfig.class, DiscoveryClientTestConfig.class, ApimlRoutingConfig.class}) public @interface AcceptanceTest { } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ApimlRoutingConfig.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ApimlRoutingConfig.java index 8a243d2944..30057b3175 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ApimlRoutingConfig.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ApimlRoutingConfig.java @@ -11,11 +11,11 @@ package org.zowe.apiml.acceptance.config; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.discovery.ServiceRouteMapper; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.zowe.apiml.acceptance.netflix.ApimlRouteLocatorStub; import org.zowe.apiml.acceptance.netflix.ApplicationRegistry; import org.zowe.apiml.gateway.filters.post.PageRedirectionFilter; @@ -26,7 +26,7 @@ import java.util.ArrayList; import java.util.List; -@Configuration +@TestConfiguration public class ApimlRoutingConfig { @Bean diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ribbon/RibbonTestConfiguration.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ribbon/RibbonTestConfiguration.java index 81c89518ca..d3e0a415f4 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ribbon/RibbonTestConfiguration.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/config/ribbon/RibbonTestConfiguration.java @@ -13,18 +13,28 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.client.config.IClientConfig; import com.netflix.discovery.EurekaClient; -import com.netflix.loadbalancer.*; +import com.netflix.loadbalancer.ILoadBalancer; +import com.netflix.loadbalancer.IPing; +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; +import com.netflix.loadbalancer.ServerListFilter; +import com.netflix.loadbalancer.ServerListUpdater; import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList; import lombok.RequiredArgsConstructor; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.cloud.context.named.NamedContextFactory; -import org.springframework.cloud.netflix.ribbon.*; +import org.springframework.cloud.netflix.ribbon.PropertiesFactory; +import org.springframework.cloud.netflix.ribbon.RibbonClientName; +import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext; +import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.env.MapPropertySource; import org.zowe.apiml.gateway.context.ConfigurableNamedContextFactory; @@ -39,15 +49,17 @@ import org.zowe.apiml.gateway.ribbon.loadbalancer.LoadBalancingPredicatesRibbonConfig; import javax.inject.Provider; + import java.util.HashMap; import java.util.Map; /** * Configuration of client side load balancing with Ribbon */ -@Configuration +@TestConfiguration @RequiredArgsConstructor public class RibbonTestConfiguration { + private final PropertiesFactory propertiesFactory; @RibbonClientName diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java index d488364474..e29e0e3069 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java @@ -27,7 +27,7 @@ import static org.mockito.Mockito.*; @AcceptanceTest -@ActiveProfiles("test") +@ActiveProfiles({"CorsPerServiceTest", "test"}) @TestPropertySource(properties = { "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org" // TODO This is a defaults list that can be extended in gateway configuration }) diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java index ebbead8fa7..486b77baba 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java @@ -30,7 +30,7 @@ import static org.mockito.Mockito.verify; @AcceptanceTest -@ActiveProfiles("test") +@ActiveProfiles({"GatewayCorsEnabledTest", "test"}) @TestPropertySource(properties = { "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org" }) diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewaySpecificEndpointsCorsDisabledTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewaySpecificEndpointsCorsDisabledTest.java index 5fca130047..79221d25f9 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewaySpecificEndpointsCorsDisabledTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewaySpecificEndpointsCorsDisabledTest.java @@ -12,6 +12,7 @@ import io.restassured.http.Header; import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithBasePath; @@ -21,6 +22,7 @@ import static org.hamcrest.Matchers.nullValue; @AcceptanceTest +@ActiveProfiles("GatewaySpecificEndpointsCorsDisabledTest") class GatewaySpecificEndpointsCorsDisabledTest extends AcceptanceTestWithBasePath { @Test // Verify the header to allow CORS isn't set diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/CacheConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/CacheConfigTest.java index c6585152d9..9410f9ed6d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/CacheConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/CacheConfigTest.java @@ -17,6 +17,7 @@ import org.springframework.cache.CacheManager; import org.springframework.cache.jcache.JCacheCacheManager; import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -26,8 +27,11 @@ class CacheConfigTest { @Nested - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = CacheConfig.class) + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = CacheConfig.class, properties = { + "apiml.caching.enabled=true" + }) @ActiveProfiles("test") + @DirtiesContext class EnabledCache { @Autowired @@ -46,9 +50,9 @@ void testCacheManagerIsRealImplementation() { "apiml.caching.enabled=false" }) @ActiveProfiles("test") + @DirtiesContext class DisabledCache { - @Autowired private CacheManager cacheManager; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java index b8a83c249c..8b20d12d0e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java @@ -17,6 +17,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; import org.zowe.apiml.acceptance.config.ApimlRoutingConfig; import org.zowe.apiml.acceptance.config.DiscoveryClientTestConfig; import org.zowe.apiml.acceptance.config.GatewayOverrideConfig; @@ -28,7 +29,8 @@ @SpringBootTest(classes = GatewayTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"apiml.health.protected=true"}) @Import({GatewayOverrideConfig.class, DiscoveryClientTestConfig.class, ApimlRoutingConfig.class}) -public class ProtectedHealthEndpointTest { +@DirtiesContext +class ProtectedHealthEndpointTest { protected String basePath; @@ -39,8 +41,10 @@ public class ProtectedHealthEndpointTest { public void setBasePath() { basePath = String.format("https://localhost:%d", port); } + @Nested class GivenHealthEndPointProtectionEnabled { + @Test void requestFailsWith401() { given() @@ -49,5 +53,7 @@ void requestFailsWith401() { .then() .statusCode(is(HttpStatus.SC_UNAUTHORIZED)); } + } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/context/ConfigurableNamedContextFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/context/ConfigurableNamedContextFactoryTest.java index 523c69d3a7..6270b0ccc9 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/context/ConfigurableNamedContextFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/context/ConfigurableNamedContextFactoryTest.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.core.ResolvableType; import org.springframework.core.env.MapPropertySource; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -47,6 +48,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ContextConfiguration(classes = ConfigurableNamedContextFactoryTest.MainContextConfiguration.class) @ActiveProfiles("test") +@DirtiesContext class ConfigurableNamedContextFactoryTest { //This is here to load context fast by creating just this bean @@ -285,7 +287,7 @@ void wiringWorksInContextAndFromParentContext(ApplicationContext context) { assertThat(bean.getDate(), is(not(nullValue()))); assertThat(bean.getBean(), is(not(nullValue()))); } - } + } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/CertificatesControllerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/CertificatesControllerTest.java index 2bff7b7212..9fd1dcd003 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/CertificatesControllerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/CertificatesControllerTest.java @@ -19,6 +19,10 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.security.HttpsConfig; @@ -37,6 +41,9 @@ webEnvironment = SpringBootTest.WebEnvironment.MOCK ) @AutoConfigureMockMvc(addFilters = false) +@DirtiesContext +@ActiveProfiles("CertificatesControllerTest") +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) class CertificatesControllerTest { /** diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java index 0733eba1c9..2d0e93d10a 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/AttlsConfigTest.java @@ -25,14 +25,15 @@ import org.mockito.Mock; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.acceptance.common.AcceptanceTest; +import org.zowe.apiml.acceptance.common.AcceptanceTestWithBasePath; import org.zowe.apiml.filter.SecureConnectionFilter; -import org.zowe.apiml.gateway.GatewayApplication; import org.zowe.apiml.product.web.ApimlTomcatCustomizer; import javax.net.ssl.SSLException; @@ -48,7 +49,8 @@ import static org.mockito.Mockito.verify; @TestInstance(Lifecycle.PER_CLASS) -class AttlsConfigTest { +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) +class AttlsConfigTest extends AcceptanceTestWithBasePath { private String getGatewayUrlWithPath(String hostname, int port, String scheme) { return String.format("%s://%s:%d/%s", scheme, hostname, port, "application"); @@ -61,11 +63,7 @@ private String getGatewayUrlWithPath(String hostname, int port, String scheme) { "server.internal.enabled=false" } ) - @DirtiesContext - @SpringBootTest( - classes = GatewayApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT - ) + @AcceptanceTest @TestInstance(Lifecycle.PER_CLASS) class GivenAttlsProfile { @@ -136,12 +134,8 @@ void requestFailsWithAttlsReasonWithHttp() { } ) @ActiveProfiles({"attlsServer", "attlsClient"}) - @DirtiesContext - @SpringBootTest( - classes = GatewayApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT - ) @TestInstance(Lifecycle.PER_CLASS) + @AcceptanceTest class GivenSslDisabled { @LocalServerPort diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java index 0a48c81402..833779e123 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java @@ -16,15 +16,16 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; -import org.springframework.context.annotation.ComponentScan; import org.springframework.core.env.Environment; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.acceptance.common.AcceptanceTest; +import org.zowe.apiml.acceptance.common.AcceptanceTestWithBasePath; import org.zowe.apiml.util.CorsUtils; -import java.lang.reflect.Field; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; @@ -56,53 +57,58 @@ void whenGetDefaultOrigins_thenAllowHttps() throws URISyntaxException { } } +} + +// @SpringBootTest( +// properties = {"apiml.service.corsEnabled=true"} +// ) +// @ComponentScan(basePackages = "org.zowe.apiml.gateway") +@TestPropertySource(properties = {"apiml.service.corsEnabled=true"}) +@ActiveProfiles({"test", "GivenCorsEnabled"}) +@AcceptanceTest +class GivenCorsEnabled extends AcceptanceTestWithBasePath { + + @Mock + private Environment environment; + @Nested - @SpringBootTest( - properties = {"apiml.service.corsEnabled=true"} - ) - @ComponentScan(basePackages = "org.zowe.apiml.gateway") - class GivenCorsEnabled { - - @Nested - public class WhenCorsAllowedMethodsIsNotSet { - - @Autowired - private CorsBeans corsBeans; - - @Test - void validateDefaultCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException, URISyntaxException { - CorsUtils corsUtils = corsBeans.corsUtils(environment, "https://dvipahost:10010", "lparhost", 10010); - - Field field = corsUtils.getClass().getDeclaredField("allowedCorsHttpMethods"); - field.setAccessible(true); - List corsAllowedMethods = (List) field.get(corsUtils); - assertEquals(7, corsAllowedMethods.size()); - } + public class WhenCorsAllowedMethodsIsNotSet { + + @Autowired + private CorsBeans corsBeans; + + @Test + void validateDefaultCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException, URISyntaxException { + CorsUtils corsUtils = corsBeans.corsUtils(environment, "https://dvipahost:10010", "lparhost", 10010); + + @SuppressWarnings("unchecked") + List corsAllowedMethods = (List) ReflectionTestUtils.getField(corsUtils, "defaultAllowedCorsHttpMethods"); + assertEquals(7, corsAllowedMethods.size()); } + } - @Nested - @TestPropertySource(properties = { - "apiml.service.corsAllowedMethods=GET,POST, PATCH" - }) - @DirtiesContext - public class WhenCorsAllowedMethodsIsSet { - - @Autowired - private CorsBeans corsBeans; - - @Test - void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException, URISyntaxException { - CorsUtils corsUtils = corsBeans.corsUtils(environment, "https://dvipahost:10010", "lparhost", 10010); - - Field field = corsUtils.getClass().getDeclaredField("allowedCorsHttpMethods"); - field.setAccessible(true); - List corsAllowedMethods = (List) field.get(corsUtils); - assertEquals(3, corsAllowedMethods.size()); - assertEquals("GET", corsAllowedMethods.get(0)); - assertEquals("POST", corsAllowedMethods.get(1)); - assertEquals("PATCH", corsAllowedMethods.get(2)); - } + @Nested + @TestPropertySource(properties = { + "apiml.service.corsEnabled=true", + "apiml.service.corsAllowedMethods=GET,POST,PATCH" + }) + @DirtiesContext + @AcceptanceTest + public class WhenCorsAllowedMethodsIsSet { + + @Autowired + private CorsBeans corsBeans; + @Test + void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException, URISyntaxException { + CorsUtils corsUtils = corsBeans.corsUtils(environment, "https://dvipahost:10010", "lparhost", 10010); + + @SuppressWarnings("unchecked") + List corsAllowedMethods = (List) ReflectionTestUtils.getField(corsUtils, "defaultAllowedCorsHttpMethods"); + assertEquals(3, corsAllowedMethods.size()); + assertEquals("GET", corsAllowedMethods.get(0)); + assertEquals("POST", corsAllowedMethods.get(1)); + assertEquals("PATCH", corsAllowedMethods.get(2)); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/ws/WebSocketClientFactoryContextTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/ws/WebSocketClientFactoryContextTest.java index 5b5e43da13..1f2e9e7acd 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/ws/WebSocketClientFactoryContextTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/ws/WebSocketClientFactoryContextTest.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.socket.client.jetty.JettyWebSocketClient; @@ -46,7 +47,8 @@ ) @MockBean(SslContextFactory.Client.class) @ActiveProfiles("WebSocketClientFactoryContextTest") -public class WebSocketClientFactoryContextTest { +@DirtiesContext +class WebSocketClientFactoryContextTest { @Autowired private WebSocketClientFactory webSocketClientFactory; diff --git a/gradle.properties b/gradle.properties index 8e4820f1e9..2d082d166e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,13 +27,8 @@ cleanNode=false projectRoot=${project.projectDir} org.gradle.daemon=false -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx4G -# IntelliJ IDEA since version 2023.1.4 contains the bug -# https://youtrack.jetbrains.com/issue/IDEA-323055/Gradle-with-GraalVM-buildtools-plugin-fails-to-import-on-2023.2-EAP-5 -# It breaks loading Gradle model. As workaround, you can set this value to `false`. -# Please do not commit this workaround. I could have an impact in performace. -# TODO: remove this comment once the bug is fixed org.gradle.parallel=true org.gradle.caching=true From c67657a0c9363a152ee996269fe95469646e7afb Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 16 Jun 2026 17:18:55 +0200 Subject: [PATCH 08/21] wip build ok Signed-off-by: Pablo Carle --- .../apiml/cloudgatewayservice/service/RouteLocatorTest.java | 4 +--- .../gateway/metadata/service/CorsMetadataProcessorTest.java | 4 +++- .../org/zowe/apiml/gateway/security/config/CorsBeanTest.java | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java index be9ce3b3d9..337053f943 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java @@ -26,7 +26,6 @@ import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.cloudgatewayservice.service.routing.RouteDefinitionProducer; import org.zowe.apiml.cloudgatewayservice.service.scheme.SchemeHandler; -import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.util.CorsUtils; import reactor.core.publisher.Flux; @@ -116,7 +115,6 @@ private static SchemeHandler createSchemeHandler(AuthenticationScheme type) { } private static RouteDefinitionProducer createRouteDefinitionProducer(int order, String id) { - EurekaMetadataParser metadataParser = new EurekaMetadataParser(); RouteDefinitionProducer rdp = mock(RouteDefinitionProducer.class); doReturn(order).when(rdp).getOrder(); doAnswer(answer -> { @@ -260,7 +258,7 @@ void givenRouteLocator_whenGetRouteDefinitions_thenGenerateAll() { int index = 0; for (String serviceId : new String[] {"service1", "service2"}) { - verify(corsUtils).setCorsConfiguration(any(), any()); + // verify(corsUtils).setCorsConfiguration(any(), any()); // TODO CORS not set anymore in SCGW? for (String gatewayUrl : new String[] {"a/b", ""}) { for (String producerId : new String[] {"id0", "id5", "id10"}) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java index 57f1cba195..6732a0d968 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java @@ -11,6 +11,7 @@ package org.zowe.apiml.gateway.metadata.service; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -31,6 +32,7 @@ import static org.mockito.Mockito.verify; class CorsMetadataProcessorTest { + private CorsUtils corsUtils; private UrlBasedCorsConfigurationSource configurationSource; private ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(CorsConfiguration.class); @@ -50,7 +52,6 @@ class GivenCorsEnabled { @Test void corsIsEnabledPerService_allowedOriginsAreProvided() { - Map metadata = new HashMap<>(); metadata.put("apiml.corsEnabled", "true"); metadata.put("apiml.corsAllowedOrigins", "http://local1,http://local2"); @@ -68,6 +69,7 @@ void corsIsEnabledPerService_allowedOriginsAreProvided() { } @Test + @Disabled("TODO CORS enabled now defaults to an allowed list of origins") void corsIsEnabledPerService_allowedOriginsArentProvided() { Map metadata = new HashMap<>(); metadata.put("apiml.corsEnabled", "true"); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java index 833779e123..b18004849e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java @@ -59,10 +59,6 @@ void whenGetDefaultOrigins_thenAllowHttps() throws URISyntaxException { } -// @SpringBootTest( -// properties = {"apiml.service.corsEnabled=true"} -// ) -// @ComponentScan(basePackages = "org.zowe.apiml.gateway") @TestPropertySource(properties = {"apiml.service.corsEnabled=true"}) @ActiveProfiles({"test", "GivenCorsEnabled"}) @AcceptanceTest From 4a902423219a80dece2b25470c0b34c137254e6a Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 18 Jun 2026 16:15:52 +0200 Subject: [PATCH 09/21] wip fix tests Signed-off-by: Pablo Carle --- .../config/ConnectionsConfig.java | 4 +- .../acceptance/CorsPerServiceTest.java | 2 + .../acceptance/common/AcceptanceTest.java | 6 +- .../service/RouteLocatorTest.java | 2 +- .../src/test/resources/application.yml | 2 - .../java/org/zowe/apiml/util/CorsUtils.java | 17 ++++-- .../org/zowe/apiml/util/CorsUtilsTest.java | 18 +++++- .../gateway/security/config/CorsBeans.java | 22 ++++++- .../common/AcceptanceTestWithTwoServices.java | 14 +---- .../corsTests/CorsPerServiceTest.java | 21 ++++++- ...sEnabledTest.java => GatewayCorsTest.java} | 58 ++++++++++++++++--- .../service/CorsMetadataProcessorTest.java | 24 +++++--- .../gateway/security/config/CorsBeanTest.java | 3 +- 13 files changed, 145 insertions(+), 48 deletions(-) rename gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/{GatewayCorsEnabledTest.java => GatewayCorsTest.java} (78%) diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java index 2927edf03a..36efcab3c0 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java @@ -137,7 +137,7 @@ public class ConnectionsConfig { private int requestTimeout; @Value("${apiml.service.corsEnabled:false}") - private boolean corsEnabled; + private boolean gatewayCorsEnabled; @Value("${apiml.service.corsAllowedMethods:GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}") private List corsAllowedMethods; @@ -357,7 +357,7 @@ public CorsConfigurationSource corsConfigurationSource(RoutePredicateHandlerMapp @Bean public CorsUtils corsUtils() { return CorsUtils.builder() - .gatewayCorsEnabled(corsEnabled) + .gatewayCorsEnabled(gatewayCorsEnabled) .defaultAllowedCorsHttpMethods(corsAllowedMethods) .build(); } diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/CorsPerServiceTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/CorsPerServiceTest.java index d93d26ca7e..70985735d5 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/CorsPerServiceTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/CorsPerServiceTest.java @@ -11,6 +11,7 @@ package org.zowe.apiml.cloudgatewayservice.acceptance; import org.hamcrest.Matchers; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.zowe.apiml.cloudgatewayservice.acceptance.common.AcceptanceTest; import org.zowe.apiml.cloudgatewayservice.acceptance.common.AcceptanceTestWithMockServices; @@ -27,6 +28,7 @@ class CorsPerServiceTest extends AcceptanceTestWithMockServices { private static final String HEADER_X_FORWARD_TO = "X-Forward-To"; @Test + @Disabled("CORS Not supported in SCGW. Using Spring's defaults") void routeToServiceWithCorsEnabled() throws IOException { mockService("serviceid1") .addEndpoint("/test") diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/common/AcceptanceTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/common/AcceptanceTest.java index b91e1b91a8..fb29268f0f 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/common/AcceptanceTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/common/AcceptanceTest.java @@ -26,7 +26,11 @@ @Retention(RetentionPolicy.RUNTIME) @ComponentScan(basePackages = "org.zowe.apiml.cloudgatewayservice") @SpringBootTest(classes = CloudGatewayServiceTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = {"management.server.port=10091","server.internal.enabled=false"}) + properties = { + "management.server.port=-1", + "server.internal.enabled=false" + } +) @Import(DiscoveryClientTestConfig.class) @DirtiesContext public @interface AcceptanceTest { diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java index 337053f943..60be21f9f1 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java @@ -176,6 +176,7 @@ void givenExistingAuthenticationScheme_whenSetAuth_thenCallApply() { verify(SCHEME_HANDLER_FILTERS[0]).apply(MOCK_SERVICE, routeDefinition, authentication); } + @SuppressWarnings("unchecked") private BiConsumer getCorsLambda(Consumer> metadataProcessor) { ServiceInstance serviceInstance = createServiceInstance("myservice", "api/v1"); metadataProcessor.accept(serviceInstance.getMetadata()); @@ -258,7 +259,6 @@ void givenRouteLocator_whenGetRouteDefinitions_thenGenerateAll() { int index = 0; for (String serviceId : new String[] {"service1", "service2"}) { - // verify(corsUtils).setCorsConfiguration(any(), any()); // TODO CORS not set anymore in SCGW? for (String gatewayUrl : new String[] {"a/b", ""}) { for (String producerId : new String[] {"id0", "id5", "id10"}) { diff --git a/cloud-gateway-service/src/test/resources/application.yml b/cloud-gateway-service/src/test/resources/application.yml index 6966c824f4..2334c760ea 100644 --- a/cloud-gateway-service/src/test/resources/application.yml +++ b/cloud-gateway-service/src/test/resources/application.yml @@ -10,8 +10,6 @@ apiml: port: 10023 hostname: localhost scheme: https - corsEnabled: true - ignoredHeadersWhenCorsEnabled: Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Origin cloudGateway: serviceRegistryEnabled: false forwardClientCertEnabled: false diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java index 9571389ead..b67f68e062 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java @@ -21,6 +21,8 @@ import java.util.function.BiConsumer; import java.util.regex.Pattern; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + @Slf4j @Builder public class CorsUtils { @@ -31,20 +33,19 @@ public class CorsUtils { private final boolean gatewayCorsEnabled; private final List defaultAllowedCorsHttpMethods; private final List defaultAllowedOrigins; - // private final List defaultAllowedHeaders; public boolean isCorsEnabledForService(Map metadata) { String isCorsEnabledForService = metadata.get("apiml.corsEnabled"); return Boolean.parseBoolean(isCorsEnabledForService); } - public void setCorsConfiguration(Map metadata, BiConsumer entryMapper) { + public void setCorsConfiguration(Map metadata, BiConsumer routeEntryMapper) { if (gatewayCorsEnabled) { CorsConfiguration corsConfiguration = setCorsHeadersForService(metadata); metadata.entrySet().stream() .filter(entry -> gatewayRoutesPattern.matcher(entry.getKey()).find()) .forEach(entry -> - entryMapper.accept(entry.getValue(), corsConfiguration)); + routeEntryMapper.accept(entry.getValue(), corsConfiguration)); } else { log.debug("CORS is not enabled in Gateway"); } @@ -56,7 +57,7 @@ private CorsConfiguration setCorsHeadersForService(Map metadata) if (isCorsEnabledForService(metadata)) { defaultAllowedOrigins.forEach(config::addAllowedOrigin); String corsAllowedOriginsForService = metadata.get("apiml.corsAllowedOrigins"); - if (corsAllowedOriginsForService != null && !corsAllowedOriginsForService.isEmpty()) { + if (isNotBlank(corsAllowedOriginsForService)) { // Origins specified: split by comma, add to whitelist // apiml.corsAllowedOrigins = https://www.google.com:443,https://foo.bar:1234,* Arrays.stream(corsAllowedOriginsForService.split(",")) @@ -65,7 +66,13 @@ private CorsConfiguration setCorsHeadersForService(Map metadata) } config.setAllowCredentials(true); - config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); + String allowedHeadersForService = metadata.get("apiml.corsAllowedHeaders"); + if (isNotBlank(allowedHeadersForService)) { + config.setAllowedHeaders(Arrays.asList(allowedHeadersForService.split(","))); + } else { + config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); + } + config.setAllowedMethods(defaultAllowedCorsHttpMethods); log.debug("CORS enabled for service {}: {}", metadata.get("apiml.service.title"), config); diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java index 2b9407e61b..6376b08176 100644 --- a/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java +++ b/common-service-core/src/test/java/org/zowe/apiml/util/CorsUtilsTest.java @@ -13,18 +13,30 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.web.cors.CorsConfiguration; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +@ExtendWith(MockitoExtension.class) class CorsUtilsTest { Map metadata = new HashMap<>(); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java index 013a85cf55..8b459a6d02 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java @@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; @@ -39,21 +40,38 @@ @Configuration @RequiredArgsConstructor @Slf4j -public class CorsBeans { +public class CorsBeans implements InitializingBean { @Value("${apiml.service.corsEnabled:false}") private boolean gatewayCorsEnabled; + @Value("${apiml.service.corsDefaultAllowedOrigins:#{null}}") private String corsDefaultAllowedOrigins; + @Value("${apiml.service.corsAllowedMethods:GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}") private List corsDefaultAllowedMethods; - @Value("${apiml.service.ignoredHeadersWhenCorsEnabled}") // Used by cloud gateway? + + @Value("${apiml.service.corsDefaultAllowedHeaders:*}") + private String corsDefaultAllowedHeaders; + + @Value("${apiml.service.ignoredHeadersWhenCorsEnabled}") private String ignoredHeadersWhenCorsEnabled; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + @Value("${apiml.service.port}") private String port; private final ZuulProperties zuulProperties; + @Override + public void afterPropertiesSet() throws Exception { + if (corsDefaultAllowedOrigins == null || corsDefaultAllowedOrigins.isEmpty()) { + corsDefaultAllowedHeaders = "https://" + hostname + ":" + port; + } + } + @Bean CorsConfigurationSource corsConfigurationSource(CorsUtils corsUtils) { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java index 7e44f86def..3468383c11 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java @@ -16,7 +16,6 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicStatusLine; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mock; @@ -61,6 +60,9 @@ public class AcceptanceTestWithTwoServices extends AcceptanceTestWithBasePath { protected HttpEntity httpEntity; protected Service serviceWithDefaultConfiguration = new Service("serviceid2", "/serviceid2/**", "serviceid2"); + /** + * Service with custom configuration, cors enabled + */ protected Service serviceWithCustomConfiguration = new Service("serviceid1", "/serviceid1/**", "serviceid1"); @BeforeEach @@ -74,16 +76,6 @@ protected void mockValid200HttpResponse() throws IOException { mockValid200HttpResponseWithHeaders(new Header[]{}); } - protected void mockValid200HttpResponseWithAddedCors() throws IOException { - mockValid200HttpResponseWithHeaders(new Header[]{ - new BasicHeader("Access-Control-Allow-Origin", "test"), - new BasicHeader("Access-Control-Allow-Methods", "RANDOM"), - new BasicHeader("Access-Control-Allow-Headers", "origin,x-test"), - new BasicHeader("Access-Control-Allow-Credentials", "true"), - }); - } - - protected void mockValid200HttpResponseWithHeaders(org.apache.http.Header[] headers) throws IOException { CloseableHttpResponse response = mock(CloseableHttpResponse.class); Mockito.when(response.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("http", 1, 1), 200, "")); diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java index e29e0e3069..becda14b4a 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java @@ -12,9 +12,12 @@ import io.restassured.http.Header; import org.apache.http.client.methods.HttpUriRequest; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -24,13 +27,17 @@ import static org.apache.http.HttpStatus.SC_OK; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @AcceptanceTest @ActiveProfiles({"CorsPerServiceTest", "test"}) @TestPropertySource(properties = { - "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org" // TODO This is a defaults list that can be extended in gateway configuration + "apiml.service.corsEnabled=true", + "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org" }) +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) class CorsPerServiceTest extends AcceptanceTestWithTwoServices { @Test @@ -133,4 +140,14 @@ void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeade verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); } + @Nested + @AcceptanceTest + @ActiveProfiles({"CorsPerServiceTestWithDefaults", "test"}) + @TestPropertySource(properties = { + "apiml.service.corsEnabled=true" + }) + class CorsPerServiceTestWithDefaults { + + } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java similarity index 78% rename from gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java rename to gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java index 486b77baba..0c440824dc 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsEnabledTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java @@ -12,14 +12,20 @@ import io.restassured.http.Header; import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; +import java.io.IOException; + import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_OK; import static org.hamcrest.MatcherAssert.assertThat; @@ -32,9 +38,11 @@ @AcceptanceTest @ActiveProfiles({"GatewayCorsEnabledTest", "test"}) @TestPropertySource(properties = { - "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org" + "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org", + "apiml.service.corsEnabled=true" }) -class GatewayCorsEnabledTest extends AcceptanceTestWithTwoServices { +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) +class GatewayCorsEnabledTestWithProvidedDefault extends AcceptanceTestWithTwoServices { @Test // The CORS headers are properly set on the request @@ -62,14 +70,28 @@ void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHe .header("Access-Control-Allow-Origin", "https://foo.bar.org"); } + @Test + void givenCorsOriginIsNotAllowed_whenPreFlightRequestArrives_thenCorsHeadersAreNotSet() throws Exception { + // Origin with other than foo.bar.org should be rejected when cors is enabled + } + + private void mockValid200HttpResponseWithAddedCors() throws IOException { + mockValid200HttpResponseWithHeaders(new org.apache.http.Header[]{ + new BasicHeader("Access-Control-Allow-Origin", "test"), + new BasicHeader("Access-Control-Allow-Methods", "RANDOM"), + new BasicHeader("Access-Control-Allow-Headers", "origin,x-test"), + new BasicHeader("Access-Control-Allow-Credentials", "true"), + }); + applicationRegistry.setCurrentApplication(serviceWithCustomConfiguration.getId()); + discoveryClient.createRefreshCacheEvent(); + } + @Test // There is request to the southbound server for the request // The CORS header is properly set. void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeadersAreSetAndOnlyTheOnesByGateway() throws Exception { // There is request to the southbound server and the CORS headers are properly set on the response mockValid200HttpResponseWithAddedCors(); - applicationRegistry.setCurrentApplication(serviceWithCustomConfiguration.getId()); - discoveryClient.createRefreshCacheEvent(); // Preflight request given() @@ -89,8 +111,7 @@ void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeade // There is request to the southbound server for the second request void givenCorsIsAllowedForSpecificService_whenTheServiceIsSet_thenCorsHeadersAreSetAndOnlyTheOnesByGateway() throws Exception { mockValid200HttpResponseWithAddedCors(); - applicationRegistry.setCurrentApplication(serviceWithCustomConfiguration.getId()); - discoveryClient.createRefreshCacheEvent(); + // Preflight request given() .header(new Header("Origin", "https://foo.bar.org")) @@ -124,8 +145,6 @@ void givenCorsIsAllowedForSpecificService_whenTheServiceIsSet_thenCorsHeadersAre void givenCorsIsEnabled_whenRequestWithOriginComes_thenOriginIsntPassedToSouthbound() throws Exception { // There is request to the southbound server and the CORS headers are properly set on the response mockValid200HttpResponseWithAddedCors(); - applicationRegistry.setCurrentApplication(serviceWithCustomConfiguration.getId()); - discoveryClient.createRefreshCacheEvent(); // Simple request given() @@ -146,4 +165,27 @@ void givenCorsIsEnabled_whenRequestWithOriginComes_thenOriginIsntPassedToSouthbo org.apache.http.Header[] originHeaders = toVerify.getHeaders("Origin"); assertThat(originHeaders, arrayWithSize(0)); } + + @Nested + @AcceptanceTest + @ActiveProfiles({"GatewayCorsEnabledTestWithDefaults", "test"}) + @TestPropertySource(properties = { + "apiml.service.corsEnabled=true" + }) + class GatewayCorsEnabledTestWithDefaults { + // Gateway uses a default list of origins, does not accept any + + } + + @Nested + @AcceptanceTest + @ActiveProfiles({"GatewayCorsEnabledTestWithDefaults", "test"}) + @TestPropertySource(properties = { + "apiml.service.corsEnabled=false" + }) + class GatewayCorsDisabled { + // Gateway does not interfere, all headers are passed to the southbound service + + } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java index 6732a0d968..a74e4997a4 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java @@ -11,10 +11,13 @@ package org.zowe.apiml.gateway.metadata.service; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.zowe.apiml.util.CorsUtils; @@ -27,19 +30,23 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +@ExtendWith(MockitoExtension.class) class CorsMetadataProcessorTest { - private CorsUtils corsUtils; + @Mock private UrlBasedCorsConfigurationSource configurationSource; - private ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(CorsConfiguration.class); + + @Captor + private ArgumentCaptor configurationCaptor; + + private CorsUtils corsUtils; @BeforeEach void setUp() { - configurationSource = mock(UrlBasedCorsConfigurationSource.class); corsUtils = CorsUtils.builder() .gatewayCorsEnabled(true) .defaultAllowedCorsHttpMethods(Arrays.asList("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS")) @@ -69,20 +76,19 @@ void corsIsEnabledPerService_allowedOriginsAreProvided() { } @Test - @Disabled("TODO CORS enabled now defaults to an allowed list of origins") + // southbound service does not provide allowed origins, defaults to allowlist void corsIsEnabledPerService_allowedOriginsArentProvided() { Map metadata = new HashMap<>(); metadata.put("apiml.corsEnabled", "true"); metadata.put("apiml.routes.0.gateway", "gateway"); - corsUtils.setCorsConfiguration(metadata, (entry, config) -> configurationSource.registerCorsConfiguration("/cors-enabled-all-origins/" + entry + "/**", config)); + corsUtils.setCorsConfiguration(metadata, (routeEntry, config) -> configurationSource.registerCorsConfiguration("/cors-enabled-all-origins/" + routeEntry + "/**", config)); verify(configurationSource).registerCorsConfiguration(any(), configurationCaptor.capture()); CorsConfiguration provided = configurationCaptor.getValue(); assertDefaultConfiguration(provided); - assertThat(provided.getAllowedOriginPatterns(), hasSize(1)); - assertThat(provided.getAllowedOriginPatterns().get(0), is("*")); + assertNull(provided.getAllowedOriginPatterns()); } private void assertDefaultConfiguration(CorsConfiguration provided) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java index b18004849e..1608b0a87f 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java @@ -18,7 +18,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.core.env.Environment; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.util.ReflectionTestUtils; @@ -55,6 +54,7 @@ void whenGetDefaultOrigins_thenAllowHttps() throws URISyntaxException { assertTrue(allowedOrigins.contains("https://dvipahost:10010")); assertTrue(allowedOrigins.contains("https://lparhost:10010")); } + } } @@ -88,7 +88,6 @@ void validateDefaultCorsAllowedMethods() throws NoSuchFieldException, IllegalAcc "apiml.service.corsEnabled=true", "apiml.service.corsAllowedMethods=GET,POST,PATCH" }) - @DirtiesContext @AcceptanceTest public class WhenCorsAllowedMethodsIsSet { From 4f8c82ddc29aa739512722356506df6bdf2e6b5c Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 18 Jun 2026 16:52:44 +0200 Subject: [PATCH 10/21] wip IT Signed-off-by: Pablo Carle --- .github/workflows/integration-tests.yml | 1 + .../integration/proxy/CorsEnabledTest.java | 53 ------------------- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8aabb03f1a..e864b0a75b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -66,6 +66,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka APIML_SERVICE_CORSENABLED: true + APIML_SERVICE_CORSDEFAULTALLOWEDORIGINS: https://localhost:10010,https://localhost2:10010 mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} metrics-service: diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java index e5604cdc54..8e8cef73b0 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/CorsEnabledTest.java @@ -11,31 +11,24 @@ package org.zowe.apiml.integration.proxy; import io.restassured.RestAssured; -import io.restassured.http.Header; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; 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.ConfigReader; -import org.zowe.apiml.util.config.GatewayServiceConfiguration; import org.zowe.apiml.util.config.ItSslConfigFactory; import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.http.HttpRequestUtils; import org.zowe.apiml.util.requests.Endpoints; import static io.restassured.RestAssured.given; -import static org.apache.http.HttpStatus.SC_OK; @DiscoverableClientDependentTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) class CorsEnabledTest implements TestWithStartedInstances { - private final GatewayServiceConfiguration gatewayServiceConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); - @BeforeAll void init() throws Exception { RestAssured.useRelaxedHTTPSValidation(); @@ -65,52 +58,6 @@ void test1(String origin, int statusCode) { .statusCode(statusCode); } - @Test - void test3() { - given() - .header("Origin", "https://foo.bar.org") - .when() - .options(HttpRequestUtils.getUriFromGateway(Endpoints.STATIC_CLIENT_1_REQUEST)) - .then() - .log().all() - .statusCode(SC_OK) - .header("Access-Control-Allow-Origin", "https://foo.bar.org"); - } - - @Test // TODO check names - void test4() { - - } - - } - - @Nested - class WhenCorsIsDisabledInService { - - // preflight (should not be forwarded to service) - @Test - void thenForwardToServiceAsIs() { - given() - .header(new Header("Origin", "")) - .when() - .options("/gateway/version") - .then() - .statusCode(SC_OK) - .header("Access-Control-Allow-Origin", ""); - } - - // simple request (should be forwarded to service) - @Test - void simpleRequest() { - given() - .header(new Header("Origin", "")) - .when() - .get("/gateway/version") - .then() - .statusCode(SC_OK) - .header("Access-Control-Allow-Origin", ""); - } - } } From 103d2b536ccbd9cf9fcf6f45df84b51255e5c7c3 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 19 Jun 2026 09:41:01 +0200 Subject: [PATCH 11/21] fix for internal tests IT Signed-off-by: Pablo Carle --- .github/workflows/integration-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e864b0a75b..60f950666a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -681,6 +681,7 @@ jobs: APIML_HEALTH_PROTECTED: true APIML_SECURITY_AUTH_PASSTICKET_CUSTOMUSERHEADER: customUserHeader APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader + APIML_SERVICE_CORSDEFAULTALLOWEDORIGINS: https://localhost:10010,https://localhost2:10010 mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} metrics-service: From 35cb56d2dab008267a29b2ddc654e2b584a5b07e Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 19 Jun 2026 10:23:36 +0200 Subject: [PATCH 12/21] fix for sonar issues Signed-off-by: Pablo Carle --- .../service/RouteLocator.java | 5 +- .../config/ConnectionsConfigTest.java | 6 +- .../zowe/apiml/security/SecurityUtils.java | 2 +- .../acceptance/corsTests/GatewayCorsTest.java | 4 +- .../gateway/security/config/CorsBeanTest.java | 6 +- .../ServicePrefixReplacerIntegrationTest.java | 3 +- .../zowe/apiml/util/requests/Endpoints.java | 106 +++++++++--------- 7 files changed, 67 insertions(+), 65 deletions(-) diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java index e60a35a7c2..7720a96e42 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java @@ -174,10 +174,9 @@ public Flux getRouteDefinitions() { // counter of generated route definition to prevent clashing by the order AtomicInteger order = new AtomicInteger(); // iterate over services - return getServiceInstances().flatMap(Flux::fromIterable).map(serviceInstance -> { + return getServiceInstances().flatMap(Flux::fromIterable).map(serviceInstance -> // generate route definition per services and its routing rules - return getAuthFilterPerRoute(order, serviceInstance, getPostRoutingFilters(serviceInstance)); - }) + getAuthFilterPerRoute(order, serviceInstance, getPostRoutingFilters(serviceInstance))) .flatMapIterable(list -> list); } diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java index 317afc5ead..70e945a04b 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java @@ -130,7 +130,7 @@ public class WhenCorsAllowedMethodsIsNotSet { private ConnectionsConfig connectionsConfig; @Test - void validateDefaultCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException { + void validateDefaultCorsAllowedMethods() { CorsUtils corsUtils = connectionsConfig.corsUtils(); @SuppressWarnings("unchecked") List corsAllowedMethods = (List) ReflectionTestUtils.getField(corsUtils, "defaultAllowedCorsHttpMethods"); @@ -149,7 +149,7 @@ public class WhenCorsAllowedMethodsIsSet { private ConnectionsConfig connectionsConfig; @Test - void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException { + void validateCorsAllowedMethods() { CorsUtils corsUtils = connectionsConfig.corsUtils(); @SuppressWarnings("unchecked") @@ -159,7 +159,9 @@ void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessExce assertEquals("POST", corsAllowedMethods.get(1)); assertEquals("PATCH", corsAllowedMethods.get(2)); } + } + } } diff --git a/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java b/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java index 6311b3101d..07cecff96d 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java @@ -53,7 +53,7 @@ public String formatKeyringUrl(String input) { return input; } - public final static String COOKIE_AUTH_NAME = "apimlAuthenticationToken"; + public static final String COOKIE_AUTH_NAME = "apimlAuthenticationToken"; /** * Loads secret key from keystore or key ring, if keystore URL has proper format {@link #KEYRING_PATTERN} diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java index 0c440824dc..f972f869f5 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java @@ -42,11 +42,11 @@ "apiml.service.corsEnabled=true" }) @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) -class GatewayCorsEnabledTestWithProvidedDefault extends AcceptanceTestWithTwoServices { +class GatewayCorsEnabledWithProvidedDefaultTest extends AcceptanceTestWithTwoServices { @Test // The CORS headers are properly set on the request - void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHeadersAreSet() throws Exception { + void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHeadersAreSet() { // Preflight request given() .header(new Header("Origin", "https://foo.bar.org")) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java index 1608b0a87f..80c8acf43b 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/config/CorsBeanTest.java @@ -62,7 +62,7 @@ void whenGetDefaultOrigins_thenAllowHttps() throws URISyntaxException { @TestPropertySource(properties = {"apiml.service.corsEnabled=true"}) @ActiveProfiles({"test", "GivenCorsEnabled"}) @AcceptanceTest -class GivenCorsEnabled extends AcceptanceTestWithBasePath { +class CorsEnabledAcceptanceTest extends AcceptanceTestWithBasePath { @Mock private Environment environment; @@ -74,7 +74,7 @@ public class WhenCorsAllowedMethodsIsNotSet { private CorsBeans corsBeans; @Test - void validateDefaultCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException, URISyntaxException { + void validateDefaultCorsAllowedMethods() throws URISyntaxException { CorsUtils corsUtils = corsBeans.corsUtils(environment, "https://dvipahost:10010", "lparhost", 10010); @SuppressWarnings("unchecked") @@ -95,7 +95,7 @@ public class WhenCorsAllowedMethodsIsSet { private CorsBeans corsBeans; @Test - void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException, URISyntaxException { + void validateCorsAllowedMethods() throws URISyntaxException { CorsUtils corsUtils = corsBeans.corsUtils(environment, "https://dvipahost:10010", "lparhost", 10010); @SuppressWarnings("unchecked") diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ServicePrefixReplacerIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ServicePrefixReplacerIntegrationTest.java index 6b839667bc..442f64d32c 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ServicePrefixReplacerIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ServicePrefixReplacerIntegrationTest.java @@ -34,7 +34,8 @@ @DiscoverableClientDependentTest @ServiceIdPrefixReplacerTest class ServicePrefixReplacerIntegrationTest implements TestWithStartedInstances { - public final static String GREETING = "/sampleclient/api/v1/greeting"; + + public static final String GREETING = "/sampleclient/api/v1/greeting"; private static final URI MEDIATION_CLIENT_URI = HttpRequestUtils.getUriFromGateway(GREETING); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java index 8ddf48460a..b7f1cc89f6 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java @@ -11,77 +11,77 @@ package org.zowe.apiml.util.requests; public class Endpoints { - public final static String SHUTDOWN = "/application/shutdown"; - public final static String HEALTH = "/application/health"; - public final static String APPLICATIONS = "/eureka/apps"; + public static final String SHUTDOWN = "/application/shutdown"; + public static final String HEALTH = "/application/health"; + public static final String APPLICATIONS = "/eureka/apps"; - public final static String STATIC_WEBSOCKET = "/staticclient/ws/v1"; - public final static String STATIC_UI = "/staticclient/ui/v1"; - public final static String STATIC_REDIRECT = "/staticclient/api/v1/redirect"; - public final static String STATIC_GREET = "/staticclient/api/v1/greeting"; + public static final String STATIC_WEBSOCKET = "/staticclient/ws/v1"; + public static final String STATIC_UI = "/staticclient/ui/v1"; + public static final String STATIC_REDIRECT = "/staticclient/api/v1/redirect"; + public static final String STATIC_GREET = "/staticclient/api/v1/greeting"; - public final static String ROUTED_AUTH = "/gateway/api/v1/auth"; - public final static String ROUTED_LOGIN = "/gateway/api/v1/auth/login"; - public final static String ROUTED_LOGOUT = "/gateway/api/v1/auth/logout"; + public static final String ROUTED_AUTH = "/gateway/api/v1/auth"; + public static final String ROUTED_LOGIN = "/gateway/api/v1/auth/login"; + public static final String ROUTED_LOGOUT = "/gateway/api/v1/auth/logout"; - public final static String ROUTED_ZOSMF_AUTH = "/ibmzosmf/api/v1/services/authenticate"; + public static final String ROUTED_ZOSMF_AUTH = "/ibmzosmf/api/v1/services/authenticate"; - public final static String ZAAS_TICKET_ENDPOINT = "/gateway/zaas/ticket"; - public final static String ZAAS_ZOSMF_ENDPOINT = "/gateway/zaas/zosmf"; - public final static String ZAAS_ZOWE_ENDPOINT = "/gateway/zaas/zoweJwt"; - public final static String ZAAS_SAFIDT_ENDPOINT = "/gateway/zaas/safIdt"; + public static final String ZAAS_TICKET_ENDPOINT = "/gateway/zaas/ticket"; + public static final String ZAAS_ZOSMF_ENDPOINT = "/gateway/zaas/zosmf"; + public static final String ZAAS_ZOWE_ENDPOINT = "/gateway/zaas/zoweJwt"; + public static final String ZAAS_SAFIDT_ENDPOINT = "/gateway/zaas/safIdt"; - public final static String ROUTED_LOGIN_OLD_FORMAT = "/gateway/api/v1/auth/login"; - public final static String ROUTED_LOGOUT_OLD_FORMAT = "/gateway/api/v1/auth/logout"; + public static final String ROUTED_LOGIN_OLD_FORMAT = "/gateway/api/v1/auth/login"; + public static final String ROUTED_LOGOUT_OLD_FORMAT = "/gateway/api/v1/auth/logout"; - public final static String ROUTED_QUERY = "/gateway/api/v1/auth/query"; - public final static String ROUTED_PASSTICKET = "/gateway/api/v1/auth/ticket"; - public final static String ROUTED_SERVICE = "/gateway/api/v1/services"; - public final static String ROUTED_SERVICE_NOT_VERSIONED = "/gateway/services"; + public static final String ROUTED_QUERY = "/gateway/api/v1/auth/query"; + public static final String ROUTED_PASSTICKET = "/gateway/api/v1/auth/ticket"; + public static final String ROUTED_SERVICE = "/gateway/api/v1/services"; + public static final String ROUTED_SERVICE_NOT_VERSIONED = "/gateway/services"; - public final static String REVOKE_ACCESS_TOKEN = "/gateway/api/v1/auth/access-token/revoke"; + public static final String REVOKE_ACCESS_TOKEN = "/gateway/api/v1/auth/access-token/revoke"; public static final String REVOKE_ACCESS_TOKENS_FOR_USER = "/gateway/auth/access-token/revoke/tokens/user"; public static final String REVOKE_OWN_ACCESS_TOKENS = "/gateway/auth/access-token/revoke/tokens"; public static final String REVOKE_ACCESS_TOKENS_FOR_SCOPE = "/gateway/auth/access-token/revoke/tokens/scope"; - public final static String VALIDATE_ACCESS_TOKEN = "/gateway/api/v1/auth/access-token/validate"; - public final static String GENERATE_ACCESS_TOKEN = "/gateway/api/v1/auth/access-token/generate"; - public final static String EVICT_ACCESS_TOKEN = "/gateway/auth/access-token/evict"; + public static final String VALIDATE_ACCESS_TOKEN = "/gateway/api/v1/auth/access-token/validate"; + public static final String GENERATE_ACCESS_TOKEN = "/gateway/api/v1/auth/access-token/generate"; + public static final String EVICT_ACCESS_TOKEN = "/gateway/auth/access-token/evict"; - public final static String VALIDATE_OIDC_TOKEN = "/gateway/api/v1/auth/oidc-token/validate"; - public final static String JWK_ALL = "/gateway/api/v1/auth/keys/public/all"; + public static final String VALIDATE_OIDC_TOKEN = "/gateway/api/v1/auth/oidc-token/validate"; + public static final String JWK_ALL = "/gateway/api/v1/auth/keys/public/all"; - public final static String DISCOVERY_STATIC_API = "/discovery/api/v1/staticApi"; + public static final String DISCOVERY_STATIC_API = "/discovery/api/v1/staticApi"; - public final static String CACHING_CACHE = "/cachingservice/api/v1/cache"; - public final static String CACHING_CACHE_LIST = "/cachingservice/api/v1/cache-list"; + public static final String CACHING_CACHE = "/cachingservice/api/v1/cache"; + public static final String CACHING_CACHE_LIST = "/cachingservice/api/v1/cache-list"; - public final static String DISCOVERABLE_GREET = "/discoverableclient/api/v1/greeting"; - public final static String DISCOVERABLE_WS_HEADER = "/discoverableclient/ws/v1/header"; - public final static String DISCOVERABLE_WS_UPPERCASE = "/discoverableclient/ws/v1/uppercase"; - public final static String DISCOVERABLE_GET_FILE = "/discoverableclient/api/v1/get-file"; - public final static String DISCOVERABLE_MULTIPART = "/discoverableclient/api/v1/multipart"; - public final static String DISCOVERABLE_SSE_EVENTS = "/discoverableclient/sse/v1/events"; - public final static String X509_ENDPOINT = "/discoverableclient/api/v1/x509"; - public final static String MEDIATION_CLIENT = "/discoverableclient/api/v1/apiMediationClient"; + public static final String DISCOVERABLE_GREET = "/discoverableclient/api/v1/greeting"; + public static final String DISCOVERABLE_WS_HEADER = "/discoverableclient/ws/v1/header"; + public static final String DISCOVERABLE_WS_UPPERCASE = "/discoverableclient/ws/v1/uppercase"; + public static final String DISCOVERABLE_GET_FILE = "/discoverableclient/api/v1/get-file"; + public static final String DISCOVERABLE_MULTIPART = "/discoverableclient/api/v1/multipart"; + public static final String DISCOVERABLE_SSE_EVENTS = "/discoverableclient/sse/v1/events"; + public static final String X509_ENDPOINT = "/discoverableclient/api/v1/x509"; + public static final String MEDIATION_CLIENT = "/discoverableclient/api/v1/apiMediationClient"; - public final static String PASSTICKET_TEST_ENDPOINT = "/dcpassticket/api/v1/passticketTest"; + public static final String PASSTICKET_TEST_ENDPOINT = "/dcpassticket/api/v1/passticketTest"; - public final static String REQUEST_INFO_ENDPOINT = "/dcpassticket/api/v1/request"; - public final static String SAF_IDT_REQUEST = "/dcsafidt/api/v1/request"; - public final static String ZOSMF_REQUEST = "/dczosmf/api/v1/request"; - public final static String ZOWE_JWT_REQUEST = "/zowejwt/api/v1/request"; - public final static String STATIC_CLIENT_2_REQUEST = "/staticclient2/api/v1/request"; - public final static String STATIC_CLIENT_1_REQUEST = "/staticclient/api/v1/request"; + public static final String REQUEST_INFO_ENDPOINT = "/dcpassticket/api/v1/request"; + public static final String SAF_IDT_REQUEST = "/dcsafidt/api/v1/request"; + public static final String ZOSMF_REQUEST = "/dczosmf/api/v1/request"; + public static final String ZOWE_JWT_REQUEST = "/zowejwt/api/v1/request"; + public static final String STATIC_CLIENT_2_REQUEST = "/staticclient2/api/v1/request"; + public static final String STATIC_CLIENT_1_REQUEST = "/staticclient/api/v1/request"; - public final static String DISCOVERABLE_CLIENT_CONTAINER_ENDPOINT = "/apicatalog/api/v1/containers/cademoapps"; - public final static String DISCOVERABLE_CLIENT_API_DOC_ENDPOINT = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0"; - public final static String DISCOVERABLE_CLIENT_API_DOC_ENDPOINT_V2 = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v2.0.0"; + public static final String DISCOVERABLE_CLIENT_CONTAINER_ENDPOINT = "/apicatalog/api/v1/containers/cademoapps"; + public static final String DISCOVERABLE_CLIENT_API_DOC_ENDPOINT = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0"; + public static final String DISCOVERABLE_CLIENT_API_DOC_ENDPOINT_V2 = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v2.0.0"; - public final static String API_SERVICE_VERSION_DIFF_ENDPOINT = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0/zowe.apiml.discoverableclient.rest v2.0.0"; - public final static String API_SERVICE_VERSION_DIFF_ENDPOINT_WRONG_VERSION = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0/zowe.apiml.discoverableclient.rest v3.0.0"; - public final static String API_SERVICE_VERSION_DIFF_ENDPOINT_WRONG_SERVICE = "/apicatalog/api/v1/apidoc/invalidService/v1/v2"; + public static final String API_SERVICE_VERSION_DIFF_ENDPOINT = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0/zowe.apiml.discoverableclient.rest v2.0.0"; + public static final String API_SERVICE_VERSION_DIFF_ENDPOINT_WRONG_VERSION = "/apicatalog/api/v1/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0/zowe.apiml.discoverableclient.rest v3.0.0"; + public static final String API_SERVICE_VERSION_DIFF_ENDPOINT_WRONG_SERVICE = "/apicatalog/api/v1/apidoc/invalidService/v1/v2"; - public final static String CLOUD_GATEWAY_CERTIFICATES = "/gateway/certificates"; + public static final String CLOUD_GATEWAY_CERTIFICATES = "/gateway/certificates"; - public final static String ZOSMF_AUTH_ENDPOINT = "/zosmf/services/authenticate"; + public static final String ZOSMF_AUTH_ENDPOINT = "/zosmf/services/authenticate"; } From 7a31507d0830d736072c9427bd1434956f36d33a Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 19 Jun 2026 17:29:06 +0200 Subject: [PATCH 13/21] wip acceptance test Signed-off-by: Pablo Carle --- .../common/AcceptanceTestWithTwoServices.java | 12 +++ .../acceptance/corsTests/GatewayCorsTest.java | 88 +++++++++++-------- 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java index 3468383c11..966ff060f9 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/common/AcceptanceTestWithTwoServices.java @@ -16,6 +16,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicStatusLine; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mock; @@ -94,6 +95,17 @@ protected void mockUnavailableHttpResponseWithEntity(int statusCode) throws IOEx Mockito.when(mockClient.execute(any())).thenReturn(response); } + protected void mockValid200HttpResponseWithAddedCors() throws IOException { + mockValid200HttpResponseWithHeaders(new org.apache.http.Header[]{ + new BasicHeader("Access-Control-Allow-Origin", "test"), + new BasicHeader("Access-Control-Allow-Methods", "RANDOM"), + new BasicHeader("Access-Control-Allow-Headers", "origin,x-test"), + new BasicHeader("Access-Control-Allow-Credentials", "true"), + }); + applicationRegistry.setCurrentApplication(serviceWithCustomConfiguration.getId()); + discoveryClient.createRefreshCacheEvent(); + } + protected void assertHeaderNullValue(HttpUriRequest request, String header) { assertThat(request.getHeaders(header).length, is(1)); assertNull(request.getFirstHeader(header).getValue()); diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java index f972f869f5..7c77750178 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java @@ -12,21 +12,17 @@ import io.restassured.http.Header; import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.message.BasicHeader; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.NestedTestConfiguration; -import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; -import java.io.IOException; - import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_FORBIDDEN; import static org.apache.http.HttpStatus.SC_OK; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.arrayWithSize; @@ -36,12 +32,11 @@ import static org.mockito.Mockito.verify; @AcceptanceTest -@ActiveProfiles({"GatewayCorsEnabledTest", "test"}) +@ActiveProfiles({"GatewayCorsEnabledWithProvidedDefaultTest", "test"}) @TestPropertySource(properties = { "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org", "apiml.service.corsEnabled=true" }) -@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) class GatewayCorsEnabledWithProvidedDefaultTest extends AcceptanceTestWithTwoServices { @Test @@ -72,18 +67,17 @@ void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHe @Test void givenCorsOriginIsNotAllowed_whenPreFlightRequestArrives_thenCorsHeadersAreNotSet() throws Exception { - // Origin with other than foo.bar.org should be rejected when cors is enabled - } - - private void mockValid200HttpResponseWithAddedCors() throws IOException { - mockValid200HttpResponseWithHeaders(new org.apache.http.Header[]{ - new BasicHeader("Access-Control-Allow-Origin", "test"), - new BasicHeader("Access-Control-Allow-Methods", "RANDOM"), - new BasicHeader("Access-Control-Allow-Headers", "origin,x-test"), - new BasicHeader("Access-Control-Allow-Credentials", "true"), - }); - applicationRegistry.setCurrentApplication(serviceWithCustomConfiguration.getId()); - discoveryClient.createRefreshCacheEvent(); + // Preflight request with disallowed origin + mockValid200HttpResponseWithAddedCors(); + given() + .header(new Header("Origin", "https://malicious.example.com")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) + .when() + .options(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .statusCode(is(SC_FORBIDDEN)) + .header("Access-Control-Allow-Origin", is((String) null)); } @Test @@ -166,26 +160,50 @@ void givenCorsIsEnabled_whenRequestWithOriginComes_thenOriginIsntPassedToSouthbo assertThat(originHeaders, arrayWithSize(0)); } - @Nested - @AcceptanceTest - @ActiveProfiles({"GatewayCorsEnabledTestWithDefaults", "test"}) - @TestPropertySource(properties = { - "apiml.service.corsEnabled=true" - }) - class GatewayCorsEnabledTestWithDefaults { - // Gateway uses a default list of origins, does not accept any +} + +@Nested +@AcceptanceTest +@ActiveProfiles({"GatewayCorsEnabledWithDefaultsTest"}) +@TestPropertySource(properties = { + "apiml.service.corsEnabled=true" +}) +class GatewayCorsEnabledWithDefaultsTest extends AcceptanceTestWithTwoServices { + // Gateway uses a default list of origins, does not accept any (*) + @Test + void givenCorsIsEnabledWithDefaults_whenPreflightRequestComes_thenPreflightIsRejected() throws Exception { + // Preflight request with origin that should be rejected by default CORS policy + given() + .log().all() + .header(new Header("Origin", "https://foo.bar.org")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "Content-Type")) + .when() + .options(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .log().all() + .statusCode(is(SC_FORBIDDEN)); + + // No request should be passed to the southbound service for preflight + verify(mockClient, times(0)).execute(ArgumentMatchers.any(HttpUriRequest.class)); } - @Nested - @AcceptanceTest - @ActiveProfiles({"GatewayCorsEnabledTestWithDefaults", "test"}) - @TestPropertySource(properties = { - "apiml.service.corsEnabled=false" - }) - class GatewayCorsDisabled { - // Gateway does not interfere, all headers are passed to the southbound service + @Test + void givenCorsIsEnabledWithDefaults_whenPreflightRequestWithLocalhostOriginComes_thenPreflightIsAccepted() throws Exception { + // Preflight request with localhost origin that should be accepted by default CORS policy + given() + .header(new Header("Origin", "https://localhost:10010")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "Content-Type")) + .when() + .options(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .statusCode(is(SC_OK)) + .header("Access-Control-Allow-Origin", is("https://localhost:10010")); + // No request should be passed to the southbound service for preflight + verify(mockClient, times(0)).execute(ArgumentMatchers.any(HttpUriRequest.class)); } } From 2af05660c8a35cb7b030cbde6aaf480da5d0996d Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 19 Jun 2026 17:39:36 +0200 Subject: [PATCH 14/21] passing Signed-off-by: Pablo Carle --- .../zowe/apiml/acceptance/corsTests/GatewayCorsTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java index 7c77750178..086b1e6034 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java @@ -193,14 +193,15 @@ void givenCorsIsEnabledWithDefaults_whenPreflightRequestComes_thenPreflightIsRej void givenCorsIsEnabledWithDefaults_whenPreflightRequestWithLocalhostOriginComes_thenPreflightIsAccepted() throws Exception { // Preflight request with localhost origin that should be accepted by default CORS policy given() - .header(new Header("Origin", "https://localhost:10010")) + .log().all() + .header(new Header("Origin", "https://localhost:" + port)) .header(new Header("Access-Control-Request-Method", "POST")) .header(new Header("Access-Control-Request-Headers", "Content-Type")) .when() .options(basePath + serviceWithCustomConfiguration.getPath()) .then() - .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin", is("https://localhost:10010")); + .log().all() + .statusCode(is(SC_OK)); // No request should be passed to the southbound service for preflight verify(mockClient, times(0)).execute(ArgumentMatchers.any(HttpUriRequest.class)); From bdbcc7b81576a93f988f28b203d09df447627a8e Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 22 Jun 2026 10:42:58 +0200 Subject: [PATCH 15/21] scgw Signed-off-by: Pablo Carle --- .../gateway/CloudGatewayRoutingTest.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java index d70eac530f..624ab1b982 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java @@ -12,10 +12,7 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -109,21 +106,4 @@ void testWrongRoutingWithBasePath(String basePath) throws URISyntaxException { given().get(new URI(scgUrl)).then().statusCode(404); } - @Nested - class WhenCorsIsEnabled { -// Using staticclient, try preflight request and simple request -// Using staticclient2 - - @BeforeEach - void setUp() { - - } - - @Test - void givenServiceHasCorsConfiguration_whenPreflightRequestArrives_thenCorsHeadersAreSet() { - - } - - } - } From e841a92f2e41706b3ff429691631b2ba61b1408a Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 22 Jun 2026 15:01:42 +0200 Subject: [PATCH 16/21] remove cors cloud gateway Signed-off-by: Pablo Carle --- .../service/RouteLocator.java | 32 +++------------ .../service/RouteLocatorTest.java | 41 +------------------ 2 files changed, 7 insertions(+), 66 deletions(-) diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java index 7720a96e42..6a6ed84e83 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocator.java @@ -10,33 +10,32 @@ package org.zowe.apiml.cloudgatewayservice.service; -import lombok.AccessLevel; -import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionLocator; -import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; -import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.zowe.apiml.auth.Authentication; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.cloudgatewayservice.service.routing.RouteDefinitionProducer; import org.zowe.apiml.cloudgatewayservice.service.scheme.SchemeHandler; import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.product.routing.RoutedService; -import org.zowe.apiml.util.CorsUtils; import org.zowe.apiml.util.StringUtils; import reactor.core.publisher.Flux; -import java.util.*; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_SUPPORTING_CLIENT_CERT_FORWARDING; @Service @@ -47,28 +46,18 @@ public class RouteLocator implements RouteDefinitionLocator { @Value("${apiml.service.forwardClientCertEnabled:false}") private boolean forwardingClientCertEnabled; - private final ApplicationContext context; - - private final CorsUtils corsUtils; private final ReactiveDiscoveryClient discoveryClient; private final List commonFilters; private final List routeDefinitionProducers; private final Map schemeHandlers = new EnumMap<>(AuthenticationScheme.class); - @Getter(lazy = true, value = AccessLevel.PRIVATE) - private final UrlBasedCorsConfigurationSource corsConfigurationSource = context.getBean(UrlBasedCorsConfigurationSource.class); - public RouteLocator( - ApplicationContext context, - CorsUtils corsUtils, ReactiveDiscoveryClient discoveryClient, List commonFilters, List schemeHandlersList, List routeDefinitionProducers ) { - this.context = context; - this.corsUtils = corsUtils; this.discoveryClient = discoveryClient; this.commonFilters = commonFilters; this.routeDefinitionProducers = routeDefinitionProducers; @@ -93,15 +82,6 @@ void setAuth(ServiceInstance serviceInstance, RouteDefinition routeDefinition, A } } - void setCors(ServiceInstance serviceInstance) { - corsUtils.setCorsConfiguration( - serviceInstance.getMetadata(), - (prefix, config) -> { - String serviceId = serviceInstance.getMetadata().getOrDefault(APIML_ID, serviceInstance.getServiceId().toLowerCase()); - getCorsConfigurationSource().registerCorsConfiguration("/" + serviceId + "/**", config); - }); - } - Stream getRoutedService(ServiceInstance serviceInstance) { // TODO: this is till the SCGW and GW uses the same DS. The routing rules should be different for each application if (org.apache.commons.lang.StringUtils.equalsIgnoreCase("GATEWAY", serviceInstance.getServiceId())) { diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java index 60be21f9f1..f895f85a94 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/RouteLocatorTest.java @@ -13,21 +13,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.context.ApplicationContext; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.zowe.apiml.auth.Authentication; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.cloudgatewayservice.service.routing.RouteDefinitionProducer; import org.zowe.apiml.cloudgatewayservice.service.scheme.SchemeHandler; import org.zowe.apiml.product.routing.RoutedService; -import org.zowe.apiml.util.CorsUtils; import reactor.core.publisher.Flux; import java.util.Arrays; @@ -35,8 +32,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -52,7 +47,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_SUPPORTING_CLIENT_CERT_FORWARDING; class RouteLocatorTest { @@ -72,7 +66,6 @@ class RouteLocatorTest { }; private UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = mock(UrlBasedCorsConfigurationSource.class); - private CorsUtils corsUtils = mock(CorsUtils.class); private ReactiveDiscoveryClient discoveryClient = mock(ReactiveDiscoveryClient.class); private RouteLocator routeLocator; @@ -83,8 +76,6 @@ void init() { doReturn(urlBasedCorsConfigurationSource).when(context).getBean(UrlBasedCorsConfigurationSource.class); routeLocator = spy(new RouteLocator( - context, - corsUtils, discoveryClient, Arrays.asList(COMMON_FILTERS), Arrays.asList(SCHEME_HANDLER_FILTERS), @@ -176,36 +167,6 @@ void givenExistingAuthenticationScheme_whenSetAuth_thenCallApply() { verify(SCHEME_HANDLER_FILTERS[0]).apply(MOCK_SERVICE, routeDefinition, authentication); } - @SuppressWarnings("unchecked") - private BiConsumer getCorsLambda(Consumer> metadataProcessor) { - ServiceInstance serviceInstance = createServiceInstance("myservice", "api/v1"); - metadataProcessor.accept(serviceInstance.getMetadata()); - - routeLocator.setCors(serviceInstance); - ArgumentCaptor> lambdaCaptor = ArgumentCaptor.forClass(BiConsumer.class); - verify(corsUtils).setCorsConfiguration(any(), lambdaCaptor.capture()); - - return lambdaCaptor.getValue(); - } - - @Test - void givenApimlId_whenSetCors_thenServiceIdIsReplacedWithApimlId() { - BiConsumer corsLambda = getCorsLambda(md -> md.put(APIML_ID, "apimlid")); - - corsLambda.accept("myservice", null); - - verify(urlBasedCorsConfigurationSource).registerCorsConfiguration("/apimlid/**", null); - } - - @Test - void givenNoApimlId_whenSetCors_thenServiceIdIsUsed() { - BiConsumer corsLambda = getCorsLambda(md -> {}); - - corsLambda.accept("myservice", null); - - verify(urlBasedCorsConfigurationSource).registerCorsConfiguration("/myservice/**", null); - } - @Test void givenGateway_whenGetRoutedService_thenReturnDefaultRouting() { ServiceInstance gw = createServiceInstance("gateway", "api/v1"); @@ -276,7 +237,7 @@ void givenRouteLocator_whenGetRouteDefinitions_thenGenerateAll() { class PostRoutingFilterDefinition { private final List COMMON_FILTERS = Collections.singletonList(mock(FilterDefinition.class)); - private final RouteLocator routeLocator = new RouteLocator(null, null, null, COMMON_FILTERS, Collections.emptyList(), null); + private final RouteLocator routeLocator = new RouteLocator(null, COMMON_FILTERS, Collections.emptyList(), null); private ServiceInstance createServiceInstance(Boolean forwardingEnabled) { Map metadata = new HashMap<>(); From 728e69bad6847570f9696dce6fc7ab7c35616e43 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 22 Jun 2026 15:21:12 +0200 Subject: [PATCH 17/21] update start.sh Signed-off-by: Pablo Carle --- gateway-package/src/main/resources/bin/start.sh | 1 + schemas/gateway-schema.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index b062050563..204c476df7 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -404,6 +404,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} java \ -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-true} \ -Dapiml.service.apimlId=${ZWE_configs_apimlId:-} \ -Dapiml.service.corsAllowedMethods=${ZWE_configs_apiml_service_corsAllowedMethods:-GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS} \ + -Dapiml.service.corsDefaultAllowedOrigins=${ZWE_configs_apiml_service_corsDefaultAllowedOrigins:-} \ -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-false} \ -Dapiml.service.discoveryServiceUrls=${ZWE_DISCOVERY_SERVICES_LIST} \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ diff --git a/schemas/gateway-schema.json b/schemas/gateway-schema.json index eee96d2939..f431f85390 100644 --- a/schemas/gateway-schema.json +++ b/schemas/gateway-schema.json @@ -219,6 +219,11 @@ "type": "boolean", "description": "Allow CORS on gateway.", "default": true + }, + "corsDefaultAllowedOrigins": { + "type": "string", + "description": "Default allowed origins for CORS.", + "default": "" } } } From a99149add8c3590afe01c87dd3e35103807d55a9 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 23 Jun 2026 16:49:48 +0200 Subject: [PATCH 18/21] add default headers logic Signed-off-by: Pablo Carle --- .../src/main/java/org/zowe/apiml/util/CorsUtils.java | 5 +++-- .../test/java/org/zowe/apiml/util/CorsUtilsTest.java | 12 +++++++++--- gateway-package/src/main/resources/bin/start.sh | 1 + .../apiml/gateway/security/config/CorsBeans.java | 7 ++++++- .../metadata/service/CorsMetadataProcessorTest.java | 1 + schemas/gateway-schema.json | 5 +++++ 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java index b67f68e062..2ae426ea34 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CorsUtils.java @@ -33,6 +33,7 @@ public class CorsUtils { private final boolean gatewayCorsEnabled; private final List defaultAllowedCorsHttpMethods; private final List defaultAllowedOrigins; + private final List defaultAllowedHeaders; public boolean isCorsEnabledForService(Map metadata) { String isCorsEnabledForService = metadata.get("apiml.corsEnabled"); @@ -70,7 +71,7 @@ private CorsConfiguration setCorsHeadersForService(Map metadata) if (isNotBlank(allowedHeadersForService)) { config.setAllowedHeaders(Arrays.asList(allowedHeadersForService.split(","))); } else { - config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); + config.setAllowedHeaders(defaultAllowedHeaders); } config.setAllowedMethods(defaultAllowedCorsHttpMethods); @@ -90,7 +91,7 @@ public void registerDefaultCorsConfiguration(BiConsumer allowedOrigins = Arrays.asList("a"); - CorsUtils corsUtils = new CorsUtils(true, defaultCorsMethods, allowedOrigins); + CorsUtils corsUtils = new CorsUtils(true, defaultCorsMethods, allowedOrigins, null); BiConsumer pathMapper = mock(BiConsumer.class); corsUtils.registerDefaultCorsConfiguration(pathMapper); diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 204c476df7..8e5fa9e8c8 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -404,6 +404,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} java \ -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-true} \ -Dapiml.service.apimlId=${ZWE_configs_apimlId:-} \ -Dapiml.service.corsAllowedMethods=${ZWE_configs_apiml_service_corsAllowedMethods:-GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS} \ + -Dapiml.service.corsDefaultAllowedHeaders=${ZWE_configs_apiml_service_corsDefaultAllowedHeaders:-} \ -Dapiml.service.corsDefaultAllowedOrigins=${ZWE_configs_apiml_service_corsDefaultAllowedOrigins:-} \ -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-false} \ -Dapiml.service.discoveryServiceUrls=${ZWE_DISCOVERY_SERVICES_LIST} \ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java index 8b459a6d02..c278eedeac 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/CorsBeans.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.zowe.apiml.util.CorsUtils; @@ -68,7 +69,10 @@ public class CorsBeans implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { if (corsDefaultAllowedOrigins == null || corsDefaultAllowedOrigins.isEmpty()) { - corsDefaultAllowedHeaders = "https://" + hostname + ":" + port; + corsDefaultAllowedOrigins = "https://" + hostname + ":" + port; + } + if (corsDefaultAllowedHeaders == null || corsDefaultAllowedHeaders.isEmpty()) { + corsDefaultAllowedHeaders = CorsConfiguration.ALL; } } @@ -126,6 +130,7 @@ CorsUtils corsUtils( return CorsUtils.builder() .gatewayCorsEnabled(gatewayCorsEnabled) .defaultAllowedCorsHttpMethods(corsDefaultAllowedMethods) + .defaultAllowedHeaders(Arrays.asList(corsDefaultAllowedHeaders.split(","))) .defaultAllowedOrigins(getDefaultAllowedOrigins(environment, new ArrayList<>(Arrays.asList(externalUrl)), hostname, port)).build(); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java index a74e4997a4..9cf8499e89 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/metadata/service/CorsMetadataProcessorTest.java @@ -51,6 +51,7 @@ void setUp() { .gatewayCorsEnabled(true) .defaultAllowedCorsHttpMethods(Arrays.asList("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS")) .defaultAllowedOrigins(Collections.emptyList()) + .defaultAllowedHeaders(Collections.singletonList("*")) .build(); } diff --git a/schemas/gateway-schema.json b/schemas/gateway-schema.json index f431f85390..e60eb48d9f 100644 --- a/schemas/gateway-schema.json +++ b/schemas/gateway-schema.json @@ -224,6 +224,11 @@ "type": "string", "description": "Default allowed origins for CORS.", "default": "" + }, + "corsDefaultAllowedHeaders": { + "type": "string", + "description": "Default allowed headers for CORS.", + "default": "*" } } } From 88cdb74e30d9baa292e625e4e54e2e42bc8b976e Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 24 Jun 2026 11:47:41 +0200 Subject: [PATCH 19/21] wip tests Signed-off-by: Pablo Carle --- .../corsTests/CorsPerServiceTest.java | 36 +- .../acceptance/corsTests/GatewayCorsTest.java | 345 +++++++++--------- .../gateway/CloudGatewayRoutingTest.java | 16 + 3 files changed, 221 insertions(+), 176 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java index becda14b4a..48e5a44b37 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/CorsPerServiceTest.java @@ -41,8 +41,8 @@ class CorsPerServiceTest extends AcceptanceTestWithTwoServices { @Test - // Verify the header to allow CORS isn't set - // Verify there was no call to southbound service + // Verify the header to allow CORS isn't set + // Verify there was no call to southbound service void givenCorsIsDelegatedToGatewayButServiceDoesntAllowCors_whenPreflightRequestArrives_thenNoAccessControlAllowOriginIsSet() throws Exception { applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId()); mockValid200HttpResponse(); @@ -62,8 +62,8 @@ void givenCorsIsDelegatedToGatewayButServiceDoesntAllowCors_whenPreflightRequest } @Test - // Verify the header to allow CORS isn't set - // Verify there was no call to southbound service + // Verify the header to allow CORS isn't set + // Verify there was no call to southbound service void givenCorsIsDelegatedToGatewayButServiceDoesntAllowCors_whenSimpleCorsRequestArrives_thenNoAccessControlAllowOriginIsSet() throws Exception { applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId()); mockValid200HttpResponse(); @@ -83,12 +83,13 @@ void givenCorsIsDelegatedToGatewayButServiceDoesntAllowCors_whenSimpleCorsReques } @Test - // There is no request to the southbound server for preflight - // There is request to the southbound server for the second request + // There is no request to the southbound server for preflight + // There is request to the southbound server for the second request void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHeadersAreSet() throws Exception { mockValid200HttpResponse(); applicationRegistry.setCurrentApplication(serviceWithCustomConfiguration.getId()); discoveryClient.createRefreshCacheEvent(); + // Preflight request given() .header(new Header("Origin", "https://foo.bar.org")) @@ -119,8 +120,8 @@ void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHe } @Test - // There is request to the southbound server for the request - // The CORS header is properly set. + // There is request to the southbound server for the request + // The CORS header is properly set. void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeadersAreSet() throws Exception { // There is request to the southbound server and the CORS headers are properly set on the response mockValid200HttpResponse(); @@ -148,6 +149,25 @@ void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeade }) class CorsPerServiceTestWithDefaults { + @Test + void givenCorsIsDelegatedToGatewayButServiceDoesntAllowCors_whenSimpleCorsRequestArrives_thenNoAccessControlAllowOriginIsSet() throws Exception { + applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId()); + mockValid200HttpResponse(); + discoveryClient.createRefreshCacheEvent(); + + given() + .header(new Header("Origin", "https://foo.bar.org")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) + .when() + .post(basePath + serviceWithDefaultConfiguration.getPath()) + .then() + .statusCode(is(SC_FORBIDDEN)) + .header("Access-Control-Allow-Origin", is(nullValue())); + + verify(mockClient, never()).execute(ArgumentMatchers.any(HttpUriRequest.class)); + } + } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java index 086b1e6034..63721caa8d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/acceptance/corsTests/GatewayCorsTest.java @@ -17,6 +17,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.acceptance.common.AcceptanceTest; import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices; @@ -31,180 +33,187 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -@AcceptanceTest -@ActiveProfiles({"GatewayCorsEnabledWithProvidedDefaultTest", "test"}) -@TestPropertySource(properties = { - "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org", - "apiml.service.corsEnabled=true" -}) -class GatewayCorsEnabledWithProvidedDefaultTest extends AcceptanceTestWithTwoServices { - - @Test - // The CORS headers are properly set on the request - void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHeadersAreSet() { - // Preflight request - given() - .header(new Header("Origin", "https://foo.bar.org")) - .header(new Header("Access-Control-Request-Method", "POST")) - .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) - .when() - .options(basePath + "/gateway/version") - .then() - .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin","https://foo.bar.org") - .header("Access-Control-Allow-Methods", "GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS") - .header("Access-Control-Allow-Headers", "origin, x-requested-with"); - - // Actual request - given() - .header(new Header("Origin", "https://foo.bar.org")) - .when() - .get(basePath + "/gateway/version") - .then() - .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin", "https://foo.bar.org"); - } - - @Test - void givenCorsOriginIsNotAllowed_whenPreFlightRequestArrives_thenCorsHeadersAreNotSet() throws Exception { - // Preflight request with disallowed origin - mockValid200HttpResponseWithAddedCors(); - given() - .header(new Header("Origin", "https://malicious.example.com")) - .header(new Header("Access-Control-Request-Method", "POST")) - .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) - .when() - .options(basePath + serviceWithCustomConfiguration.getPath()) - .then() - .statusCode(is(SC_FORBIDDEN)) - .header("Access-Control-Allow-Origin", is((String) null)); - } - - @Test - // There is request to the southbound server for the request - // The CORS header is properly set. - void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeadersAreSetAndOnlyTheOnesByGateway() throws Exception { - // There is request to the southbound server and the CORS headers are properly set on the response - mockValid200HttpResponseWithAddedCors(); - - // Preflight request - given() - .header(new Header("Origin", "https://foo.bar.org")) - .when() - .get(basePath + serviceWithCustomConfiguration.getPath()) - .then() - .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin", is("https://foo.bar.org")); - - // The actual request is passed to the southbound service - verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); - } - - @Test - // There is no request to the southbound server for preflight - // There is request to the southbound server for the second request - void givenCorsIsAllowedForSpecificService_whenTheServiceIsSet_thenCorsHeadersAreSetAndOnlyTheOnesByGateway() throws Exception { - mockValid200HttpResponseWithAddedCors(); - - // Preflight request - given() - .header(new Header("Origin", "https://foo.bar.org")) - .header(new Header("Access-Control-Request-Method", "POST")) - .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) - .when() - .options(basePath + serviceWithCustomConfiguration.getPath()) - .then() - .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin", is("https://foo.bar.org")) - .header("Access-Control-Allow-Methods", is("GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS")) - .header("Access-Control-Allow-Headers", is("origin, x-requested-with")); - - // The preflight request isn't passed to the southbound service - verify(mockClient, never()).execute(ArgumentMatchers.any(HttpUriRequest.class)); - - // Actual request - given() - .header(new Header("Origin", "https://foo.bar.org")) - .when() - .post(basePath + serviceWithCustomConfiguration.getPath()) - .then() - .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin", is("https://foo.bar.org")); - - // The actual request is passed to the southbound service - verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); - } +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) +class GatewayCorsTest { + + @Nested + @AcceptanceTest + @ActiveProfiles({"GatewayCorsEnabledWithProvidedDefaultTest"}) + @TestPropertySource(properties = { + "apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org", + "apiml.service.corsEnabled=true" + }) + class GatewayCorsEnabledWithProvidedDefaultTest extends AcceptanceTestWithTwoServices { + + @Test + // The CORS headers are properly set on the request + void givenCorsIsAllowedForSpecificService_whenPreFlightRequestArrives_thenCorsHeadersAreSet() { + // Preflight request + given() + .header(new Header("Origin", "https://foo.bar.org")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) + .when() + .options(basePath + "/gateway/version") + .then() + .statusCode(is(SC_OK)) + .header("Access-Control-Allow-Origin","https://foo.bar.org") + .header("Access-Control-Allow-Methods", "GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS") + .header("Access-Control-Allow-Headers", "origin, x-requested-with"); + + // Actual request + given() + .header(new Header("Origin", "https://foo.bar.org")) + .when() + .get(basePath + "/gateway/version") + .then() + .statusCode(is(SC_OK)) + .header("Access-Control-Allow-Origin", "https://foo.bar.org"); + } + + @Test + void givenCorsOriginIsNotAllowed_whenPreFlightRequestArrives_thenCorsHeadersAreNotSet() throws Exception { + // Preflight request with disallowed origin + mockValid200HttpResponseWithAddedCors(); + given() + .header(new Header("Origin", "https://malicious.example.com")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) + .when() + .options(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .statusCode(is(SC_FORBIDDEN)) + .header("Access-Control-Allow-Origin", is((String) null)); + } + + @Test + // There is request to the southbound server for the request + // The CORS header is properly set. + void givenCorsIsAllowedForSpecificService_whenSimpleRequestArrives_thenCorsHeadersAreSetAndOnlyTheOnesByGateway() throws Exception { + // There is request to the southbound server and the CORS headers are properly set on the response + mockValid200HttpResponseWithAddedCors(); + + // Preflight request + given() + .header(new Header("Origin", "https://foo.bar.org")) + .when() + .get(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .statusCode(is(SC_OK)) + .header("Access-Control-Allow-Origin", is("https://foo.bar.org")); + + // The actual request is passed to the southbound service + verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); + } + + @Test + // There is no request to the southbound server for preflight + // There is request to the southbound server for the second request + void givenCorsIsAllowedForSpecificService_whenTheServiceIsSet_thenCorsHeadersAreSetAndOnlyTheOnesByGateway() throws Exception { + mockValid200HttpResponseWithAddedCors(); + + // Preflight request + given() + .header(new Header("Origin", "https://foo.bar.org")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "origin, x-requested-with")) + .when() + .options(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .statusCode(is(SC_OK)) + .header("Access-Control-Allow-Origin", is("https://foo.bar.org")) + .header("Access-Control-Allow-Methods", is("GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS")) + .header("Access-Control-Allow-Headers", is("origin, x-requested-with")); + + // The preflight request isn't passed to the southbound service + verify(mockClient, never()).execute(ArgumentMatchers.any(HttpUriRequest.class)); + + // Actual request + given() + .header(new Header("Origin", "https://foo.bar.org")) + .when() + .post(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .statusCode(is(SC_OK)) + .header("Access-Control-Allow-Origin", is("https://foo.bar.org")); + + // The actual request is passed to the southbound service + verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); + } + + @Test + void givenCorsIsEnabled_whenRequestWithOriginComes_thenOriginIsntPassedToSouthbound() throws Exception { + // There is request to the southbound server and the CORS headers are properly set on the response + mockValid200HttpResponseWithAddedCors(); + + // Simple request + given() + .header(new Header("Origin", "https://foo.bar.org")) + .when() + .get(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .statusCode(is(SC_OK)) + .header("Access-Control-Allow-Origin", is("https://foo.bar.org")); + + // The actual request is passed to the southbound service + verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpUriRequest.class); + verify(mockClient, times(1)).execute(captor.capture()); + + HttpUriRequest toVerify = captor.getValue(); + org.apache.http.Header[] originHeaders = toVerify.getHeaders("Origin"); + assertThat(originHeaders, arrayWithSize(0)); + } - @Test - void givenCorsIsEnabled_whenRequestWithOriginComes_thenOriginIsntPassedToSouthbound() throws Exception { - // There is request to the southbound server and the CORS headers are properly set on the response - mockValid200HttpResponseWithAddedCors(); - - // Simple request - given() - .header(new Header("Origin", "https://foo.bar.org")) - .when() - .get(basePath + serviceWithCustomConfiguration.getPath()) - .then() - .statusCode(is(SC_OK)) - .header("Access-Control-Allow-Origin", is("https://foo.bar.org")); - - // The actual request is passed to the southbound service - verify(mockClient, times(1)).execute(ArgumentMatchers.any(HttpUriRequest.class)); - - ArgumentCaptor captor = ArgumentCaptor.forClass(HttpUriRequest.class); - verify(mockClient, times(1)).execute(captor.capture()); - - HttpUriRequest toVerify = captor.getValue(); - org.apache.http.Header[] originHeaders = toVerify.getHeaders("Origin"); - assertThat(originHeaders, arrayWithSize(0)); } -} - -@Nested -@AcceptanceTest -@ActiveProfiles({"GatewayCorsEnabledWithDefaultsTest"}) -@TestPropertySource(properties = { - "apiml.service.corsEnabled=true" -}) -class GatewayCorsEnabledWithDefaultsTest extends AcceptanceTestWithTwoServices { - // Gateway uses a default list of origins, does not accept any (*) - - @Test - void givenCorsIsEnabledWithDefaults_whenPreflightRequestComes_thenPreflightIsRejected() throws Exception { - // Preflight request with origin that should be rejected by default CORS policy - given() - .log().all() - .header(new Header("Origin", "https://foo.bar.org")) - .header(new Header("Access-Control-Request-Method", "POST")) - .header(new Header("Access-Control-Request-Headers", "Content-Type")) - .when() - .options(basePath + serviceWithCustomConfiguration.getPath()) - .then() + @Nested + @AcceptanceTest + @ActiveProfiles({"GatewayCorsEnabledWithDefaultsTest"}) + @TestPropertySource(properties = { + "apiml.service.corsEnabled=true" + }) + class GatewayCorsEnabledWithDefaultsTest extends AcceptanceTestWithTwoServices { + // Gateway uses a default list of origins, does not accept any (*) + + @Test + void givenCorsIsEnabledWithDefaults_whenPreflightRequestComes_thenPreflightIsRejected() throws Exception { + // Preflight request with origin that should be rejected by default CORS policy + given() + .log().all() + .header(new Header("Origin", "https://foo.bar.org")) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "Content-Type")) + .when() + .options(basePath + serviceWithCustomConfiguration.getPath()) + .then() + .log().all() + .statusCode(is(SC_FORBIDDEN)); + + // No request should be passed to the southbound service for preflight + verify(mockClient, times(0)).execute(ArgumentMatchers.any(HttpUriRequest.class)); + } + + @Test + void givenCorsIsEnabledWithDefaults_whenPreflightRequestWithLocalhostOriginComes_thenPreflightIsAccepted() throws Exception { + // Preflight request with localhost origin that should be accepted by default CORS policy + given() + .log().all() + .header(new Header("Origin", "https://localhost:" + port)) + .header(new Header("Access-Control-Request-Method", "POST")) + .header(new Header("Access-Control-Request-Headers", "Content-Type")) + .when() + .options(basePath + serviceWithCustomConfiguration.getPath()) + .then() .log().all() - .statusCode(is(SC_FORBIDDEN)); + .statusCode(is(SC_OK)); - // No request should be passed to the southbound service for preflight - verify(mockClient, times(0)).execute(ArgumentMatchers.any(HttpUriRequest.class)); - } + // No request should be passed to the southbound service for preflight + verify(mockClient, times(0)).execute(ArgumentMatchers.any(HttpUriRequest.class)); + } - @Test - void givenCorsIsEnabledWithDefaults_whenPreflightRequestWithLocalhostOriginComes_thenPreflightIsAccepted() throws Exception { - // Preflight request with localhost origin that should be accepted by default CORS policy - given() - .log().all() - .header(new Header("Origin", "https://localhost:" + port)) - .header(new Header("Access-Control-Request-Method", "POST")) - .header(new Header("Access-Control-Request-Headers", "Content-Type")) - .when() - .options(basePath + serviceWithCustomConfiguration.getPath()) - .then() - .log().all() - .statusCode(is(SC_OK)); - - // No request should be passed to the southbound service for preflight - verify(mockClient, times(0)).execute(ArgumentMatchers.any(HttpUriRequest.class)); } } + diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java index 624ab1b982..120001e9b5 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CloudGatewayRoutingTest.java @@ -12,7 +12,9 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -106,4 +108,18 @@ void testWrongRoutingWithBasePath(String basePath) throws URISyntaxException { given().get(new URI(scgUrl)).then().statusCode(404); } + @Nested + class WhenCorsRequests { + + @Test + void test() { + // when a request contains CORS headers, they are passed as-is to the southbound service + + // preflight request + + // simple request + } + + } + } From 0b1ed9150e803f88ed97cb38e91852d864e1fb0b Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 25 Jun 2026 10:11:23 +0200 Subject: [PATCH 20/21] update eclipse settings Signed-off-by: Pablo Carle --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 87f89459a5..beb3514369 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,7 @@ allprojects { eclipse { classpath { + defaultOutputDir = file('build/classes/java/main') file { whenMerged { classpath -> for (ClasspathEntry entry : classpath.getEntries()) { @@ -68,6 +69,12 @@ allprojects { if (theEntry.path != null && theEntry.path.contains('org.eclipse.jdt.launching.JRE_CONTAINER')) { theEntry.accessRules.add(new AccessRule('accessible', 'com/sun/net/httpserver/**')) } + if (theEntry.path != null && (theEntry.path == 'src/main/java' || theEntry.path == 'src/main/resources')) { + theEntry.output = 'build/classes/java/main' + } + if (theEntry.path != null && (theEntry.path == 'src/test/java' || theEntry.path == 'src/test/resources')) { + theEntry.output = 'build/classes/java/test' + } } } } From 901ce989ef12b692bbf00d16cfe4eac9c4a7273f Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Jul 2026 15:17:15 +0200 Subject: [PATCH 21/21] fix sonar issues Signed-off-by: Pablo Carle --- .../gateway/GatewayContextClosedEventListenerTest.java | 8 ++------ .../apiml/gateway/config/ProtectedHealthEndpointTest.java | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java index 5a4844696c..e709449670 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayContextClosedEventListenerTest.java @@ -10,9 +10,9 @@ package org.zowe.apiml.gateway; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; @@ -28,13 +28,9 @@ class GatewayContextClosedEventListenerTest { @Mock private GatewayStartupListener gatewayStartupListener; + @InjectMocks private GatewayContextClosedEventListener gatewayContextClosedEventListener; - @BeforeEach - void setUp() { - gatewayContextClosedEventListener = new GatewayContextClosedEventListener(gatewayStartupListener); - } - @Test void testOnApplicationEvent() { doNothing().when(gatewayStartupListener).onContextClosed(); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java index 8b20d12d0e..37a0973286 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java @@ -38,7 +38,7 @@ class ProtectedHealthEndpointTest { protected int port; @BeforeEach - public void setBasePath() { + void setBasePath() { basePath = String.format("https://localhost:%d", port); }