diff --git a/api-catalog-package/src/main/resources/bin/start.sh b/api-catalog-package/src/main/resources/bin/start.sh index 88d63061a1..9cbf6ee2fd 100755 --- a/api-catalog-package/src/main/resources/bin/start.sh +++ b/api-catalog-package/src/main/resources/bin/start.sh @@ -115,64 +115,65 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CATALOG_CODE} ${JAVA_BIN_DIR}java \ ${EXTERNAL_URL} \ ${EUREKA_IP_ADDRESS} \ ${CUSTOM_JVM_OPTS} \ - -Dibm.serversocket.recover=true \ - -Dfile.encoding=UTF-8 \ - -Djava.io.tmpdir=${TMPDIR:-/tmp} \ - -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-} \ - -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ - -Dapiml.service.port=${ZWE_configs_port:-7552} \ - -Dapiml.service.discoveryServiceUrls=${ZWE_DISCOVERY_SERVICES_LIST} \ - -Dapiml.service.gatewayHostname=${ZWE_GATEWAY_HOST:-${ZWE_haInstance_hostname:-localhost}} \ - -Dapiml.discovery.userid=${discoveryUserid} \ + -Dapiml.catalog.customStyle.backgroundColor=${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-} \ + -Dapiml.catalog.customStyle.docLink=${ZWE_configs_apiml_catalog_customStyle_docLink:-} \ + -Dapiml.catalog.customStyle.fontFamily=${ZWE_configs_apiml_catalog_customStyle_fontFamily:-} \ + -Dapiml.catalog.customStyle.headerColor=${ZWE_configs_apiml_catalog_customStyle_headerColor:-} \ + -Dapiml.catalog.customStyle.logo=${ZWE_configs_apiml_catalog_customStyle_logo:-} \ + -Dapiml.catalog.customStyle.textColor=${ZWE_configs_apiml_catalog_customStyle_textColor:-} \ + -Dapiml.catalog.customStyle.titlesColor=${ZWE_configs_apiml_catalog_customStyle_titlesColor:-} \ + -Dapiml.catalog.hide.serviceInfo=${ZWE_configs_apiml_catalog_hide_serviceInfo:-false} \ -Dapiml.discovery.password=${discoveryPassword} \ - -Dapiml.logs.location=${ZWE_zowe_logDirectory} \ - -Dapiml.health.protected=${ZWE_configs_apiml_health_protected:-true} \ -Dapiml.discovery.staticApiDefinitionsDirectories=${ZWE_STATIC_DEFINITIONS_DIR} \ - -Dapiml.security.ssl.verifySslCertificatesOfServices=${verifySslCertificatesOfServices:-false} \ - -Dapiml.security.ssl.nonStrictVerifySslCertificatesOfServices=${nonStrictVerifySslCertificatesOfServices:-false} \ - -Dapiml.security.authorization.provider=${ZWE_configs_apiml_security_authorization_provider:-${ZWE_components_gateway_apiml_security_authorization_provider:-"native"}} \ + -Dapiml.discovery.userid=${discoveryUserid} \ + -Dapiml.health.protected=${ZWE_configs_apiml_health_protected:-true} \ + -Dapiml.logs.location=${ZWE_zowe_logDirectory} \ + -Dapiml.security.allowedDomains=${ZWE_ALLOWED_DOMAINS} \ + -Dapiml.security.auth.cookieProperties.cookieName=${cookieName:-apimlAuthenticationToken} \ -Dapiml.security.authorization.endpoint.enabled=${ZWE_components_gateway_apiml_security_authorization_endpoint_enabled:-false} \ -Dapiml.security.authorization.endpoint.url=${ZWE_components_gateway_apiml_security_authorization_endpoint_url:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port}/zss/api/v1/saf-auth"} \ + -Dapiml.security.authorization.provider=${ZWE_configs_apiml_security_authorization_provider:-${ZWE_components_gateway_apiml_security_authorization_provider:-"native"}} \ -Dapiml.security.authorization.resourceClass=${ZWE_components_gateway_apiml_security_authorization_resourceClass:-ZOWE} \ - -Dapiml.security.auth.cookieProperties.cookieName=${cookieName:-apimlAuthenticationToken} \ - -Dapiml.catalog.hide.serviceInfo=${ZWE_configs_apiml_catalog_hide_serviceInfo:-false} \ - -Dapiml.catalog.customStyle.logo=${ZWE_configs_apiml_catalog_customStyle_logo:-} \ - -Dapiml.catalog.customStyle.fontFamily=${ZWE_configs_apiml_catalog_customStyle_fontFamily:-} \ - -Dapiml.catalog.customStyle.backgroundColor=${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-} \ - -Dapiml.catalog.customStyle.titlesColor=${ZWE_configs_apiml_catalog_customStyle_titlesColor:-} \ - -Dapiml.catalog.customStyle.headerColor=${ZWE_configs_apiml_catalog_customStyle_headerColor:-} \ - -Dapiml.catalog.customStyle.textColor=${ZWE_configs_apiml_catalog_customStyle_textColor:-} \ - -Dapiml.catalog.customStyle.docLink=${ZWE_configs_apiml_catalog_customStyle_docLink:-} \ - -Dapiml.service.ssl.enabled-protocols=${ZWE_configs_apiml_service_ssl_enabled_protocols:-${client_enabled_protocols}} \ + -Dapiml.security.ssl.nonStrictVerifySslCertificatesOfServices=${nonStrictVerifySslCertificatesOfServices:-false} \ + -Dapiml.security.ssl.verifySslCertificatesOfServices=${verifySslCertificatesOfServices:-false} \ + -Dapiml.service.discoveryServiceUrls=${ZWE_DISCOVERY_SERVICES_LIST} \ + -Dapiml.service.gatewayHostname=${ZWE_GATEWAY_HOST:-${ZWE_haInstance_hostname:-localhost}} \ + -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ + -Dapiml.service.port=${ZWE_configs_port:-7552} \ -Dapiml.service.ssl.ciphers=${ZWE_configs_apiml_service_ssl_ciphers:-${client_ciphers}} \ + -Dapiml.service.ssl.enabled-protocols=${ZWE_configs_apiml_service_ssl_enabled_protocols:-${client_enabled_protocols}} \ -Dapiml.service.ssl.key-alias="${client_key_alias}" \ -Dapiml.service.ssl.key-password="${client_key_pass}" \ - -Dapiml.service.ssl.key-store="${client_keystore_location}" \ -Dapiml.service.ssl.key-store-password="${client_keystore_pass}" \ -Dapiml.service.ssl.key-store-type="${client_keystore_type}" \ + -Dapiml.service.ssl.key-store="${client_keystore_location}" \ -Dapiml.service.ssl.protocol=${ZWE_configs_apiml_service_ssl_protocol:-${server_protocol}} \ - -Dapiml.service.ssl.trust-store="${client_truststore_location}" \ -Dapiml.service.ssl.trust-store-password="${client_truststore_pass}" \ -Dapiml.service.ssl.trust-store-type="${client_truststore_type}" \ + -Dapiml.service.ssl.trust-store="${client_truststore_location}" \ + -Dfile.encoding=UTF-8 \ + -Dibm.serversocket.recover=true \ + -Djava.io.tmpdir=${TMPDIR:-/tmp} \ + -Djava.library.path=${LIBPATH} \ + -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ + -Dloader.path=${COMMON_LIB} \ + -Dotel.sdk.disabled=true \ + -Dserver.address=${ZWE_configs_zowe_network_server_listenAddresses:-${ZWE_zowe_network_server_listenAddresses:-"0.0.0.0"}} \ -Dserver.ssl.ciphers=${server_ciphers} \ - -Dserver.ssl.protocol=${server_protocol} \ -Dserver.ssl.enabled-protocols=${server_enabled_protocols} \ - -Dserver.address=${ZWE_configs_zowe_network_server_listenAddresses:-${ZWE_zowe_network_server_listenAddresses:-"0.0.0.0"}} \ -Dserver.ssl.enabled=${ZWE_configs_server_ssl_enabled:-true} \ - -Dserver.ssl.keyStore="${keystore_location}" \ - -Dserver.ssl.keyStoreType="${keystore_type}" \ - -Dserver.ssl.keyStorePassword="${keystore_pass}" \ -Dserver.ssl.keyAlias="${key_alias}" \ -Dserver.ssl.keyPassword="${key_pass}" \ + -Dserver.ssl.keyStore="${keystore_location}" \ + -Dserver.ssl.keyStorePassword="${keystore_pass}" \ + -Dserver.ssl.keyStoreType="${keystore_type}" \ + -Dserver.ssl.protocol=${server_protocol} \ -Dserver.ssl.trustStore="${truststore_location}" \ - -Dserver.ssl.trustStoreType="${truststore_type}" \ -Dserver.ssl.trustStorePassword="${truststore_pass}" \ - -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -Dloader.path=${COMMON_LIB} \ - -Djava.library.path=${LIBPATH} \ - -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ - -Dotel.sdk.disabled=true \ + -Dserver.ssl.trustStoreType="${truststore_type}" \ + -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-} \ -jar "${JAR_FILE}" & pid=$! echo "pid=${pid}" diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocService.java index 955118624a..06365fd409 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocService.java @@ -16,6 +16,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -24,6 +25,8 @@ import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.config.ApiInfo; import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; +import org.zowe.apiml.exception.MetadataValidationException; +import org.zowe.apiml.product.eureka.web.MetadataFilterService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.routing.RoutedService; @@ -48,6 +51,7 @@ public class ApiDocService { private final TransformApiDocService transformApiDocService; private final ApiDocRetrievalServiceLocal apiDocRetrievalServiceLocal; private final ApiDocRetrievalServiceRest apiDocRetrievalServiceRest; + private final MetadataFilterService metadataFilterService; ServiceInstance getInstanceInfo(String serviceId) { return EurekaUtils.getInstanceInfo(discoveryClient, serviceId) @@ -329,8 +333,15 @@ private ApiInfo findApi(List apiInfos, String apiVersion) { * @throws ApiDocNotFoundException if the response is error */ public Mono retrieveApiDoc(@NonNull String serviceId, String apiVersion) { - ServiceInstance serviceInstance = getInstanceInfo(serviceId); - List apiInfoList = metadataParser.parseApiInfo(serviceInstance.getMetadata()); + EurekaServiceInstance serviceInstance = (EurekaServiceInstance) getInstanceInfo(serviceId); + try { + serviceInstance = new EurekaServiceInstance(metadataFilterService.verifyAllowedDomains(serviceInstance.getInstanceInfo())); + } catch (MetadataValidationException e) { + log.debug("Failure validating metadata against allowed domains", e); + throw new ApiDocNotFoundException(e.getMessage()); + } + + var apiInfoList = metadataParser.parseApiInfo(serviceInstance.getMetadata()); var apiInfo = findApi(apiInfoList, apiVersion); return retrieveApiDoc(serviceInstance, apiInfo); } @@ -348,8 +359,15 @@ public Mono retrieveApiDoc(@NonNull String serviceId, String apiVersion) * @throws ApiDocNotFoundException if the response is error */ public Mono retrieveDefaultApiDoc(@NonNull String serviceId) { - ServiceInstance serviceInstance = getInstanceInfo(serviceId); - List apiInfoList = metadataParser.parseApiInfo(serviceInstance.getMetadata()); + EurekaServiceInstance serviceInstance = (EurekaServiceInstance) getInstanceInfo(serviceId); + try { + serviceInstance = new EurekaServiceInstance(metadataFilterService.verifyAllowedDomains(serviceInstance.getInstanceInfo())); + } catch (MetadataValidationException e) { + log.debug("Failure validating metadata against allowed domains", e); + throw new ApiDocNotFoundException(e.getMessage()); + } + + var apiInfoList = metadataParser.parseApiInfo(serviceInstance.getMetadata()); var apiInfo = getDefaultApiInfo(apiInfoList); return retrieveApiDoc(serviceInstance, apiInfo); } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocServiceTest.java index 4deecc54cb..1200d36770 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocServiceTest.java @@ -37,6 +37,7 @@ import org.zowe.apiml.config.ApiInfo; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.eureka.web.MetadataFilterService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import reactor.core.publisher.Mono; @@ -49,6 +50,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import static java.util.Collections.singletonList; import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; import static org.apache.hc.core5.http.HttpStatus.SC_OK; import static org.junit.jupiter.api.Assertions.*; @@ -92,6 +94,9 @@ class ViaRestCall { @Mock private ClientResponse clientResponse; + @Mock + private MetadataFilterService metadataFilterService; + private WebClient webClient; private AtomicReference lastApiInfo = new AtomicReference<>(); @@ -115,7 +120,8 @@ public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { } }, mock(ApiDocRetrievalServiceLocal.class), - apiDocRetrievalServiceRest + apiDocRetrievalServiceRest, + metadataFilterService ) { @Override Mono retrieveApiDoc(ServiceInstance serviceInstance, ApiInfo apiInfo) { @@ -133,9 +139,10 @@ class WhenGetApiDoc { @Test void givenValidApiInfo_thenReturnApiDoc() { var responseBody = "api-doc body"; + var instance = getStandardInstance(getStandardMetadata(), true); - when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); + when(discoveryClient.getInstances(SERVICE_ID)).thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -153,6 +160,7 @@ void givenValidApiInfo_thenReturnApiDoc() { }) .verifyComplete(); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals(0L, elapsed.toSeconds()); } @@ -167,13 +175,17 @@ void givenNoApiDocFoundForService() { @Test void givenServerErrorWhenRequestingSwaggerUrl() { - when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); + var instance = getStandardInstance(getStandardMetadata(), true); + + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); + when(discoveryClient.getInstances(SERVICE_ID)).thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_INTERNAL_SERVER_ERROR)).when(clientResponse).statusCode(); Mono apiDocMono = apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); Exception exception = assertThrows(ApiDocNotFoundException.class, apiDocMono::block); + + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals("No API Documentation was retrieved due to " + SERVICE_ID + " server error: 500", exception.getMessage()); } @@ -212,10 +224,11 @@ void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() { } } """.replaceAll("\\s+", ""); - String responseBody = "api-doc body"; + var responseBody = "api-doc body"; + var instance = getStandardInstance(getMetadataWithoutSwaggerUrl(), true); - when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutSwaggerUrl(), true))); + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); + when(discoveryClient.getInstances(SERVICE_ID)).thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -232,6 +245,8 @@ void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() { assertEquals(generatedResponseBody, actualApiDoc.replaceAll("\\s+", "")); }) .verifyComplete(); + + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals(0L, elapsed.toSeconds()); } @@ -239,8 +254,11 @@ void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() { void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() { var responseBody = "api-doc body"; + var instance = getStandardInstance(getMetadataWithoutApiInfo(), true); + + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), true))); + .thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -252,6 +270,7 @@ void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() { }) .verifyComplete(); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals(0L, elapsed.toSeconds()); } @@ -259,8 +278,10 @@ void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() { void shouldCreateApiDocUrlFromRoutingAndUseHttp() { var responseBody = "api-doc body"; - when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), false))); + var instance = getStandardInstance(getMetadataWithoutApiInfo(), false); + + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); + when(discoveryClient.getInstances(SERVICE_ID)).thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -272,13 +293,16 @@ void shouldCreateApiDocUrlFromRoutingAndUseHttp() { }) .verifyComplete(); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals(0L, elapsed.toSeconds()); } @Test void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() { - when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); + var instance = getStandardInstance(getStandardMetadata(), true); + + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); + when(discoveryClient.getInstances(SERVICE_ID)).thenReturn(singletonList(instance)); var exception = new IOException("Unable to reach the host"); doReturn(HttpStatusCode.valueOf(SC_INTERNAL_SERVER_ERROR)).when(clientResponse).statusCode(); @@ -293,8 +317,10 @@ void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() assertEquals(SERVICE_VERSION, lastApiInfo.get().getVersion()); assertEquals(SWAGGER_URL, lastApiInfo.get().getSwaggerUrl()); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); verify(apimlLogger, times(1)).log("org.zowe.apiml.apicatalog.apiDocHostCommunication", SERVICE_ID, exception.getMessage()); } + } @Nested @@ -304,9 +330,11 @@ class WhenGetDefaultApiDoc { void givenDefaultApiDoc_thenReturnIt() { var responseBody = "api-doc body"; var metadata = getMetadataWithMultipleApiInfo(); + var instance = getStandardInstance(metadata, true); + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(metadata, true))); + .thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -324,6 +352,8 @@ void givenDefaultApiDoc_thenReturnIt() { }) .verifyComplete(); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); + assertEquals(0L, elapsed.toSeconds()); } @@ -333,8 +363,11 @@ void givenNoDefaultApiDoc_thenReturnHighestVersion() { var metadata = getMetadataWithMultipleApiInfo(); metadata.remove(API_INFO + ".1." + API_INFO_IS_DEFAULT); // unset default API, so higher version becomes default + var instance = getStandardInstance(metadata, true); + + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(metadata, true))); + .thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -352,6 +385,7 @@ void givenNoDefaultApiDoc_thenReturnHighestVersion() { }) .verifyComplete(); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals(0L, elapsed.toSeconds()); } @@ -359,8 +393,11 @@ void givenNoDefaultApiDoc_thenReturnHighestVersion() { void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() { var responseBody = "api-doc body"; + var instance = getStandardInstance(getMetadataWithMultipleApiInfoWithDifferentVersionFormat(), true); + + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithMultipleApiInfoWithDifferentVersionFormat(), true))); + .thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -378,6 +415,7 @@ void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() { }) .verifyComplete(); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals(0L, elapsed.toSeconds()); } @@ -385,8 +423,11 @@ void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() { void givenNoApiDocs_thenReturnNull() { var responseBody = "api-doc body"; + var instance = getStandardInstance(getMetadataWithoutApiInfo(), true); + + when(metadataFilterService.verifyAllowedDomains(instance.getInstanceInfo())).thenReturn(instance.getInstanceInfo()); when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), true))); + .thenReturn(singletonList(instance)); doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); @@ -398,6 +439,7 @@ void givenNoApiDocs_thenReturnNull() { }) .verifyComplete(); + verify(metadataFilterService).verifyAllowedDomains(instance.getInstanceInfo()); assertEquals(0L, elapsed.toSeconds()); } } @@ -407,10 +449,10 @@ class WhenGetApiVersions { @Test void givenApiVersions_thenReturnThem() { when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), false))); + .thenReturn(singletonList(getStandardInstance(getStandardMetadata(), false))); List actualVersions = apiDocService.retrieveApiVersions(SERVICE_ID); - assertEquals(Collections.singletonList(SERVICE_VERSION_V), actualVersions); + assertEquals(singletonList(SERVICE_VERSION_V), actualVersions); } @Test @@ -429,7 +471,7 @@ class WhenGetDefaultApiVersion { @Test void givenDefaultApiVersion_thenReturnIt() { when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithMultipleApiInfo(), false))); + .thenReturn(singletonList(getStandardInstance(getMetadataWithMultipleApiInfo(), false))); String defaultVersion = apiDocService.retrieveDefaultApiVersion(SERVICE_ID); assertEquals(SERVICE_VERSION_V, defaultVersion); @@ -441,7 +483,7 @@ void givenNoDefaultApiVersion_thenReturnHighestVersion() { metadata.remove(API_INFO + ".1." + API_INFO_IS_DEFAULT); // unset default API, so higher version becomes default when(discoveryClient.getInstances(SERVICE_ID)) - .thenReturn(Collections.singletonList(getStandardInstance(metadata, false))); + .thenReturn(singletonList(getStandardInstance(metadata, false))); String defaultVersion = apiDocService.retrieveDefaultApiVersion(SERVICE_ID); assertEquals(HIGHER_SERVICE_VERSION_V, defaultVersion); @@ -458,7 +500,7 @@ void givenNoApiInfo_thenThrowException() { } } - private ServiceInstance getStandardInstance(Map metadata, Boolean isPortSecure) { + private EurekaServiceInstance getStandardInstance(Map metadata, Boolean isPortSecure) { InstanceInfo instance = InstanceInfo.Builder.newBuilder() .setAppName(SERVICE_ID) .setHostName(SERVICE_HOST) @@ -564,9 +606,14 @@ class ViaApiCall { @MockitoSpyBean private ApiDocRetrievalServiceLocal apiDocRetrievalServiceLocal; + @MockitoSpyBean + private MetadataFilterService metadataFilterService; + @Autowired private GatewayClient gatewayClient; + private EurekaServiceInstance apiCatalogInstance; + @BeforeEach void onboardCatalog() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() @@ -578,17 +625,21 @@ void onboardCatalog() { "apiml.apiInfo.0.version", "1.0.0" )) .build(); - doReturn(new EurekaServiceInstance(instanceInfo)).when(apiDocService).getInstanceInfo("apicatalog"); + apiCatalogInstance = new EurekaServiceInstance(instanceInfo); + doReturn(apiCatalogInstance).when(apiDocService).getInstanceInfo("apicatalog"); gatewayClient.setGatewayConfigProperties(GW_SERVICE_ADDRESS); } @Test void givenApiCatalogId_whenRetrieveApiDoc_thenCallLocally() { + when(metadataFilterService.verifyAllowedDomains(apiCatalogInstance.getInstanceInfo())).thenReturn(apiCatalogInstance.getInstanceInfo()); + StepVerifier.create(apiDocService.retrieveApiDoc(CoreService.API_CATALOG.getServiceId(), "zowe.apiml.apicatalog v1.0.0")) .expectNextMatches(apiDoc -> apiDoc.contains("/containers/{id}")) .verifyComplete(); + verify(metadataFilterService, times(2)).verifyAllowedDomains(apiCatalogInstance.getInstanceInfo()); verify(apiDocRetrievalServiceLocal).retrieveApiDoc(any(), any()); } diff --git a/apiml-common/build.gradle b/apiml-common/build.gradle index f8c7432847..6e5981bfe1 100644 --- a/apiml-common/build.gradle +++ b/apiml-common/build.gradle @@ -11,6 +11,8 @@ dependencies { implementation libs.opentelemetry.spring.boot.starter implementation libs.opentelemetry.exporter.common implementation libs.opentelemetry.exporter.sender.okhttp + implementation libs.caffeine + implementation libs.guava compileOnly libs.netty.reactor.http implementation libs.eureka.core diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/DomainAllowListMetadataException.java b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/DomainAllowListMetadataException.java similarity index 92% rename from discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/DomainAllowListMetadataException.java rename to apiml-common/src/main/java/org/zowe/apiml/product/eureka/DomainAllowListMetadataException.java index 8f99f7b647..291f732d80 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/DomainAllowListMetadataException.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/DomainAllowListMetadataException.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.metadata; +package org.zowe.apiml.product.eureka; import org.zowe.apiml.exception.MetadataValidationException; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataFilterService.java b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/web/MetadataFilterService.java similarity index 98% rename from discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataFilterService.java rename to apiml-common/src/main/java/org/zowe/apiml/product/eureka/web/MetadataFilterService.java index 086e30dc5f..d5bdd0a40a 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataFilterService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/web/MetadataFilterService.java @@ -8,8 +8,9 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.metadata; +package org.zowe.apiml.product.eureka.web; +import ch.qos.logback.core.util.IpAddressMatcher; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.base.Objects; @@ -19,10 +20,10 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.web.util.matcher.IpAddressMatcher; import org.springframework.stereotype.Service; import org.zowe.apiml.exception.MetadataValidationException; import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.eureka.DomainAllowListMetadataException; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import java.net.IDN; diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataFilterServiceTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/eureka/web/MetadataFilterServiceTest.java similarity index 99% rename from discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataFilterServiceTest.java rename to apiml-common/src/test/java/org/zowe/apiml/product/eureka/web/MetadataFilterServiceTest.java index 5851813b5f..5c574555a1 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataFilterServiceTest.java +++ b/apiml-common/src/test/java/org/zowe/apiml/product/eureka/web/MetadataFilterServiceTest.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.metadata; +package org.zowe.apiml.product.eureka.web; import com.netflix.appinfo.InstanceInfo; import org.junit.jupiter.api.BeforeEach; diff --git a/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java b/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java index 081d592191..5c91eb41df 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java @@ -20,13 +20,13 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.i18n.LocaleContextResolver; import org.zowe.apiml.cache.StorageException; -import org.zowe.apiml.discovery.metadata.DomainAllowListMetadataException; import org.zowe.apiml.gateway.controllers.GatewayExceptionHandler; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketException; import org.zowe.apiml.passticket.UsernameNotProvidedException; +import org.zowe.apiml.product.eureka.DomainAllowListMetadataException; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.security.common.error.AccessTokenInvalidBodyException; import org.zowe.apiml.security.common.error.AccessTokenMissingBodyException; diff --git a/config/docker/api-catalog-services.yml b/config/docker/api-catalog-services.yml index ac99542fb2..aebccaa049 100644 --- a/config/docker/api-catalog-services.yml +++ b/config/docker/api-catalog-services.yml @@ -43,6 +43,8 @@ apiml: banner: console discovery: staticApiDefinitionsDirectories: /api-defs;/docker/api-defs + security: + allowedDomains: discovery-service,discovery-service-2,discoverable-client,discoverable-client-2,api-catalog-services,api-catalog-services-2,caching-service,caching-service-2,caching-service-3,gateway-service,gateway-service-2,zaas-service,zaas-service-2,mock-services,mock-services-2,zowe.github.io,www.zowe.org,localhost,www.ibm.com,central-gateway-service,central-gateway-service-2,testrunner spring: output: ansi: diff --git a/discovery-service/build.gradle b/discovery-service/build.gradle index 4ac7207d3a..065c3c2b43 100644 --- a/discovery-service/build.gradle +++ b/discovery-service/build.gradle @@ -65,7 +65,6 @@ dependencies { implementation libs.apache.commons.lang3 implementation libs.jackson.dataformat.yaml implementation libs.caffeine - implementation libs.guava testImplementation(testFixtures(project(":apiml-common"))) diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java index cea72e0dfa..dd89553783 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/ApimlInstanceRegistry.java @@ -27,17 +27,22 @@ import org.springframework.context.ApplicationContext; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.discovery.config.EurekaConfig; -import org.zowe.apiml.discovery.metadata.MetadataFilterService; import org.zowe.apiml.exception.MetadataValidationException; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; +import org.zowe.apiml.product.eureka.web.MetadataFilterService; import org.zowe.apiml.util.EurekaUtils; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/EurekaConfig.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/EurekaConfig.java index 6eb4076a4e..f4d84d7255 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/EurekaConfig.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/EurekaConfig.java @@ -12,7 +12,10 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; -import com.netflix.eureka.*; +import com.netflix.eureka.DefaultEurekaServerContext; +import com.netflix.eureka.EurekaServerConfig; +import com.netflix.eureka.EurekaServerContext; +import com.netflix.eureka.EurekaServerContextHolder; import com.netflix.eureka.cluster.PeerEurekaNodes; import com.netflix.eureka.resources.ServerCodecs; import com.netflix.eureka.transport.EurekaServerHttpClientFactory; @@ -32,9 +35,10 @@ import org.springframework.context.annotation.Primary; import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.discovery.eureka.RefreshablePeerEurekaNodes; -import org.zowe.apiml.discovery.metadata.MetadataFilterService; +import org.zowe.apiml.product.eureka.web.MetadataFilterService; import javax.net.ssl.SSLContext; + import java.util.Collection; /** diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/DomainAllowListExceptionMapper.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/DomainAllowListExceptionMapper.java index b49b205584..bd18e5b19e 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/DomainAllowListExceptionMapper.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/DomainAllowListExceptionMapper.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.product.eureka.DomainAllowListMetadataException; @Provider @Slf4j diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java index 98d66424a0..13ee8a5251 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/ApimlInstanceRegistryTest.java @@ -10,7 +10,11 @@ package org.zowe.apiml.discovery; -import com.netflix.appinfo.*; +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.DataCenterInfo; +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.MyDataCenterInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; import com.netflix.eureka.DefaultEurekaServerConfig; @@ -32,7 +36,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.discovery.config.EurekaConfig; -import org.zowe.apiml.discovery.metadata.MetadataFilterService; +import org.zowe.apiml.product.eureka.web.MetadataFilterService; import java.lang.invoke.MethodHandle; import java.util.HashMap; @@ -41,10 +45,24 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ApimlInstanceRegistryTest { diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataValidationExceptionMapperTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataValidationExceptionMapperTest.java index 532266a01c..ed2615294b 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataValidationExceptionMapperTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataValidationExceptionMapperTest.java @@ -22,6 +22,7 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.core.MessageType; import org.zowe.apiml.message.template.MessageTemplate; +import org.zowe.apiml.product.eureka.DomainAllowListMetadataException; import static org.apache.http.HttpStatus.SC_BAD_REQUEST; import static org.junit.jupiter.api.Assertions.assertEquals;