From 7a79102de13a01cdc99ebbe21ca568b901900fd6 Mon Sep 17 00:00:00 2001 From: Gowtham Selvaraj Date: Mon, 27 Oct 2025 23:18:10 +0530 Subject: [PATCH 001/152] API for delegating credentials to generate a z/OS PassTicket based on an existing authentication --- .../gateway/config/AuthEndpointConfig.java | 3 +- .../apiml/gateway/config/WebSecurity.java | 4 +- .../apiml/zaas/controllers/StsController.java | 96 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java index 7f781f5362..1a10fc33dd 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java @@ -144,7 +144,8 @@ RouterFunction routes() { .andRoute(path("/gateway/api/v1/auth/keys/public/current"), resendTo("/api/v1/auth/keys/public/current")) .andRoute(path("/gateway/api/v1/auth/oidc-token/validate"), resendTo("/api/v1/auth/oidc-token/validate")) .andRoute(path("/gateway/api/v1/auth/oidc/webfinger"), resendTo("/api/v1/auth/oidc/webfinger")) - .andRoute(path("/gateway/auth/check"), resendTo("/auth/check")); + .andRoute(path("/gateway/auth/check"), resendTo("/auth/check")) + .andRoute(path("/gateway/api/v1/auth/delegations/passticket"), resendTo("/api/v1/auth/delegations/passticket")); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java index c1e93234fa..e307643306 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java @@ -122,6 +122,7 @@ public class WebSecurity { public static final String OAUTH_2_AUTHORIZATION_URI = CONTEXT_PATH + "/oauth2/authorization/{registrationId}"; public static final String OAUTH_2_REDIRECT_URI = CONTEXT_PATH + "/login/oauth2/code/**"; public static final String OAUTH_2_REDIRECT_LOGIN_URI = CONTEXT_PATH + "/login/oauth2/code/{registrationId}"; + public static final String STS_PASSTICKET_URL = "/gateway/api/v1/auth/delegations/passticket"; @Value("${apiml.security.oidc.cookie.sameSite:Lax}") public String sameSite; @@ -358,7 +359,7 @@ SecurityWebFilterChain defaultSecurityWebFilterChain(ServerHttpSecurity http) { @Bean @Order(1) @ConditionalOnMissingBean(name = "modulithConfig") - SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { return defaultSecurityConfig(http) .securityMatcher(ServerWebExchangeMatchers.pathMatchers( REGISTRY_PATH, @@ -371,6 +372,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfi CONFORMANCE_LONG_URL, VALIDATE_SHORT_URL, VALIDATE_LONG_URL, + STS_PASSTICKET_URL, "/application/**" )) .authorizeExchange(authorizeExchangeSpec -> { diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java new file mode 100644 index 0000000000..1493c46185 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java @@ -0,0 +1,96 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.zowe.commons.usermap.MapperResponse; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper; + + +/** + * Controller offer method to control security. It can contain method for user + * and also method for calling services + * by gateway to distribute state of authentication between nodes. + */ +@RequiredArgsConstructor +@RestController +@RequestMapping(StsController.CONTROLLER_PATH) +@Slf4j +public class StsController { + + @Value("${apiml.security.oidc.registry:}") + protected String registry; + + private final PassTicketService passTicketService; + private final NativeMapperWrapper nativeMapper; + + public static final String CONTROLLER_PATH = "/zaas/api/v1/auth/delegations"; + public static final String PASSTICKET_PATH = "/passticket"; + + @PostMapping(value = StsController.PASSTICKET_PATH, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(description = "The authenticated service uses this endpoint to request a PassTicket for a target user (identified by emailId) for a specific z/OS application (applid). The incoming Bearer token is validated to ensure the requester is authorized to perform delegation before the ticket is generated.", tags = { + "Security" }, security = { + @SecurityRequirement(name = "Bearer"), + @SecurityRequirement(name = "LoginBasicAuth"), + @SecurityRequirement(name = "ClientCert") + }) + public ResponseEntity getPassTicket(@RequestBody PassTicketRequest passticketRequest) + throws Exception { + String applID = passticketRequest.getApplId(); + String emailID = passticketRequest.getEmailId(); + String zosUserId = ""; + + if (Strings.isBlank(emailID) || Strings.isBlank(applID)) { + return ResponseEntity.badRequest().build(); + } + try { + MapperResponse response = nativeMapper.getUserIDForDN(emailID, registry); + if (response.getRc() == 0 && StringUtils.isNotEmpty(response.getUserId())) { + zosUserId = response.getUserId(); + } + log.info("Getting ZOS_User_id: {} ", zosUserId); + var ticket = passTicketService.generate(zosUserId, applID); + log.info("Getting request email id: {} and ZOS_Userid: {}", emailID, zosUserId); + return ResponseEntity.ok(new PassTicketResponse(ticket, zosUserId)); + } catch (Exception ex) { + log.error("Error calling delegations passticket api", ex); + throw ex; + } + } + + @Data + public static class PassTicketRequest { + private String emailId; + private String applId; + } + + @Data + @Builder + public static class PassTicketResponse { + private String passticket; + private String tsoUserid; + } + +} From d8d382fd67103d15a8965d315bef2436afaac2b7 Mon Sep 17 00:00:00 2001 From: Gowtham Selvaraj Date: Tue, 28 Oct 2025 12:10:43 +0530 Subject: [PATCH 002/152] Added the unit test for sts controller --- .../zaas/controllers/StsControllerTest.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java new file mode 100644 index 0000000000..78240e824f --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java @@ -0,0 +1,105 @@ +package org.zowe.apiml.zaas.controllers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.springframework.http.ResponseEntity; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper; +import org.zowe.commons.usermap.MapperResponse; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class StsControllerTest { + + @Mock + private PassTicketService passTicketService; + + @Mock + private NativeMapperWrapper nativeMapper; + + @InjectMocks + private StsController stsController; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + stsController.registry = "testRegistry"; + } + + @Test + void testGetPassTicket_Success() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("TESTAPP"); + request.setEmailId("test@company.com"); + + MapperResponse mapperResponse = new MapperResponse("ZOSUSER", 0, 0, 0, 0); + + when(nativeMapper.getUserIDForDN("test@company.com", "testRegistry")).thenReturn(mapperResponse); + when(passTicketService.generate("ZOSUSER", "TESTAPP")).thenReturn("TICKET123"); + + ResponseEntity response = stsController.getPassTicket(request); + assertEquals(200, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertEquals("TICKET123", response.getBody().getPassticket()); + assertEquals("ZOSUSER", response.getBody().getTsoUserid()); + + verify(nativeMapper).getUserIDForDN("test@company.com", "testRegistry"); + verify(passTicketService).generate("ZOSUSER", "TESTAPP"); + } + + @Test + void testGetPassTicket_BadRequest_BlankEmail() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId(""); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(400, response.getStatusCode().value()); + verifyNoInteractions(passTicketService, nativeMapper); + } + + @Test + void testGetPassTicket_BadRequest_BlankApplId() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setEmailId("test@company.com"); + request.setApplId(""); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(400, response.getStatusCode().value()); + verifyNoInteractions(passTicketService, nativeMapper); + } + + @Test + void testGetPassTicket_NativeMapperFailure() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId("test@company.com"); + + when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenThrow(new RuntimeException("Mapper failed")); + + Exception exception = assertThrows(RuntimeException.class, () -> stsController.getPassTicket(request)); + assertEquals("Mapper failed", exception.getMessage()); + } + + @Test + void testGetPassTicket_MapperReturnsNoUser() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId("test@company.com"); + + MapperResponse mapperResponse = new MapperResponse("", 0, 0, 0, 0); + + when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenReturn(mapperResponse); + when(passTicketService.generate("", "APPID")).thenReturn("TICKET123"); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(200, response.getStatusCode().value()); + assertEquals("TICKET123", response.getBody().getPassticket()); + assertEquals("", response.getBody().getTsoUserid()); + } +} From 92c9cbb63c3ee42617afbfa7160e2ca2296db512 Mon Sep 17 00:00:00 2001 From: Gowtham Selvaraj Date: Mon, 27 Oct 2025 23:18:10 +0530 Subject: [PATCH 003/152] API for delegating credentials to generate a z/OS PassTicket based on an existing authentication Signed-off-by: Gowtham Selvaraj --- .../gateway/config/AuthEndpointConfig.java | 3 +- .../apiml/gateway/config/WebSecurity.java | 4 +- .../apiml/zaas/controllers/StsController.java | 96 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java index 7f781f5362..1a10fc33dd 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java @@ -144,7 +144,8 @@ RouterFunction routes() { .andRoute(path("/gateway/api/v1/auth/keys/public/current"), resendTo("/api/v1/auth/keys/public/current")) .andRoute(path("/gateway/api/v1/auth/oidc-token/validate"), resendTo("/api/v1/auth/oidc-token/validate")) .andRoute(path("/gateway/api/v1/auth/oidc/webfinger"), resendTo("/api/v1/auth/oidc/webfinger")) - .andRoute(path("/gateway/auth/check"), resendTo("/auth/check")); + .andRoute(path("/gateway/auth/check"), resendTo("/auth/check")) + .andRoute(path("/gateway/api/v1/auth/delegations/passticket"), resendTo("/api/v1/auth/delegations/passticket")); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java index c1e93234fa..e307643306 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java @@ -122,6 +122,7 @@ public class WebSecurity { public static final String OAUTH_2_AUTHORIZATION_URI = CONTEXT_PATH + "/oauth2/authorization/{registrationId}"; public static final String OAUTH_2_REDIRECT_URI = CONTEXT_PATH + "/login/oauth2/code/**"; public static final String OAUTH_2_REDIRECT_LOGIN_URI = CONTEXT_PATH + "/login/oauth2/code/{registrationId}"; + public static final String STS_PASSTICKET_URL = "/gateway/api/v1/auth/delegations/passticket"; @Value("${apiml.security.oidc.cookie.sameSite:Lax}") public String sameSite; @@ -358,7 +359,7 @@ SecurityWebFilterChain defaultSecurityWebFilterChain(ServerHttpSecurity http) { @Bean @Order(1) @ConditionalOnMissingBean(name = "modulithConfig") - SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { return defaultSecurityConfig(http) .securityMatcher(ServerWebExchangeMatchers.pathMatchers( REGISTRY_PATH, @@ -371,6 +372,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfi CONFORMANCE_LONG_URL, VALIDATE_SHORT_URL, VALIDATE_LONG_URL, + STS_PASSTICKET_URL, "/application/**" )) .authorizeExchange(authorizeExchangeSpec -> { diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java new file mode 100644 index 0000000000..1493c46185 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java @@ -0,0 +1,96 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.zowe.commons.usermap.MapperResponse; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper; + + +/** + * Controller offer method to control security. It can contain method for user + * and also method for calling services + * by gateway to distribute state of authentication between nodes. + */ +@RequiredArgsConstructor +@RestController +@RequestMapping(StsController.CONTROLLER_PATH) +@Slf4j +public class StsController { + + @Value("${apiml.security.oidc.registry:}") + protected String registry; + + private final PassTicketService passTicketService; + private final NativeMapperWrapper nativeMapper; + + public static final String CONTROLLER_PATH = "/zaas/api/v1/auth/delegations"; + public static final String PASSTICKET_PATH = "/passticket"; + + @PostMapping(value = StsController.PASSTICKET_PATH, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(description = "The authenticated service uses this endpoint to request a PassTicket for a target user (identified by emailId) for a specific z/OS application (applid). The incoming Bearer token is validated to ensure the requester is authorized to perform delegation before the ticket is generated.", tags = { + "Security" }, security = { + @SecurityRequirement(name = "Bearer"), + @SecurityRequirement(name = "LoginBasicAuth"), + @SecurityRequirement(name = "ClientCert") + }) + public ResponseEntity getPassTicket(@RequestBody PassTicketRequest passticketRequest) + throws Exception { + String applID = passticketRequest.getApplId(); + String emailID = passticketRequest.getEmailId(); + String zosUserId = ""; + + if (Strings.isBlank(emailID) || Strings.isBlank(applID)) { + return ResponseEntity.badRequest().build(); + } + try { + MapperResponse response = nativeMapper.getUserIDForDN(emailID, registry); + if (response.getRc() == 0 && StringUtils.isNotEmpty(response.getUserId())) { + zosUserId = response.getUserId(); + } + log.info("Getting ZOS_User_id: {} ", zosUserId); + var ticket = passTicketService.generate(zosUserId, applID); + log.info("Getting request email id: {} and ZOS_Userid: {}", emailID, zosUserId); + return ResponseEntity.ok(new PassTicketResponse(ticket, zosUserId)); + } catch (Exception ex) { + log.error("Error calling delegations passticket api", ex); + throw ex; + } + } + + @Data + public static class PassTicketRequest { + private String emailId; + private String applId; + } + + @Data + @Builder + public static class PassTicketResponse { + private String passticket; + private String tsoUserid; + } + +} From bf1bb1d860f87a920b42290532e44989fd8a2467 Mon Sep 17 00:00:00 2001 From: Gowtham Selvaraj Date: Tue, 28 Oct 2025 12:10:43 +0530 Subject: [PATCH 004/152] Added the unit test for sts controller Signed-off-by: Gowtham Selvaraj --- .../zaas/controllers/StsControllerTest.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java new file mode 100644 index 0000000000..78240e824f --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java @@ -0,0 +1,105 @@ +package org.zowe.apiml.zaas.controllers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.springframework.http.ResponseEntity; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper; +import org.zowe.commons.usermap.MapperResponse; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class StsControllerTest { + + @Mock + private PassTicketService passTicketService; + + @Mock + private NativeMapperWrapper nativeMapper; + + @InjectMocks + private StsController stsController; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + stsController.registry = "testRegistry"; + } + + @Test + void testGetPassTicket_Success() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("TESTAPP"); + request.setEmailId("test@company.com"); + + MapperResponse mapperResponse = new MapperResponse("ZOSUSER", 0, 0, 0, 0); + + when(nativeMapper.getUserIDForDN("test@company.com", "testRegistry")).thenReturn(mapperResponse); + when(passTicketService.generate("ZOSUSER", "TESTAPP")).thenReturn("TICKET123"); + + ResponseEntity response = stsController.getPassTicket(request); + assertEquals(200, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertEquals("TICKET123", response.getBody().getPassticket()); + assertEquals("ZOSUSER", response.getBody().getTsoUserid()); + + verify(nativeMapper).getUserIDForDN("test@company.com", "testRegistry"); + verify(passTicketService).generate("ZOSUSER", "TESTAPP"); + } + + @Test + void testGetPassTicket_BadRequest_BlankEmail() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId(""); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(400, response.getStatusCode().value()); + verifyNoInteractions(passTicketService, nativeMapper); + } + + @Test + void testGetPassTicket_BadRequest_BlankApplId() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setEmailId("test@company.com"); + request.setApplId(""); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(400, response.getStatusCode().value()); + verifyNoInteractions(passTicketService, nativeMapper); + } + + @Test + void testGetPassTicket_NativeMapperFailure() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId("test@company.com"); + + when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenThrow(new RuntimeException("Mapper failed")); + + Exception exception = assertThrows(RuntimeException.class, () -> stsController.getPassTicket(request)); + assertEquals("Mapper failed", exception.getMessage()); + } + + @Test + void testGetPassTicket_MapperReturnsNoUser() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId("test@company.com"); + + MapperResponse mapperResponse = new MapperResponse("", 0, 0, 0, 0); + + when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenReturn(mapperResponse); + when(passTicketService.generate("", "APPID")).thenReturn("TICKET123"); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(200, response.getStatusCode().value()); + assertEquals("TICKET123", response.getBody().getPassticket()); + assertEquals("", response.getBody().getTsoUserid()); + } +} From 3e16c67c0df27c3a7069544e765dd4b445bfbdaf Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 2 Jul 2025 10:31:54 +0200 Subject: [PATCH 005/152] fix: modulith mode does not distribute logout (#4191) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 255 +++--------------- .github/workflows/service-registration.yml | 2 +- .../org/zowe/apiml/WebSecurityConfig.java | 68 +++-- .../ReactiveAuthenticationController.java | 69 +++-- ...ReactiveAuthenticationControllerTests.java | 81 +++++- .../ReactiveAuthenticationControllerTest.java | 19 +- apiml/src/test/resources/application.yml | 49 ++++ gradle/versions.gradle | 6 +- .../integration/ha/GatewayChaoticTest.java | 5 + keystore/docker/all-services.ext | 1 + .../service/AuthenticationService.java | 51 +++- .../ModulithAuthenticationService.java | 49 ++++ 12 files changed, 364 insertions(+), 291 deletions(-) create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 297b34f592..46f85f75d4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -77,7 +77,7 @@ jobs: - uses: ./.github/actions/setup - - name: Run CI Tests + - name: Run Modulith CI Tests run: > ENV_CONFIG=docker-modulith ./gradlew runStartUpCheck :integration-tests:runContainerModulithTests --info -Ddiscoverableclient.instances=1 -Denvironment.config=-docker-modulith -Denvironment.modulith=true @@ -1021,12 +1021,16 @@ jobs: - uses: ./.github/actions/teardown - CITestsDiscoveryChaoticHA: + # CITestsChaoticHAModulith: + + CITestsChaoticHA: needs: PublishJibContainers container: ubuntu:latest runs-on: ubuntu-latest timeout-minutes: 15 - + strategy: + matrix: + type: ["discovery", "gateway", "discoverableclient", "websocket"] services: api-catalog-services: image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} @@ -1036,6 +1040,10 @@ jobs: image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + discoverable-client-2: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: discoverable-client-2 mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} discovery-service: @@ -1076,81 +1084,27 @@ jobs: - uses: ./.github/actions/setup - - name: Run Discovery Service Chaotic HA Tests + - name: Run Startup Check + if: always() run: > - ./gradlew runStartUpCheck :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.DiscoveryChaoticTest - --info -Denvironment.config=-ha -Ddiscoverableclient.instances=1 + ./gradlew runStartUpCheck --info -Denvironment.config=-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - - name: Correct Permisions - run: | - chmod 755 -R .gradle - - - name: Store results - uses: actions/upload-artifact@v4 - if: always() - with: - name: CITestsDiscoveryChaoticHA-${{ env.JOB_ID }} - path: | - integration-tests/build/reports/** - - - uses: ./.github/actions/teardown - - CITestsGatewayChaoticHA: - needs: PublishJibContainers - container: ubuntu:latest - runs-on: ubuntu-latest - timeout-minutes: 15 - - services: - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} - discoverable-client: - image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} - mock-services: - image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - discovery-service: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service:10011/eureka,https://discovery-service-2:10011/eureka - discovery-service-2: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: discovery-service-2 - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service-2:10011/eureka,https://discovery-service:10011/eureka - zaas-service: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - zaas-service-2: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ - gateway-service-2: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} + - name: Run Discovery Service Chaotic HA Tests + if: ${{ 'discovery' == matrix.type }} + run: > + ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.DiscoveryChaoticTest + --info -Denvironment.config=-ha -Ddiscoverableclient.instances=1 + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD env: - APIML_SERVICE_HOSTNAME: gateway-service-2 - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ - logbackService: ZWEAGW2 - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - uses: ./.github/actions/setup + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - name: Run Gateway Service Chaotic HA Tests + if: ${{ 'gateway' == matrix.type }} run: > ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.GatewayChaoticTest --info -Denvironment.config=-ha @@ -1159,76 +1113,8 @@ jobs: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - - name: Correct Permisions - run: | - chmod 755 -R .gradle - # Coverage results are not stored in this job as it would not provide much additional data - - name: Store results - uses: actions/upload-artifact@v4 - if: always() - with: - name: CITestsGatewayChaoticHA-${{ env.JOB_ID }} - path: | - integration-tests/build/reports/** - - - uses: ./.github/actions/teardown - - CITestsDiscoverableClientChaoticHA: - needs: PublishJibContainers - container: ubuntu:latest - runs-on: ubuntu-latest - timeout-minutes: 15 - - services: - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} - discoverable-client: - image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} - discoverable-client-2: - image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_HOSTNAME: discoverable-client-2 - mock-services: - image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - discovery-service: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service:10011/eureka,https://discovery-service-2:10011/eureka - discovery-service-2: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: discovery-service-2 - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service-2:10011/eureka,https://discovery-service:10011/eureka - zaas-service: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - zaas-service-2: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ - gateway-service-2: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_HOSTNAME: gateway-service-2 - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ - logbackService: ZWEAGW2 - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - uses: ./.github/actions/setup - - name: Run Discoverable Client Chaotic HA Tests + if: ${{ 'discoverableclient' == matrix.type }} run: > ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.SouthboundServiceChaoticTest --info -Denvironment.config=-ha @@ -1237,76 +1123,8 @@ jobs: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - - name: Correct Permisions - run: | - chmod 755 -R .gradle - # Coverage results are not stored in this job as it would not provide much additional data - - name: Store results - uses: actions/upload-artifact@v4 - if: always() - with: - name: CITestsDiscoverableClientChaoticHA-${{ env.JOB_ID }} - path: | - integration-tests/build/reports/** - - - uses: ./.github/actions/teardown - - CITestsWebSocketChaoticHA: - needs: PublishJibContainers - container: ubuntu:latest - runs-on: ubuntu-latest - timeout-minutes: 15 - - services: - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} - discoverable-client: - image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} - discoverable-client-2: - image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_HOSTNAME: discoverable-client-2 - mock-services: - image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - discovery-service: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service:10011/eureka,https://discovery-service-2:10011/eureka - discovery-service-2: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: discovery-service-2 - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service-2:10011/eureka,https://discovery-service:10011/eureka - zaas-service: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - zaas-service-2: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ - gateway-service-2: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_HOSTNAME: gateway-service-2 - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ - logbackService: ZWEAGW2 - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - uses: ./.github/actions/setup - - name: Run WebSocket Chaotic HA Tests + if: ${{ 'websocket' == matrix.type }} run: > ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.WebSocketChaoticTest --info -Denvironment.config=-ha @@ -1315,25 +1133,17 @@ jobs: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - - uses: ./.github/actions/dump-jacoco - if: always() - - - name: Dump DC jacoco data - run: > - java -jar ./scripts/jacococli.jar dump --address discoverable-client --port 6303 --destfile ./results/discoverable-client.exec || echo "Discoverable Client is not available to obtain JaCoCo report at the moment" - - name: Correct Permisions run: | chmod 755 -R .gradle - # Coverage results are not stored in this job as it would not provide much additional data + - name: Store results uses: actions/upload-artifact@v4 if: always() with: - name: CITestsWebSocketChaoticHA-${{ env.JOB_ID }} + name: CITestsChaoticHA-${{ env.JOB_ID }}-${{ matrix.type }} path: | integration-tests/build/reports/** - results/** - uses: ./.github/actions/teardown @@ -1584,7 +1394,10 @@ jobs: - uses: ./.github/actions/teardown PublishResults: - needs: [ CITests,CITestsModulith,CITestsWithInfinispan,CITestsZaas,GatewayProxy,GatewayServiceRouting,CITestsWebSocketChaoticHA ] + needs: [ + CITests,CITestsWithInfinispan,CITestsZaas,GatewayProxy,GatewayServiceRouting,CITestsChaoticHA, + CITestsModulith + ] runs-on: ubuntu-latest timeout-minutes: 20 @@ -1618,8 +1431,8 @@ jobs: path: ContainerCITestsZaas - uses: actions/download-artifact@v4 with: - name: CITestsWebSocketChaoticHA-${{ env.JOB_ID }} - path: citestswebsocketchaoticha + name: CITestsChaoticHA-${{ env.JOB_ID }}-websocket + path: citestschaoticha - uses: actions/download-artifact@v4 with: name: GatewayServiceRouting-${{ env.JOB_ID }} @@ -1627,7 +1440,7 @@ jobs: - name: Code coverage and publish results run: > - ./gradlew --scan coverage sonar -Dresults="containercitests/results,containercitestsmodulith/results,citestswithinfinispan/results,GatewayProxy/results,citestswebsocketchaoticha/results,GatewayServiceRouting/results,ContainerCITestsZaas/results" + ./gradlew --scan coverage sonar -Dresults="containercitests/results,containercitestsmodulith/results,citestswithinfinispan/results,GatewayProxy/results,citestschaoticha/results,GatewayServiceRouting/results,ContainerCITestsZaas/results" -Psonar.host.url=$SONAR_HOST_URL -Dsonar.token=$SONAR_TOKEN -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} diff --git a/.github/workflows/service-registration.yml b/.github/workflows/service-registration.yml index 593d7dc07e..a50146be3d 100644 --- a/.github/workflows/service-registration.yml +++ b/.github/workflows/service-registration.yml @@ -36,7 +36,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: CITests-${{ env.JOB_ID }} + name: BuildAndTest-${{ env.JOB_ID }} path: | */build/reports/** diff --git a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java index 7952c2e990..ba548da7de 100644 --- a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java @@ -29,9 +29,9 @@ import org.springframework.security.web.server.authentication.logout.HttpStatusReturningServerLogoutSuccessHandler; import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.zowe.apiml.filter.BasicLoginFilter; import org.zowe.apiml.filter.CategorizeCertsWebFilter; import org.zowe.apiml.filter.LogoutHandler; @@ -53,6 +53,10 @@ import java.util.List; import java.util.Set; +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; import static org.zowe.apiml.gateway.services.ServicesInfoController.SERVICES_FULL_URL; import static org.zowe.apiml.gateway.services.ServicesInfoController.SERVICES_SHORT_URL; @@ -103,13 +107,13 @@ public class WebSecurityConfig { "/favicon.ico"); private final ServerWebExchangeMatcher discoveryPortMatcher = exchange -> exchange.getRequest().getURI().getPort() == internalDiscoveryPort ? MatchResult.match() : MatchResult.notMatch(); - private final ServerWebExchangeMatcher isInUnauthenticatedPaths = ServerWebExchangeMatchers.pathMatchers(UNAUTHENTICATED_PATTERNS.toArray(new String[]{})); + private final ServerWebExchangeMatcher isInUnauthenticatedPaths = pathMatchers(UNAUTHENTICATED_PATTERNS.toArray(new String[]{})); private final ServerWebExchangeMatcher notInUnauthenticatedPaths = new NegatedServerWebExchangeMatcher(isInUnauthenticatedPaths); @Bean SecurityWebFilterChain errorFilterChain(ServerHttpSecurity http) { return http - .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/error")) + .securityMatcher(pathMatchers("/error")) .authorizeExchange(exchanges -> exchanges.anyExchange().permitAll()) .build(); } @@ -127,7 +131,7 @@ SecurityWebFilterChain discoveryServiceClientCertificateFilterChain(ServerHttpSe http .securityMatcher(new AndServerWebExchangeMatcher( discoveryPortMatcher, - ServerWebExchangeMatchers.pathMatchers("/eureka/**"), + pathMatchers("/eureka/**"), notInUnauthenticatedPaths, exchange -> exchange.getRequest().getURI().getPath().startsWith("/eureka/") ? MatchResult.match() : MatchResult.notMatch() // Prevents matching /eureka (mapping for homepage in modulith) )) @@ -167,7 +171,7 @@ SecurityWebFilterChain discoveryServiceBasicAuthOrTokenOrCertFilterChain(ServerH .securityMatcher(new AndServerWebExchangeMatcher( discoveryPortMatcher, notInUnauthenticatedPaths, - ServerWebExchangeMatchers.pathMatchers("/discovery/**") + pathMatchers("/discovery/**") )) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) @@ -250,7 +254,7 @@ SecurityWebFilterChain healthEndpointFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { http - .securityMatcher(ServerWebExchangeMatchers.pathMatchers(APPLICATION_HEALTH)) + .securityMatcher(pathMatchers(APPLICATION_HEALTH)) .csrf(ServerHttpSecurity.CsrfSpec::disable) .authorizeExchange(exchange -> { if (!isHealthEndpointProtected) { @@ -291,8 +295,8 @@ SecurityWebFilterChain applicationEndpointsProtected(ServerHttpSecurity http, AuthExceptionHandlerReactive authExceptionHandlerReactive) { return http .securityMatcher(new AndServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers("/application/**"), - new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(APPLICATION_HEALTH, APPLICATION_INFO, "/application/version")) + pathMatchers("/application/**"), + new NegatedServerWebExchangeMatcher(pathMatchers(APPLICATION_HEALTH, APPLICATION_INFO, "/application/version")) )) .csrf(ServerHttpSecurity.CsrfSpec::disable) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) @@ -331,18 +335,18 @@ SecurityWebFilterChain discoveryBasicAuthOrToken(ServerHttpSecurity http, * @return */ @Bean - SecurityWebFilterChain loginFilter(ServerHttpSecurity http, LogoutHandler logoutHandler) { + SecurityWebFilterChain loginAndLogoutSecurityWebFilterChain(ServerHttpSecurity http, LogoutHandler logoutHandler) { var man = new ProviderManager(x509AuthenticationProvider); var reactiveX509provider = new ReactiveAuthenticationManagerAdapter(man); return http.csrf(ServerHttpSecurity.CsrfSpec::disable) .securityMatcher(new AndServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "gateway/api/v1/auth/login", "gateway/api/v1/auth/logout") + pathMatchers(POST, "gateway/api/v1/auth/login", "gateway/api/v1/auth/logout") )) .authorizeExchange(exchange -> - exchange.matchers(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "gateway/api/v1/auth/logout")).authenticated() + exchange.matchers(pathMatchers(POST, "gateway/api/v1/auth/logout")).authenticated() ) .authorizeExchange(exchange -> - exchange.matchers(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "gateway/api/v1/auth/login")).permitAll() + exchange.matchers(pathMatchers(POST, "gateway/api/v1/auth/login")).permitAll() ) .logout(c -> c .logoutUrl("/gateway/api/v1/auth/logout") @@ -368,7 +372,7 @@ SecurityWebFilterChain queryFilter(ServerHttpSecurity http) { return http.csrf(ServerHttpSecurity.CsrfSpec::disable) .securityMatcher(new AndServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers("gateway/api/v1/auth/query") + pathMatchers("gateway/api/v1/auth/query") )) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) @@ -394,7 +398,7 @@ SecurityWebFilterChain accessTokenFilter(ServerHttpSecurity http) { var reactiveX509provider = new ReactiveAuthenticationManagerAdapter(man); return http .csrf(ServerHttpSecurity.CsrfSpec::disable) - .securityMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/gateway/api/v1/auth/access-token/generate")) + .securityMatcher(pathMatchers(POST, "/gateway/api/v1/auth/access-token/generate")) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .addFilterAfter(new CategorizeCertsWebFilter(publicKeyCertificatesBase64, certificateValidator), SecurityWebFiltersOrder.FIRST) @@ -422,7 +426,7 @@ SecurityWebFilterChain revokeTokenFilterChain(ServerHttpSecurity http, var reactiveX509provider = new ReactiveAuthenticationManagerAdapter(man); return x509SecurityConfig(http) - .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/gateway/api/v1/auth/access-token/revoke/tokens/**")) + .securityMatcher(pathMatchers("/gateway/api/v1/auth/access-token/revoke/tokens/**")) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .addFilterAfter(new CategorizeCertsWebFilter(publicKeyCertificatesBase64, certificateValidator), SecurityWebFiltersOrder.FIRST) @@ -444,14 +448,36 @@ SecurityWebFilterChain refreshTokenFilter(ServerHttpSecurity http) { var reactiveTokenAuthProvider = new ReactiveAuthenticationManagerAdapter(man); return x509SecurityConfig(http) .securityMatcher(new AndServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "gateway/api/v1/auth/refresh") + pathMatchers(POST, "gateway/api/v1/auth/refresh") )) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) - .addFilterAfter(new QueryWebFilter(failedAuthenticationWebHandler, HttpMethod.POST, true, reactiveTokenAuthProvider, httpUtils), SecurityWebFiltersOrder.AUTHENTICATION) + .addFilterAfter(new QueryWebFilter(failedAuthenticationWebHandler, POST, true, reactiveTokenAuthProvider, httpUtils), SecurityWebFiltersOrder.AUTHENTICATION) .build(); } + /** + * This security filter chain secures the auth/distribute/** and auth/invalidate/** endpoints + * They require only a trusted certificate + * + * @param http + * @return + */ + @Bean + SecurityWebFilterChain gatewayInvalidateAndDistribute(ServerHttpSecurity http) { + return x509SecurityConfig(http) + .securityMatcher( + new OrServerWebExchangeMatcher( + pathMatchers(DELETE, "gateway/api/v1/auth/invalidate/**"), + pathMatchers(GET, "gateway/api/v1/auth/distribute/**") + ) + ) + .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) + .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) + .build(); + } + + /** * This security filter chain secures the /ticket endpoint * @@ -464,11 +490,11 @@ SecurityWebFilterChain ticketFilter(ServerHttpSecurity http) { var reactiveTokenAuthProvider = new ReactiveAuthenticationManagerAdapter(man); return x509SecurityConfig(http) .securityMatcher(new AndServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers("gateway/api/v1/auth/ticket") + pathMatchers("gateway/api/v1/auth/ticket") )) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) - .addFilterAfter(new QueryWebFilter(failedAuthenticationWebHandler, HttpMethod.POST, true, reactiveTokenAuthProvider, httpUtils), SecurityWebFiltersOrder.AUTHENTICATION) + .addFilterAfter(new QueryWebFilter(failedAuthenticationWebHandler, POST, true, reactiveTokenAuthProvider, httpUtils), SecurityWebFiltersOrder.AUTHENTICATION) .build(); } @@ -489,7 +515,7 @@ SecurityWebFilterChain safResourceCheckFilter(ServerHttpSecurity http, return x509SecurityConfig(http) .securityMatcher(new AndServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers("gateway/auth/check") + pathMatchers("gateway/auth/check") )) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) @@ -515,7 +541,7 @@ SecurityWebFilterChain safResourceCheckFilter(ServerHttpSecurity http, @Bean SecurityWebFilterChain gatewayAuthenticatedEndpoints(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { return x509SecurityConfig(http, false) - .securityMatcher(ServerWebExchangeMatchers.pathMatchers( + .securityMatcher(pathMatchers( REGISTRY_PATH, REGISTRY_PATH + "/**", SERVICES_SHORT_URL, diff --git a/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java b/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java index a2de1169d7..b22e2c139b 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java @@ -12,12 +12,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -36,6 +36,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -58,7 +59,8 @@ import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; -import static org.zowe.apiml.zaas.controllers.AuthController.INVALIDATE_PATH; + + @RestController @RequestMapping("/gateway/api/v1/auth") @@ -203,33 +205,62 @@ public Mono> query() { .switchIfEmpty(Mono.just(ResponseEntity.status(SC_UNAUTHORIZED).build())); } - @DeleteMapping(path = INVALIDATE_PATH) - @Operation(summary = "Logout JWT token.", - tags = {"Security"}, - operationId = "invalidateJwtToken", - description = "Use the `/auth/invalidate` API to invalidate token on specific instance of Gateway.", - security = { - @SecurityRequirement(name = "ClientCert") - }) + @Operation( + summary = "Invalidate mainframe user session.", + tags = { "Security" }, + operationId = "logoutUsingPOST", + description = """ + Use the `/logout` API to invalidate mainframe user session. + + The cookie named `apimlAuthenticationToken` will be removed. + """ + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully invalidated"), - @ApiResponse(responseCode = "400", description = "Invalid token"), - @ApiResponse(responseCode = "503", description = "Authentication service is not available") + @ApiResponse(responseCode = "204", description = "Invalidated user session") }) - public Mono> invalidateJwtToken(ServerWebExchange exchange) { - var endpoint = "/auth/invalidate/"; - var uri = exchange.getRequest().getURI().getPath(); - var index = uri.indexOf(endpoint); + @PostMapping("/logout") + public String postMethodName() { + throw new IllegalStateException( + """ + This method should not be called. + Logout handler is implemented in Spring Security (see WebSecurityConfig) + This method is created for OpenAPI documentation purposes only. + """); + } - var jwtToken = uri.substring(index + endpoint.length()); + /** + * Invalidate JWT, hidden endpoint undocumented + * + * @param token The JWT token to invalidate + * @return + */ + @Hidden + @DeleteMapping(path = "/invalidate/{token}") + public Mono> invalidateJwtToken(@PathVariable String token) { try { var app = peerAwareInstanceRegistry.getApplications().getRegisteredApplications(CoreService.GATEWAY.getServiceId()); - boolean invalidated = authenticationService.invalidateJwtTokenGateway(jwtToken, false, app); + var invalidated = authenticationService.invalidateJwtTokenGateway(token, false, app); return Mono.just(ResponseEntity.status(invalidated ? SC_OK : SC_SERVICE_UNAVAILABLE).build()); } catch (TokenNotValidException e) { return Mono.just(ResponseEntity.status(SC_BAD_REQUEST).build()); } + } + /** + * Distribute JWT invalidate action to path-specified instance ID + * Undocumented endpoint + * + * @param instanceId The instance Id to distribute JWT invalidation to + * @return 200 if distributed, 204 if not + */ + @Hidden + @GetMapping(path = "/distribute/{instanceId}") + public Mono> distributeInvalidate(@PathVariable String instanceId) { + var distributed = authenticationService.distributeInvalidate(instanceId); + if (distributed) { + return Mono.just(ResponseEntity.ok().build()); + } + return Mono.just(ResponseEntity.noContent().build()); } @Operation(summary = "Refresh authentication token.", diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java b/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java index c656e699af..c8a555d7c0 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java @@ -19,6 +19,8 @@ import java.net.URI; import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @AcceptanceTest @@ -26,6 +28,8 @@ class ReactiveAuthenticationControllerTests extends AcceptanceTestWithMockServic private static final String REFRESH_ENDPOINT = "/gateway/api/v1/auth/refresh"; private static final String AUTH_COOKIE = "apimlAuthenticationToken"; + private static final String DISTRIBUTE_INVALIDATE_ENDPOINT = "/gateway/api/v1/auth/distribute"; + private static final String INVALIDATE_JWT_ENDPOINT = "/gateway/api/v1/auth/invalidate"; @Value("${server.ssl.keyPassword}") char[] password; @@ -66,7 +70,7 @@ void whenLoginWithBody_thenSuccess() { } @Test - void whenRefreshPATWithoutCert_then403() { + void whenRefreshTokenWithoutCert_then403() { given() .when() .post(URI.create(basePath + REFRESH_ENDPOINT)) @@ -83,4 +87,79 @@ void whenWrongMethod_thenFail() { .statusCode(405); } + @Test + void whenRefreshTokenWithCert_thenSuccess() { + var token = login(); + + var newToken = given() + .config(SslContext.clientCertApiml) + .cookie(AUTH_COOKIE, token) + .when() + .post(URI.create(basePath + REFRESH_ENDPOINT)) + .then() + .statusCode(200) + .cookie(AUTH_COOKIE) + .extract() + .cookie(AUTH_COOKIE); + + assertNotEquals(token, newToken); + } + + @Test + void whenDistributeInvalidate_thenRequireCertificateAuthentication() { + given() + .log() + .all() + .when() + .get(URI.create(basePath + DISTRIBUTE_INVALIDATE_ENDPOINT + "/instanceId")) + .then() + .statusCode(403); + } + + @Test + void whenDistributeInvalidate_withCert_thenSuccess() { + given() + .config(SslContext.clientCertApiml) + .when() + .get(URI.create(basePath + DISTRIBUTE_INVALIDATE_ENDPOINT + "/instanceId")) + .then() + .statusCode(204); + } + + @Test + void whenInvalidateJwt_thenRequireCertificateAuthentication() { + var token = login(); + + given() + .log() + .all() + .when() + .delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT + "/" + token)) + .then() + .statusCode(403); + } + + @Test + void whenInvalidate_wrongMethod_thenFail() { + var token = login(); + + given() + .when() + .get(URI.create(basePath + INVALIDATE_JWT_ENDPOINT + "/" + token)) + .then() + .statusCode(SC_METHOD_NOT_ALLOWED); + } + + @Test + void whenInvalidateJwt_withCert_thenSuccess() { + var token = login(); + + given() + .config(SslContext.clientCertApiml) + .when() + .delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT + "/" + token)) + .then() + .statusCode(200); + } + } diff --git a/apiml/src/test/java/org/zowe/apiml/controller/ReactiveAuthenticationControllerTest.java b/apiml/src/test/java/org/zowe/apiml/controller/ReactiveAuthenticationControllerTest.java index 848bbe1d4c..d807e2698e 100644 --- a/apiml/src/test/java/org/zowe/apiml/controller/ReactiveAuthenticationControllerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/controller/ReactiveAuthenticationControllerTest.java @@ -22,7 +22,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; -import org.springframework.http.ResponseEntity; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.core.context.ReactiveSecurityContextHolder; @@ -86,17 +85,13 @@ void login_success() { @Test void invalidateJwtToken_success() { String jwtToInvalidate = "some.jwt.token"; - String path = "/gateway/api/v1/auth/invalidate/" + jwtToInvalidate; - MockServerHttpRequest mockRequest = MockServerHttpRequest.delete(path).build(); - MockServerWebExchange exchange = MockServerWebExchange.from(mockRequest); - Applications mockApplications = mock(Applications.class); Application mockApplication = mock(Application.class); when(peerAwareInstanceRegistry.getApplications()).thenReturn(mockApplications); when(mockApplications.getRegisteredApplications(CoreService.GATEWAY.getServiceId())).thenReturn(mockApplication); when(authenticationService.invalidateJwtTokenGateway(eq(jwtToInvalidate), eq(false), any(Application.class))).thenReturn(true); - Mono> result = controller.invalidateJwtToken(exchange); + var result = controller.invalidateJwtToken(jwtToInvalidate); StepVerifier.create(result) .expectNextMatches(responseEntity -> HttpStatus.OK.equals(responseEntity.getStatusCode())) @@ -106,17 +101,13 @@ void invalidateJwtToken_success() { @Test void invalidateJwtToken_serviceUnavailable() { String jwtToInvalidate = "some.jwt.token"; - String path = "/gateway/api/v1/auth/invalidate/" + jwtToInvalidate; - MockServerHttpRequest mockRequest = MockServerHttpRequest.delete(path).build(); - MockServerWebExchange exchange = MockServerWebExchange.from(mockRequest); - Applications mockApplications = mock(Applications.class); Application mockApplication = mock(Application.class); when(peerAwareInstanceRegistry.getApplications()).thenReturn(mockApplications); when(mockApplications.getRegisteredApplications(CoreService.GATEWAY.getServiceId())).thenReturn(mockApplication); when(authenticationService.invalidateJwtTokenGateway(eq(jwtToInvalidate), eq(false), any(Application.class))).thenReturn(false); - Mono> result = controller.invalidateJwtToken(exchange); + var result = controller.invalidateJwtToken(jwtToInvalidate); StepVerifier.create(result) .expectNextMatches(responseEntity -> HttpStatus.SERVICE_UNAVAILABLE.equals(responseEntity.getStatusCode())) @@ -126,10 +117,6 @@ void invalidateJwtToken_serviceUnavailable() { @Test void invalidateJwtToken_tokenNotValidException() { String jwtToInvalidate = "invalid.jwt.token"; - String path = "/gateway/api/v1/auth/invalidate/" + jwtToInvalidate; - MockServerHttpRequest mockRequest = MockServerHttpRequest.delete(path).build(); - MockServerWebExchange exchange = MockServerWebExchange.from(mockRequest); - Applications mockApplications = mock(Applications.class); Application mockApplication = mock(Application.class); when(peerAwareInstanceRegistry.getApplications()).thenReturn(mockApplications); @@ -137,7 +124,7 @@ void invalidateJwtToken_tokenNotValidException() { when(authenticationService.invalidateJwtTokenGateway(eq(jwtToInvalidate), eq(false), any(Application.class))) .thenThrow(new TokenNotValidException("Token is not valid")); - Mono> result = controller.invalidateJwtToken(exchange); + var result = controller.invalidateJwtToken(jwtToInvalidate); StepVerifier.create(result) .expectNextMatches(responseEntity -> HttpStatus.BAD_REQUEST.equals(responseEntity.getStatusCode())) diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml index 1117a2de09..4afbd8913b 100644 --- a/apiml/src/test/resources/application.yml +++ b/apiml/src/test/resources/application.yml @@ -29,6 +29,52 @@ apiml: userid: eureka # Userid that Eureka server will use to check authentication of its clients (other services) password: password # Password that Eureka server will use to check authentication of its clients (other services) allPeersUrls: http://${apiml.discovery.userid}:${apiml.discovery.password}@${apiml.service.hostname}:${apiml.service.port}/eureka/ + gateway: + eureka: + instance: + instanceId: ${apiml.service.hostname}:${apiml.service.id}:${apiml.service.port} + hostname: ${apiml.service.hostname} + #ports are computed in code + homePageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/ + healthCheckUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/application/health + healthCheckUrlPath: /application/health + port: ${apiml.service.port} + securePort: ${apiml.service.port} + nonSecurePortEnabled: ${apiml.service.nonSecurePortEnabled} + securePortEnabled: ${apiml.service.securePortEnabled} + statusPageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/application/info + statusPageUrlPath: /application/info + metadata-map: + apiml: + registrationType: primary + apiBasePath: /gateway/api/v1 + catalog: + tile: + id: apimediationlayer + title: API Mediation Layer API + description: The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation. + version: 1.0.0 + routes: + api_v1: + gatewayUrl: / + serviceUrl: / + apiInfo: + - apiId: zowe.apiml.gateway + version: 1.0.0 + gatewayUrl: api/v1 + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/gateway/api-docs + documentationUrl: https://zowe.github.io/docs-site/ + service: + title: API Gateway + description: API Gateway service to route requests to services registered in the API Mediation Layer and provides an API for mainframe security. + supportClientCertForwarding: true + apimlId: ${apiml.service.apimlId:${apiml.service.hostname}_${apiml.service.port}} + externalUrl: ${apiml.service.externalUrl:${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}} + authentication: + sso: true + registry: + enabled: false + metadata-key-allow-list: zos.sysname,zos.system,zos.sysplex,zos.cpcName,zos.zosName,zos.lpar service: apimlId: apiml1 corsEnabled: true @@ -49,6 +95,7 @@ apiml: auth: provider: zosmf zosmf: + jwtAutoconfiguration: ltpa serviceId: zosmf saf: urls: @@ -69,6 +116,8 @@ server: trustStoreType: PKCS12 spring: + application: + name: gateway main: allow-circular-references: true banner-mode: ${apiml.banner:"console"} diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 78a1b24fd6..65652de92d 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -47,12 +47,14 @@ dependencyResolutionManagement { version('jakartaInject', '2.0.1') version('jakartaServlet', '6.1.0') version('javaxAnnotation', '1.3.2') + + // Eureka requires this specific version of Jakarta JAXB bindings version('jaxbApi') { - strictly '[2.3.3,3.0.0[' + strictly '[2.3.3,3.0.0)' prefer '2.3.3' } version('jaxbImpl') { - strictly '[2.3.9,3.0.0[' + strictly '[2.3.9,3.0.0)' prefer '2.3.9' } version('jbossLogging', '3.6.1.Final') diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayChaoticTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayChaoticTest.java index 5ea0ed7063..d2059861f2 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayChaoticTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayChaoticTest.java @@ -27,12 +27,15 @@ */ @ChaoticHATest public class GatewayChaoticTest { + private final HAGatewayRequests haGatewayRequests = new HAGatewayRequests(); @Nested class GivenHASetUp { + @Nested class whenOneGatewayIsNotAvailable { + @Test void routeToInstanceThroughAliveGateway() { assumeTrue(haGatewayRequests.existing() > 1); @@ -42,7 +45,9 @@ void routeToInstanceThroughAliveGateway() { JsonResponse result = haGatewayRequests.route(1, Endpoints.DISCOVERABLE_GREET); assertThat(result.getStatus(), is(SC_OK)); } + } + } } diff --git a/keystore/docker/all-services.ext b/keystore/docker/all-services.ext index fe278aa23a..484a1dacc0 100644 --- a/keystore/docker/all-services.ext +++ b/keystore/docker/all-services.ext @@ -26,3 +26,4 @@ DNS.20 = gateway-service-2 DNS.21 = central-gateway-service DNS.22 = central-gateway-service-2 DNS.23 = apiml +DNS.24 = apiml-2 diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java index f6a656fce5..9830f71770 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java @@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; @@ -41,14 +42,27 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.*; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.token.TokenExpireException; +import org.zowe.apiml.security.common.token.TokenFormatNotValidException; +import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.util.CacheUtils; import org.zowe.apiml.util.EurekaUtils; import org.zowe.apiml.zaas.controllers.AuthController; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import static org.zowe.apiml.zaas.security.service.JwtUtils.getJwtClaims; import static org.zowe.apiml.zaas.security.service.JwtUtils.handleJwtParserException; @@ -63,6 +77,7 @@ @RequiredArgsConstructor @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @EnableAspectJAutoProxy(proxyTargetClass = true) +@ConditionalOnMissingBean(name = "modulithConfig") public class AuthenticationService { public static final String LTPA_CLAIM_NAME = "ltpa"; @@ -148,6 +163,8 @@ public QueryResponse parseJwtWithSignature(String jwt) throws SignatureException * - on logout phase (distribute = true) * - from another ZAAS instance to notify about change (distribute = false) * + * Note: This method should not be called from modulith-mode + * * @param jwtToken token to invalidate * @param distribute distribute invalidation to another instances? * @return state of invalidate (true - token was invalidated) @@ -213,18 +230,29 @@ public Boolean invalidateJwtTokenGateway(String jwtToken, boolean distribute, Ap return invalidate(jwtToken, distribute, app); } - private boolean invalidateTokenOnAnotherInstance(String jwtToken, Application application) { + /** + * Obtain URL to use to invalidate a JWT + * + * @param instanceInfo Registration data for the authentication service used + * @param jwtToken JWT token to invalidate + * @return + */ + protected String getInvalidateUrl(InstanceInfo instanceInfo, String jwtToken) { + return EurekaUtils.getUrl(instanceInfo) + AuthController.CONTROLLER_PATH + "/invalidate/" + jwtToken; + } - // wrong state, ZAAS have to exists (at least this current instance), return false like unsuccessful + private boolean invalidateTokenOnAnotherInstance(String jwtToken, Application application) { if (application == null) { return Boolean.FALSE; } final String myInstanceId = eurekaClient.getApplicationInfoManager().getInfo().getInstanceId(); for (final InstanceInfo instanceInfo : application.getInstances()) { - if (StringUtils.equals(myInstanceId, instanceInfo.getInstanceId())) continue; + if (StringUtils.equals(myInstanceId, instanceInfo.getInstanceId())) { + continue; + } - final String url = EurekaUtils.getUrl(instanceInfo) + AuthController.CONTROLLER_PATH + "/invalidate/" + jwtToken; + final String url = getInvalidateUrl(instanceInfo, jwtToken); try { restTemplate.delete(url); } catch (HttpClientErrorException e) { @@ -321,17 +349,20 @@ public TokenAuthentication createTokenAuthentication(String user, String jwtToke * in argument toInstanceId. If instance cannot be find it return false. A notification can throw an runtime * exception. In all other cases all invalidated token are distributed and method returns true. * + * Node: This method should not be used in modulith-mode + * * @param toInstanceId instanceId of ZAAS where invalidated JWT token should be sent * @return true if all token were sent, otherwise false */ public boolean distributeInvalidate(String toInstanceId) { - final Application application = eurekaClient.getApplication(CoreService.ZAAS.getServiceId()); - if (application == null) return false; + var zaas = eurekaClient.getApplication(CoreService.ZAAS.getServiceId()); + + if (zaas == null) return false; - final InstanceInfo instanceInfo = application.getByInstanceId(toInstanceId); + final InstanceInfo instanceInfo = zaas.getByInstanceId(toInstanceId); if (instanceInfo == null) return false; - final String url = EurekaUtils.getUrl(instanceInfo) + AuthController.CONTROLLER_PATH + "/invalidate/{}"; + var url = EurekaUtils.getUrl(instanceInfo) + AuthController.CONTROLLER_PATH + "/invalidate/{}"; final Collection invalidated = cacheUtils.getAllRecords(cacheManager, CACHE_INVALIDATED_JWT_TOKENS); for (final String invalidatedToken : invalidated) { diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java new file mode 100644 index 0000000000..9f82203bc9 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java @@ -0,0 +1,49 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.security.service; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.cache.CacheManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.util.CacheUtils; +import org.zowe.apiml.util.EurekaUtils; +import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; + +@Slf4j +@Service +@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) +@EnableAspectJAutoProxy(proxyTargetClass = true) +@ConditionalOnBean(name = "modulithConfig") +public class ModulithAuthenticationService extends AuthenticationService { + + public ModulithAuthenticationService(ApplicationContext applicationContext, + AuthConfigurationProperties authConfigurationProperties, JwtSecurity jwtSecurityInitializer, + ZosmfService zosmfService, EurekaClient eurekaClient, RestTemplate restTemplate, CacheManager cacheManager, + CacheUtils cacheUtils) { + super(applicationContext, authConfigurationProperties, jwtSecurityInitializer, zosmfService, eurekaClient, restTemplate, + cacheManager, cacheUtils); + } + + @Override + protected String getInvalidateUrl(InstanceInfo instanceInfo, String jwtToken) { + return EurekaUtils.getUrl(instanceInfo) + "/gateway/api/v1/auth/invalidate/" + jwtToken; + } + +} From 2181db8983655b3fbde582a61e648e026ccb4695 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 4 Jul 2025 00:43:18 +0000 Subject: [PATCH 006/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.2.21'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d337b5a7ba..c09386592d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.21-SNAPSHOT +version=3.2.21 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 35d724161129d45a79c85b5f27232c047cf7777d Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 4 Jul 2025 00:43:20 +0000 Subject: [PATCH 007/152] [Gradle Release plugin] Create new version: 'v3.2.22-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c09386592d..e8a181d757 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.21 +version=3.2.22-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 783b74f65e597304bba25d5c4c231e7c2579b62e Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 4 Jul 2025 00:43:21 +0000 Subject: [PATCH 008/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 9d0a544209..67ce380af9 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,4 +8,4 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.2.21-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.2.22-SNAPSHOT From 13aa5563d01eb58aa03f28f8cae195f33279920a Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:48:15 +0200 Subject: [PATCH 009/152] chore: Update all non-major dependencies (v3.x.x) (#4134) Signed-off-by: Renovate Bot Signed-off-by: Gowtham Selvaraj --- gradle/versions.gradle | 34 ++++++++++++------------ gradle/wrapper/gradle-wrapper.properties | 2 +- integration-tests/build.gradle | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 65652de92d..0cfc372672 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -5,8 +5,8 @@ dependencyResolutionManagement { version('projectNode', '20.14.0') version('projectNpm', '10.7.0') - version('springBoot', '3.5.0') - version('springBootGraphQl', '3.5.0') + version('springBoot', '3.5.3') + version('springBootGraphQl', '3.5.3') version('springCloudNetflix', '4.3.0') version('springCloudCommons', '4.3.0') version('springCloudCB', '3.3.0') @@ -14,8 +14,8 @@ dependencyResolutionManagement { version('springFramework', '6.2.8') version('springRetry', '2.0.12') - version('modulith', '1.4.0') - version('jmolecules', '2023.3.1') + version('modulith', '1.4.1') + version('jmolecules', '2023.3.2') version('glassfishHk2', '3.1.1') version('zosUtils', '2.0.6') @@ -23,7 +23,7 @@ dependencyResolutionManagement { version('awaitility', '4.3.0') version('bouncyCastle', '1.81') version('caffeine', '3.2.1') - version('checkerQual', '3.49.4') + version('checkerQual', '3.49.5') version('commonsLang3', '3.17.0') version('commonsLogging', '1.3.5') version('commonsText', '1.13.1') @@ -31,7 +31,7 @@ dependencyResolutionManagement { version('ehCache', '3.10.8') version('eureka', '2.0.5') version('netflixServo', '0.13.2') - version('googleErrorprone', '2.38.0') + version('googleErrorprone', '2.39.0') version('gradleGitProperties', '2.5.0') // Used in classpath dependencies version('googleGson', '2.13.1') version('guava', '33.4.8-jre') @@ -39,9 +39,9 @@ dependencyResolutionManagement { version('httpClient4', '4.5.14') version('httpClient5', '5.5') version('infinispan', '15.2.4.Final') - version('jacksonCore', '2.19.0') - version('jacksonDatabind', '2.19.0') - version('jacksonDataformatYaml', '2.19.0') + version('jacksonCore', '2.19.1') + version('jacksonDatabind', '2.19.1') + version('jacksonDataformatYaml', '2.19.1') version('janino', '3.1.12') version('jakartaValidation', '3.1.1') version('jakartaInject', '2.0.1') @@ -59,19 +59,19 @@ dependencyResolutionManagement { } version('jbossLogging', '3.6.1.Final') version('jerseySun', '1.19.4') - version('jettyWebSocketClient', '12.0.22') + version('jettyWebSocketClient', '12.0.23') version('jettison', '1.5.4') //0.12.x version contains breaking changes version('jjwt', '0.12.6') version('jodaTime', '2.14.0') version('jsonPath', '2.9.0') version('jsonSmart', '2.5.2') - version('junitJupiter', '5.13.1') - version('junitPlatform', '1.13.1') + version('junitJupiter', '5.13.3') + version('junitPlatform', '1.13.3') version('jxpath', '1.4.0') version('lettuce', '6.7.1.RELEASE') // force version in build.gradle file - compatibility with Slf4j - version('log4j', '2.24.3') + version('log4j', '2.25.0') version('lombok', '1.18.38') version('netty', '4.2.2.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 @@ -84,10 +84,10 @@ dependencyResolutionManagement { version('restAssured', '5.5.5') version('rhino', '1.8.0') version('springDoc', '2.8.9') - version('swaggerCore', '2.2.32') + version('swaggerCore', '2.2.34') version('swaggerInflector', '2.0.13') version('swagger2Parser', '1.0.75') - version('swagger3Parser', '2.1.29') + version('swagger3Parser', '2.1.30') version('thymeleaf', '3.1.3.RELEASE') version('velocity', '2.4.1') version('woodstoxCore', '7.1.1') @@ -100,9 +100,9 @@ dependencyResolutionManagement { version('reactorBom', '2023.0') version('gradleTestLogger', '4.0.0') version('testLogger', '4.0.0') - version('micronautPlatform', '4.6.1') + version('micronautPlatform', '4.8.3') version('micronaut', '4.8.18') - version('micronautPlugin', '4.5.3') + version('micronautPlugin', '4.5.4') version('shadow', '8.1.1') version('checkstyle', '10.17.0') version('jacoco', '0.8.11') diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 002b867c48..d4081da476 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index b61375161c..ceba004e88 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -25,7 +25,7 @@ configurations.all { dependencies { testImplementation project(':apiml-security-common') testImplementation project(':zaas-client') - testImplementation group: 'org.springframework.graphql', name: 'spring-graphql-test', version: '1.4.0' + testImplementation group: 'org.springframework.graphql', name: 'spring-graphql-test', version: '1.4.1' testImplementation libs.spring.boot.starter.actuator From 65436028fe2d41849a34d6df8a9d08e0a6ca0010 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:35:50 +0200 Subject: [PATCH 010/152] chore: Update all non-major dependencies (v3.x.x) (#4199) Signed-off-by: Gowtham Selvaraj --- gradle/versions.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 0cfc372672..38b4284dc0 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -100,8 +100,8 @@ dependencyResolutionManagement { version('reactorBom', '2023.0') version('gradleTestLogger', '4.0.0') version('testLogger', '4.0.0') - version('micronautPlatform', '4.8.3') - version('micronaut', '4.8.18') + version('micronautPlatform', '4.9.0') + version('micronaut', '4.9.7') version('micronautPlugin', '4.5.4') version('shadow', '8.1.1') version('checkstyle', '10.17.0') From 71c78b5af80be15b77cd4531abbd5376c715b216 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 11 Jul 2025 00:45:41 +0000 Subject: [PATCH 011/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.2.22'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e8a181d757..02ddb90671 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.22-SNAPSHOT +version=3.2.22 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 6f438a86b24373bcf9b15f6832e94fedf70621a7 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 11 Jul 2025 00:45:42 +0000 Subject: [PATCH 012/152] [Gradle Release plugin] Create new version: 'v3.2.23-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 02ddb90671..20eca9acaf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.22 +version=3.2.23-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 99e60f32fe6903ffa690d4d0569e307b0b230fbe Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 11 Jul 2025 00:45:43 +0000 Subject: [PATCH 013/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 67ce380af9..5764830851 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,4 +8,4 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.2.22-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.2.23-SNAPSHOT From 363a724a614e1bc8c82601e13db02d46a8c96fca Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:33:29 +0200 Subject: [PATCH 014/152] chore: Update all non-major dependencies (v3.x.x) (#4200) Signed-off-by: Gowtham Selvaraj --- gradle/versions.gradle | 14 +- .../package-lock.json | 12 +- .../package.json | 4 +- onboarding-enabler-nodejs/package-lock.json | 132 ++-- onboarding-enabler-nodejs/package.json | 8 +- package-lock.json | 8 +- package.json | 2 +- .../package-lock.json | 615 ++++++++++-------- zowe-cli-id-federation-plugin/package.json | 30 +- 9 files changed, 431 insertions(+), 394 deletions(-) diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 38b4284dc0..aaac2dafb3 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -18,21 +18,21 @@ dependencyResolutionManagement { version('jmolecules', '2023.3.2') version('glassfishHk2', '3.1.1') - version('zosUtils', '2.0.6') + version('zosUtils', '2.0.7') version('aws', '1.12.787') version('awaitility', '4.3.0') version('bouncyCastle', '1.81') - version('caffeine', '3.2.1') + version('caffeine', '3.2.2') version('checkerQual', '3.49.5') - version('commonsLang3', '3.17.0') + version('commonsLang3', '3.18.0') version('commonsLogging', '1.3.5') version('commonsText', '1.13.1') version('commonsIo', '2.19.0') version('ehCache', '3.10.8') version('eureka', '2.0.5') version('netflixServo', '0.13.2') - version('googleErrorprone', '2.39.0') - version('gradleGitProperties', '2.5.0') // Used in classpath dependencies + version('googleErrorprone', '2.40.0') + version('gradleGitProperties', '2.5.2') // Used in classpath dependencies version('googleGson', '2.13.1') version('guava', '33.4.8-jre') version('hamcrest', '3.0') @@ -71,7 +71,7 @@ dependencyResolutionManagement { version('jxpath', '1.4.0') version('lettuce', '6.7.1.RELEASE') // force version in build.gradle file - compatibility with Slf4j - version('log4j', '2.25.0') + version('log4j', '2.25.1') version('lombok', '1.18.38') version('netty', '4.2.2.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 @@ -87,7 +87,7 @@ dependencyResolutionManagement { version('swaggerCore', '2.2.34') version('swaggerInflector', '2.0.13') version('swagger2Parser', '1.0.75') - version('swagger3Parser', '2.1.30') + version('swagger3Parser', '2.1.31') version('thymeleaf', '3.1.3.RELEASE') version('velocity', '2.4.1') version('woodstoxCore', '7.1.1') diff --git a/onboarding-enabler-nodejs-sample-app/package-lock.json b/onboarding-enabler-nodejs-sample-app/package-lock.json index bf1e598f0a..0c361bc466 100644 --- a/onboarding-enabler-nodejs-sample-app/package-lock.json +++ b/onboarding-enabler-nodejs-sample-app/package-lock.json @@ -13,8 +13,8 @@ "express": "4.21.2" }, "engines": { - "node": "=20.19.2", - "npm": "=10.9.2" + "node": "=20.19.3", + "npm": "=10.9.3" } }, "../onboarding-enabler-nodejs": { @@ -30,12 +30,12 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.2.0", + "chai": "5.2.1", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", "eslint-plugin-import": "1.16.0", - "gulp": "5.0.0", + "gulp": "5.0.1", "gulp-babel": "7.0.1", "gulp-env": "0.4.0", "gulp-eslint": "6.0.0", @@ -46,8 +46,8 @@ "sinon-chai": "4.0.0" }, "engines": { - "node": "=20.19.2", - "npm": "=10.9.2" + "node": "=20.19.3", + "npm": "=10.9.3" } }, "node_modules/@zowe/apiml-onboarding-enabler-nodejs": { diff --git a/onboarding-enabler-nodejs-sample-app/package.json b/onboarding-enabler-nodejs-sample-app/package.json index 4735b9a697..d7ce4ac95b 100755 --- a/onboarding-enabler-nodejs-sample-app/package.json +++ b/onboarding-enabler-nodejs-sample-app/package.json @@ -22,7 +22,7 @@ "tough-cookie": "5.1.2" }, "engines": { - "npm": "=10.9.2", - "node": "=20.19.2" + "npm": "=10.9.3", + "node": "=20.19.3" } } diff --git a/onboarding-enabler-nodejs/package-lock.json b/onboarding-enabler-nodejs/package-lock.json index 6cc5d46b78..4d29425d7f 100644 --- a/onboarding-enabler-nodejs/package-lock.json +++ b/onboarding-enabler-nodejs/package-lock.json @@ -17,12 +17,12 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.2.0", + "chai": "5.2.1", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", "eslint-plugin-import": "1.16.0", - "gulp": "5.0.0", + "gulp": "5.0.1", "gulp-babel": "7.0.1", "gulp-env": "0.4.0", "gulp-eslint": "6.0.0", @@ -33,8 +33,8 @@ "sinon-chai": "4.0.0" }, "engines": { - "node": "=20.19.2", - "npm": "=10.9.2" + "node": "=20.19.3", + "npm": "=10.9.3" } }, "node_modules/@babel/code-frame": { @@ -74,7 +74,7 @@ }, "node_modules/@gulpjs/to-absolute-glob": { "version": "4.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", "dev": true, "license": "MIT", @@ -1621,9 +1621,9 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -1634,7 +1634,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -1795,13 +1795,6 @@ "node": ">=0.8" } }, - "node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", - "dev": true, - "license": "MIT" - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/co/-/co-4.6.0.tgz", @@ -2143,7 +2136,7 @@ }, "node_modules/detect-file": { "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/detect-file/-/detect-file-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "dev": true, "license": "MIT", @@ -3136,7 +3129,7 @@ }, "node_modules/expand-tilde": { "version": "2.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, "license": "MIT", @@ -3271,9 +3264,9 @@ } }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3342,7 +3335,7 @@ }, "node_modules/findup-sync": { "version": "5.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/findup-sync/-/findup-sync-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "dev": true, "license": "MIT", @@ -3358,7 +3351,7 @@ }, "node_modules/fined": { "version": "2.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/fined/-/fined-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, "license": "MIT", @@ -3375,7 +3368,7 @@ }, "node_modules/flagged-respawn": { "version": "2.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true, "license": "MIT", @@ -3601,7 +3594,7 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/glob-parent/-/glob-parent-6.0.2.tgz", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", @@ -3613,9 +3606,9 @@ } }, "node_modules/glob-stream": { - "version": "8.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/glob-stream/-/glob-stream-8.0.2.tgz", - "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.3.tgz", + "integrity": "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==", "dev": true, "license": "MIT", "dependencies": { @@ -3648,7 +3641,7 @@ }, "node_modules/global-modules": { "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/global-modules/-/global-modules-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, "license": "MIT", @@ -3663,7 +3656,7 @@ }, "node_modules/global-prefix": { "version": "1.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/global-prefix/-/global-prefix-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, "license": "MIT", @@ -3722,16 +3715,16 @@ "license": "ISC" }, "node_modules/gulp": { - "version": "5.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/gulp/-/gulp-5.0.0.tgz", - "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.1.tgz", + "integrity": "sha512-PErok3DZSA5WGMd6XXV3IRNO0mlB+wW3OzhFJLEec1jSERg2j1bxJ6e5Fh6N6fn3FH2T9AP4UYNb/pYlADB9sA==", "dev": true, "license": "MIT", "dependencies": { "glob-watcher": "^6.0.0", - "gulp-cli": "^3.0.0", + "gulp-cli": "^3.1.0", "undertaker": "^2.0.0", - "vinyl-fs": "^4.0.0" + "vinyl-fs": "^4.0.2" }, "bin": { "gulp": "bin/gulp.js" @@ -3760,9 +3753,9 @@ } }, "node_modules/gulp-cli": { - "version": "3.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/gulp-cli/-/gulp-cli-3.0.0.tgz", - "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.1.0.tgz", + "integrity": "sha512-zZzwlmEsTfXcxRKiCHsdyjZZnFvXWM4v1NqBJSYbuApkvVKivjcmOS2qruAJ+PkEHLFavcDKH40DPc1+t12a9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3771,7 +3764,7 @@ "copy-props": "^4.0.0", "gulplog": "^2.2.0", "interpret": "^3.1.1", - "liftoff": "^5.0.0", + "liftoff": "^5.0.1", "mute-stdout": "^2.0.0", "replace-homedir": "^2.0.0", "semver-greatest-satisfied-range": "^2.0.0", @@ -4601,7 +4594,7 @@ }, "node_modules/homedir-polyfill": { "version": "1.0.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, "license": "MIT", @@ -4861,7 +4854,7 @@ }, "node_modules/is-absolute": { "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/is-absolute/-/is-absolute-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, "license": "MIT", @@ -4997,7 +4990,7 @@ }, "node_modules/is-negated-glob": { "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", "dev": true, "license": "MIT", @@ -5044,7 +5037,7 @@ }, "node_modules/is-relative": { "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/is-relative/-/is-relative-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, "license": "MIT", @@ -5084,7 +5077,7 @@ }, "node_modules/is-unc-path": { "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, "license": "MIT", @@ -5120,7 +5113,7 @@ }, "node_modules/is-windows": { "version": "1.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/is-windows/-/is-windows-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, "license": "MIT", @@ -5420,9 +5413,9 @@ } }, "node_modules/liftoff": { - "version": "5.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/liftoff/-/liftoff-5.0.0.tgz", - "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.1.tgz", + "integrity": "sha512-wwLXMbuxSF8gMvubFcFRp56lkFV69twvbU5vDPbaw+Q+/rF8j0HKjGbIdlSi+LuJm9jf7k9PB+nTxnsLMPcv2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5600,7 +5593,7 @@ }, "node_modules/map-cache": { "version": "0.2.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/map-cache/-/map-cache-0.2.2.tgz", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, "license": "MIT", @@ -5617,7 +5610,7 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/micromatch/-/micromatch-4.0.8.tgz", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", @@ -6038,7 +6031,7 @@ }, "node_modules/object.pick": { "version": "1.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/object.pick/-/object.pick-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, "license": "MIT", @@ -6160,7 +6153,7 @@ }, "node_modules/parse-filepath": { "version": "1.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, "license": "MIT", @@ -6185,7 +6178,7 @@ }, "node_modules/parse-passwd": { "version": "1.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true, "license": "MIT", @@ -6239,7 +6232,7 @@ }, "node_modules/path-root": { "version": "0.1.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/path-root/-/path-root-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, "license": "MIT", @@ -6252,7 +6245,7 @@ }, "node_modules/path-root-regex": { "version": "0.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true, "license": "MIT", @@ -6616,7 +6609,7 @@ }, "node_modules/rechoir": { "version": "0.8.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/rechoir/-/rechoir-0.8.0.tgz", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "license": "MIT", @@ -6834,7 +6827,7 @@ }, "node_modules/resolve-dir": { "version": "1.0.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, "license": "MIT", @@ -6917,9 +6910,9 @@ "license": "ISC" }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -7735,7 +7728,7 @@ }, "node_modules/unc-path-regex": { "version": "0.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "dev": true, "license": "MIT", @@ -7877,14 +7870,13 @@ "license": "MIT" }, "node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", "dev": true, "license": "MIT", "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -7908,14 +7900,14 @@ } }, "node_modules/vinyl-fs": { - "version": "4.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", - "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", + "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", "dev": true, "license": "MIT", "dependencies": { "fs-mkdirp-stream": "^2.0.1", - "glob-stream": "^8.0.0", + "glob-stream": "^8.0.3", "graceful-fs": "^4.2.11", "iconv-lite": "^0.6.3", "is-valid-glob": "^1.0.0", @@ -7926,7 +7918,7 @@ "streamx": "^2.14.0", "to-through": "^3.0.0", "value-or-function": "^4.0.0", - "vinyl": "^3.0.0", + "vinyl": "^3.0.1", "vinyl-sourcemap": "^2.0.0" }, "engines": { diff --git a/onboarding-enabler-nodejs/package.json b/onboarding-enabler-nodejs/package.json index e44f950594..4b70dd8366 100644 --- a/onboarding-enabler-nodejs/package.json +++ b/onboarding-enabler-nodejs/package.json @@ -27,12 +27,12 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.2.0", + "chai": "5.2.1", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", "eslint-plugin-import": "1.16.0", - "gulp": "5.0.0", + "gulp": "5.0.1", "gulp-babel": "7.0.1", "gulp-env": "0.4.0", "gulp-eslint": "6.0.0", @@ -52,7 +52,7 @@ ] }, "engines": { - "npm": "=10.9.2", - "node": "=20.19.2" + "npm": "=10.9.3", + "node": "=20.19.3" } } diff --git a/package-lock.json b/package-lock.json index 23db0fa596..43e2065164 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.0.0", "license": "EPL-2.0", "devDependencies": { - "concurrently": "9.1.2" + "concurrently": "9.2.0" } }, "node_modules/ansi-regex": { @@ -97,9 +97,9 @@ "dev": true }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "version": "9.2.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5e75985422..391700d4d2 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,6 @@ }, "homepage": "https://github.com/zowe/api-layer#readme", "devDependencies": { - "concurrently": "9.1.2" + "concurrently": "9.2.0" } } diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index e27ab2cfc1..a6710d54d3 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -12,18 +12,18 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@types/jest": "29.5.14", - "@types/node": "20.17.50", - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@zowe/cli": "8.21.0", - "@zowe/cli-test-utils": "8.21.0", - "@zowe/imperative": "8.21.0", + "@types/node": "20.19.7", + "@typescript-eslint/eslint-plugin": "8.36.0", + "@typescript-eslint/parser": "8.36.0", + "@zowe/cli": "8.24.4", + "@zowe/cli-test-utils": "8.24.2", + "@zowe/imperative": "8.24.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.27.0", - "eslint-plugin-jest": "28.11.0", + "eslint": "9.31.0", + "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.1.4", "globals": "15.15.0", @@ -36,17 +36,17 @@ "jest-junit": "16.0.0", "jest-stare": "2.5.2", "madge": "8.0.0", - "ts-jest": "29.3.4", + "ts-jest": "29.4.0", "ts-node": "10.9.2", - "typedoc": "0.28.4", + "typedoc": "0.28.7", "typescript": "5.8.3" }, "engines": { - "node": "=20.19.2", - "npm": "=10.9.2" + "node": "=20.19.3", + "npm": "=10.9.3" }, "peerDependencies": { - "@zowe/imperative": "8.21.0" + "@zowe/imperative": "8.24.2" } }, "node_modules/@ampproject/remapping": { @@ -130,16 +130,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -172,6 +172,16 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.24.7", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", @@ -274,13 +284,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -516,14 +526,14 @@ } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -531,38 +541,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -666,9 +666,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -681,9 +681,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -740,9 +740,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.31.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -777,16 +777,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.2.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@gerrit0/mini-shiki/-/mini-shiki-3.2.2.tgz", - "integrity": "sha512-vaZNGhGLKMY14HbF53xxHNgFO9Wz+t5lTlGNpl2N9xFiKQ0I5oIe0vKjU9dh7Nb3Dw6lZ7wqUE0ri+zcdpnK+Q==", + "version": "3.7.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@gerrit0/mini-shiki/-/mini-shiki-3.7.0.tgz", + "integrity": "sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.2.1", - "@shikijs/langs": "^3.2.1", - "@shikijs/themes": "^3.2.1", - "@shikijs/types": "^3.2.1", + "@shikijs/engine-oniguruma": "^3.7.0", + "@shikijs/langs": "^3.7.0", + "@shikijs/themes": "^3.7.0", + "@shikijs/types": "^3.7.0", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -1342,18 +1342,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1365,16 +1361,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1382,10 +1368,11 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1655,40 +1642,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.1.tgz", - "integrity": "sha512-wZZAkayEn6qu2+YjenEoFqj0OyQI64EWsNR6/71d1EkG4sxEOFooowKivsWPpaWNBu3sxAG+zPz5kzBL/SsreQ==", + "version": "3.7.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz", + "integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.2.1", + "@shikijs/types": "3.7.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/langs/-/langs-3.2.1.tgz", - "integrity": "sha512-If0iDHYRSGbihiA8+7uRsgb1er1Yj11pwpX1c6HLYnizDsKAw5iaT3JXj5ZpaimXSWky/IhxTm7C6nkiYVym+A==", + "version": "3.7.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/langs/-/langs-3.7.0.tgz", + "integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.2.1" + "@shikijs/types": "3.7.0" } }, "node_modules/@shikijs/themes": { - "version": "3.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/themes/-/themes-3.2.1.tgz", - "integrity": "sha512-k5DKJUT8IldBvAm8WcrDT5+7GA7se6lLksR+2E3SvyqGTyFMzU2F9Gb7rmD+t+Pga1MKrYFxDIeyWjMZWM6uBQ==", + "version": "3.7.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/themes/-/themes-3.7.0.tgz", + "integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.2.1" + "@shikijs/types": "3.7.0" } }, "node_modules/@shikijs/types": { - "version": "3.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/types/-/types-3.2.1.tgz", - "integrity": "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA==", + "version": "3.7.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/types/-/types-3.7.0.tgz", + "integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==", "dev": true, "license": "MIT", "dependencies": { @@ -2073,13 +2060,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.50", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.17.50.tgz", - "integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", + "version": "20.19.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.7.tgz", + "integrity": "sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/stack-utils": { @@ -2118,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", + "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/type-utils": "8.36.0", + "@typescript-eslint/utils": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2142,15 +2129,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.36.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -2171,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.36.0.tgz", + "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4" }, "engines": { @@ -2195,15 +2182,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", + "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.36.0", + "@typescript-eslint/types": "^8.36.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", + "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2213,15 +2222,32 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", + "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", + "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/utils": "8.36.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2251,9 +2277,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.36.0.tgz", + "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", "dev": true, "license": "MIT", "engines": { @@ -2265,14 +2291,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", + "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.36.0", + "@typescript-eslint/tsconfig-utils": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2292,9 +2320,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2331,16 +2359,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.36.0.tgz", + "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2355,14 +2383,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.36.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", + "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.36.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2373,9 +2401,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2442,25 +2470,25 @@ "dev": true }, "node_modules/@zowe/cli": { - "version": "8.21.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.21.0.tgz", - "integrity": "sha512-CXZyxiI5nn1YZCgf0mQw8JJUOzCiJ0XJ4nh2gwcFMiKE3fHTSZzYsHI5Wde7xdsugV44Yfi7aGcXgln33SUGTQ==", + "version": "8.24.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.24.4.tgz", + "integrity": "sha512-4rbJfEmlPeLVzAvnoKEw1sg9K+uVov9G9kEg1xZbi30bXv/hG92T1+rSifUhsXDxcv1nB+yH1J0JODKj4+apFw==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, "license": "EPL-2.0", "dependencies": { - "@zowe/core-for-zowe-sdk": "8.21.0", - "@zowe/imperative": "8.21.0", - "@zowe/provisioning-for-zowe-sdk": "8.21.0", - "@zowe/zos-console-for-zowe-sdk": "8.21.0", - "@zowe/zos-files-for-zowe-sdk": "8.21.0", - "@zowe/zos-jobs-for-zowe-sdk": "8.21.0", - "@zowe/zos-logs-for-zowe-sdk": "8.21.0", - "@zowe/zos-tso-for-zowe-sdk": "8.21.0", - "@zowe/zos-uss-for-zowe-sdk": "8.21.0", - "@zowe/zos-workflows-for-zowe-sdk": "8.21.0", - "@zowe/zosmf-for-zowe-sdk": "8.21.0", + "@zowe/core-for-zowe-sdk": "8.24.2", + "@zowe/imperative": "8.24.2", + "@zowe/provisioning-for-zowe-sdk": "8.24.2", + "@zowe/zos-console-for-zowe-sdk": "8.24.2", + "@zowe/zos-files-for-zowe-sdk": "8.24.2", + "@zowe/zos-jobs-for-zowe-sdk": "8.24.2", + "@zowe/zos-logs-for-zowe-sdk": "8.24.2", + "@zowe/zos-tso-for-zowe-sdk": "8.24.3", + "@zowe/zos-uss-for-zowe-sdk": "8.24.2", + "@zowe/zos-workflows-for-zowe-sdk": "8.24.2", + "@zowe/zosmf-for-zowe-sdk": "8.24.2", "find-process": "1.4.7", "lodash": "4.17.21", "minimatch": "9.0.5", @@ -2473,13 +2501,13 @@ "node": ">=18.12.0" }, "optionalDependencies": { - "@zowe/secrets-for-zowe-sdk": "8.18.3" + "@zowe/secrets-for-zowe-sdk": "8.24.2" } }, "node_modules/@zowe/cli-test-utils": { - "version": "8.21.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.21.0.tgz", - "integrity": "sha512-NDYvcN5xUbr1MdpKfo7pq+KFYodobeZqY20FtX5wl7HmOoUcbLvX5oy1WbNhm53JkKxsilCYgGgj4wcliePEdA==", + "version": "8.24.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.24.2.tgz", + "integrity": "sha512-kJcKgFPFv3IFw0Nty9RcExOINb/M0wiYuYbYPiXfvx1mdEvf0V4kOFCOBlpuDO+rKhYpg+QDg7B2mB9IU2Nc3A==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -3059,9 +3087,9 @@ "license": "MIT" }, "node_modules/@zowe/cli/node_modules/@zowe/core-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-6uKc6Q5X8DEaKk9mIAvOkUlFSwoOrklMQetyAxgOG8vxtk4k8jrWxVolyywa5mbvgPlH3TSewPwCzq7j5N2cGg==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-eo2JbXKREM1E6S6plynOu8aT7JQprzxkAAED0IkcOqzUPkU4X0zHhTeRexxkH1b50xD780ZXM/YttjcBusl7CA==", "dev": true, "dependencies": { "comment-json": "~4.2.3", @@ -3075,9 +3103,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/imperative": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.21.0.tgz", - "integrity": "sha512-r4ItPkBtqB5h9rMqWq0No/WF9gnTEy+g6XKoRMj9wpd1bv9QJ3UMt+YGK+4yAujDrieP87fm8KWhoyKKqNZNVg==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.24.2.tgz", + "integrity": "sha512-gMUYEVBfPMi9/g8VtNheP5qkc7Y1CSoU3I/LrnX57cJTi++UczZu4CeRMDCLAYKb266DIXIiHRmRVNhP17z+jQ==", "dev": true, "dependencies": { "@types/yargs": "^17.0.32", @@ -3124,9 +3152,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/imperative/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "engines": { "node": ">= 14" @@ -3203,9 +3231,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/provisioning-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-HfHxOSjZG5TkLDF23NNm0dGbvFYExe5TiK3N46bAd1b1dp7cF4biN6zOkjcAJkkF3ThO9m8ZJGFpwp1Rj25AgQ==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-7W3vRqtZDsMzGhWZes7/FDqTlfPB0Q4BfQE6ejhsTAW3IYonZoAbtTrX9ewA7oHQqIOX8pdwa9TtSgUCRtSKLg==", "dev": true, "dependencies": { "js-yaml": "^4.1.0" @@ -3219,9 +3247,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/secrets-for-zowe-sdk": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/@zowe/secrets-for-zowe-sdk/-/secrets-for-zowe-sdk-8.18.3.tgz", - "integrity": "sha512-AIO/FKCYbF4GQJf3ZyslF8gK4VQ5+bP5vuo8lsv32ahUO59Azz1JmCS/Od4VTMaecBOLVUla5O2AKfG2tUmjEg==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/secrets-for-zowe-sdk/-/secrets-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-b66ebBQTBlC/tSU4muC2DhgXfwO8gsFoCnli5oyAji4AHkBs36/KDmo7UNh0gQRPAVb6qc8MsRl0HBV0t/Oj/Q==", "dev": true, "hasInstallScript": true, "optional": true, @@ -3230,9 +3258,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-console-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-nmfBAjsAwwyo2c1ocp3b1Mql8R3K0gcJ26wMOCk8TvPaBCvaJnuJZo87P8vt+wQD/GV9bAdoQqlSiIE3hmI0VQ==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-dsfHAgFWhDbiD0/lNn3CiEnTDBXtDRZTjrIGTZDWEUo16gUvSrg+CoOmknZ1zzoMkYPLqyr8lVzkHcFeK5CWZw==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3243,9 +3271,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-files-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-eOFU0DIOU1nyRLI4a3cIAyfdKVU8N9dHIb7LUYWLAtOsd4WaHzaZ96oce/h1AhDxH8BBWmPsIcqWB3R3hCLk7g==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-1UwLC5zsFTNqtc6+kLBHAUjIP/DD9mTpbqyGe7imYJyK0/1sU+z5nzpMl5MnTYZVQ3Q+bzqYzHLlv4J/d7AuCw==", "dev": true, "dependencies": { "lodash": "^4.17.21", @@ -3260,12 +3288,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-jobs-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-evl0T9JMJorb6lpZzIf2sbaEdd7lrLAE+/W/XqTMFo5A9O0EgqZg56jMGX9VoVQbuBcW+BAsnCjAkgv46OYTkQ==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-R/y7SaaLZul6pHWadCaCt7Zrmwm3amOQ4OMm6O22vT1vfGf+bUKYleYu+nHmdS5/OIns79GdwQiHfLE4w2HNGA==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.21.0" + "@zowe/zos-files-for-zowe-sdk": "8.24.2" }, "engines": { "node": ">=18.12.0" @@ -3276,9 +3304,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-logs-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-8IXkqn1fkTtDalWiuCY4QCb9wAMHANJ9009vd45uKSxEOdiFnxOcXZCw4I1L09TNMvNxcF6pSX8fQ9poxFl18Q==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-Na8EjKHJNt8qVoTKwEqoVknLTuDiVvLF2VIgGd9+GnoCNX+CtmWJAAFg3quZ36QtC6ssJYCTc2hHT3qxhobX5g==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3289,12 +3317,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-tso-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-AhDNs4UAunyzDEobJ/QIxBtlLTlyiL4xCu0ZU39XRyfwodJnBlY+sp5jhxC2H0uKOfdYBVbL1wx7BtxP4XZQNQ==", + "version": "8.24.3", + "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.24.3.tgz", + "integrity": "sha512-WICorY6KchPojvGrDYh0xITXEdkcskkdj6bS1gzC3x1uKqRusnjVbfV+A03PtaoCtJzKDVSOcElkH46yrU98rw==", "dev": true, "dependencies": { - "@zowe/zosmf-for-zowe-sdk": "8.21.0" + "@zowe/zosmf-for-zowe-sdk": "8.24.2" }, "engines": { "node": ">=18.12.0" @@ -3305,9 +3333,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-uss-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-QirT/K8t51W1DvnHMcM+mRltkpRGG0xf3/VNXJ0e8kD7jeFRl8orjqJMOVcxMeu16fLRc2j8qe/syqNcocBj2w==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-8Eurw5yEqGALhXPMhsJpQaiwE2a9wD9v/Am21nWK17+TMYiNQi/CocZCszsSGHRo/+FbAklTP/i/XgyJDavC3g==", "dev": true, "dependencies": { "ssh2": "^1.15.0" @@ -3320,12 +3348,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-workflows-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-3veDlERT2Zoc3dwW8LzpcubXhSu7lFS5eUrUwDfydOZMHYhPD8/S1hXYFYKK1l00Px2viJwbzD6FV4KDs/XrLA==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-Sdlv7R4nCWfw91qBd5fa7sa+Jg8KIPZ6eOTxrfL72LWvCwASJT47hEdk+FeOJYpcGZuOpnFdZQOL/WajymwOvg==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.21.0" + "@zowe/zos-files-for-zowe-sdk": "8.24.2" }, "engines": { "node": ">=18.12.0" @@ -3336,9 +3364,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zosmf-for-zowe-sdk": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.21.0.tgz", - "integrity": "sha512-Uih5ozzooM9CDdgcdle2cMXv4v1sPf2yqEc/6gjk+4QhFi8xuU0uTG4AnZlo9VTGuUXx3dyzZxB9fKNTn8BMQg==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.24.2.tgz", + "integrity": "sha512-ZbB0o0NBONf5q9mW3Gp48iIsdmi++NFi7u8CceFpEerszwHFcRippDJZspPiDHNDThwxbvRK/ATwK/C0Pw5vVQ==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3415,6 +3443,16 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/@zowe/cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@zowe/cli/node_modules/braces": { "version": "3.0.3", "dev": true, @@ -4340,15 +4378,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@zowe/cli/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@zowe/cli/node_modules/minimist": { "version": "1.2.6", "dev": true, @@ -6006,9 +6035,9 @@ } }, "node_modules/@zowe/imperative": { - "version": "8.21.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.21.0.tgz", - "integrity": "sha512-r4ItPkBtqB5h9rMqWq0No/WF9gnTEy+g6XKoRMj9wpd1bv9QJ3UMt+YGK+4yAujDrieP87fm8KWhoyKKqNZNVg==", + "version": "8.24.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.24.2.tgz", + "integrity": "sha512-gMUYEVBfPMi9/g8VtNheP5qkc7Y1CSoU3I/LrnX57cJTi++UczZu4CeRMDCLAYKb266DIXIiHRmRVNhP17z+jQ==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -6072,9 +6101,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -7615,19 +7644,19 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.31.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7639,9 +7668,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7676,9 +7705,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.11.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", - "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", + "version": "28.14.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", + "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7727,9 +7756,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7756,6 +7785,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@humanwhocodes/retry/-/retry-0.4.3.tgz", @@ -7771,9 +7813,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7784,15 +7826,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7802,9 +7844,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -12376,16 +12418,15 @@ } }, "node_modules/ts-jest": { - "version": "29.3.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ts-jest/-/ts-jest-29.3.4.tgz", - "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "version": "29.4.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", @@ -12401,10 +12442,11 @@ }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { @@ -12422,6 +12464,9 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, @@ -12551,17 +12596,17 @@ } }, "node_modules/typedoc": { - "version": "0.28.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.4.tgz", - "integrity": "sha512-xKvKpIywE1rnqqLgjkoq0F3wOqYaKO9nV6YkkSat6IxOWacUCc/7Es0hR3OPmkIqkPoEn7U3x+sYdG72rstZQA==", + "version": "0.28.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.7.tgz", + "integrity": "sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.2.2", + "@gerrit0/mini-shiki": "^3.7.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.7.1" + "yaml": "^2.8.0" }, "bin": { "typedoc": "bin/typedoc" @@ -12575,9 +12620,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12628,9 +12673,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -12986,16 +13031,16 @@ "dev": true }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 93e6377fe4..3031893bb8 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -49,18 +49,18 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@types/jest": "29.5.14", - "@types/node": "20.17.50", - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@zowe/cli": "8.21.0", - "@zowe/cli-test-utils": "8.21.0", - "@zowe/imperative": "8.21.0", + "@types/node": "20.19.7", + "@typescript-eslint/eslint-plugin": "8.36.0", + "@typescript-eslint/parser": "8.36.0", + "@zowe/cli": "8.24.4", + "@zowe/cli-test-utils": "8.24.2", + "@zowe/imperative": "8.24.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.27.0", - "eslint-plugin-jest": "28.11.0", + "eslint": "9.31.0", + "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.1.4", "globals": "15.15.0", @@ -73,20 +73,20 @@ "jest-junit": "16.0.0", "jest-stare": "2.5.2", "madge": "8.0.0", - "ts-jest": "29.3.4", + "ts-jest": "29.4.0", "ts-node": "10.9.2", - "typedoc": "0.28.4", + "typedoc": "0.28.7", "typescript": "5.8.3" }, "overrides": { - "@babel/traverse": "7.27.1" + "@babel/traverse": "7.28.0" }, "peerDependencies": { - "@zowe/imperative": "8.21.0" + "@zowe/imperative": "8.24.2" }, "engines": { - "npm": "=10.9.2", - "node": "=20.19.2" + "npm": "=10.9.3", + "node": "=20.19.3" }, "jest": { "modulePathIgnorePatterns": [ From 9a71046714e464e105ea4112182c9f7bc6c43992 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 15 Jul 2025 08:32:47 +0200 Subject: [PATCH 015/152] chore: Enable Modulith HA tests (#4194) Signed-off-by: Pablo Carle Signed-off-by: ac892247 Co-authored-by: Pablo Carle Co-authored-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 674 ++++++++++++++++-- README.md | 20 + .../frontend/cypress.modulith.config.js | 40 ++ .../cypress/e2e/dashboard/dashboard.cy.js | 21 +- .../cypress/e2e/dashboard/wizard-dialog.cy.js | 2 +- .../cypress/e2e/detail-page/detail-page.cy.js | 11 +- .../multiple-gateway-services.cy.js | 4 +- .../detail-page/service-version-compare.cy.js | 15 +- .../detail-page/service-version-switch.cy.js | 3 + .../e2e/detail-page/swagger-rendering.cy.js | 120 ++-- .../swagger-try-out-and-code-snippets.cy.js | 2 + .../cypress/e2e/graphql/graphql-apiml.cy.js | 4 +- .../cypress/e2e/login/login-bad.cy.js | 3 +- .../cypress/e2e/login/login-oauth2.cy.js | 17 +- .../frontend/cypress/e2e/login/login-ok.cy.js | 3 +- api-catalog-ui/frontend/package.json | 3 +- .../actions/refresh-static-apis-actions.jsx | 12 +- .../eureka/client/ApimlPeerEurekaNode.java | 4 +- .../zowe/apiml/util/config/SslContext.java | 12 +- .../java/org/zowe/apiml/ApimlApplication.java | 5 + .../org/zowe/apiml/EurekaRestController.java | 2 +- .../zowe/apiml/GatewayHealthIndicator.java | 96 +++ .../java/org/zowe/apiml/ModulithConfig.java | 134 ++-- .../org/zowe/apiml/WebSecurityConfig.java | 17 +- .../zowe/apiml/ZaasSchemeTransformApi.java | 42 +- .../controller/ApimlExceptionHandler.java | 9 + .../src/main/resources/apiml-log-messages.yml | 24 - apiml/src/main/resources/application.yml | 5 + .../apiml/GatewayHealthIndicatorTest.java | 169 +++++ .../apiml/ZaasSchemeTransformApiTest.java | 425 ++++++----- .../zowe/apiml/acceptance/AcceptanceTest.java | 27 +- .../acceptance/EurekaEndpointsTests.java | 18 + .../controller/ApimlExceptionHandlerTest.java | 69 ++ config/local-multi/apiml-1.yml | 73 ++ config/local-multi/apiml-2.yml | 69 ++ config/local/apiml-service.yml | 2 + .../discovery/ApimlInstanceRegistry.java | 32 +- .../StaticServicesRegistrationService.java | 4 +- .../discovery/ApimlInstanceRegistryTest.java | 24 +- .../metadata/MetadataDefaultsServiceTest.java | 2 +- ...StaticServicesRegistrationServiceTest.java | 2 +- docker-compose.yml | 102 +++ .../config/GatewayHealthIndicator.java | 17 +- .../gateway/config/ServiceCorsUpdater.java | 26 +- .../filters/AbstractAuthSchemeFactory.java | 2 +- .../acceptance/CorsPerServiceTest.java | 2 +- .../config/ServiceCorsUpdaterTest.java | 19 +- gradle/jib.gradle | 2 +- integration-tests/build.gradle | 18 +- .../providers/SafLoginTest.java | 6 + .../providers/SafLogoutTest.java | 8 + .../schemes/PassticketSchemeTest.java | 42 +- .../integration/ha/AuthenticationHaTest.java | 75 ++ .../integration/ha/EurekaReplicationTest.java | 13 + .../ha/GatewayMultipleInstancesTest.java | 6 + .../impl/ApiMediationLayerStartupChecker.java | 38 +- .../org/zowe/apiml/util/SecurityUtils.java | 31 +- .../apiml/util/categories/SAFAuthTest.java | 2 +- .../zowe/apiml/util/config/ConfigReader.java | 3 +- .../apiml/util/http/HttpRequestUtils.java | 26 +- ...nment-configuration-docker-modulith-ha.yml | 65 ++ keystore/docker/all-services.keystore.p12 | Bin 4521 -> 5854 bytes .../apiml/zaas/ZaasServiceAvailableEvent.java | 23 + .../zowe/apiml/zaas/ZaasStartupListener.java | 3 + .../dummy/DummyAuthenticationProvider.java | 13 +- .../dummy/InMemoryUserDetailsService.java | 11 +- .../service/AuthenticationService.java | 46 +- .../DummyAuthenticationProviderTest.java | 4 +- 68 files changed, 2262 insertions(+), 561 deletions(-) create mode 100644 api-catalog-ui/frontend/cypress.modulith.config.js create mode 100644 apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java delete mode 100644 apiml/src/main/resources/apiml-log-messages.yml create mode 100644 apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java create mode 100644 apiml/src/test/java/org/zowe/apiml/controller/ApimlExceptionHandlerTest.java create mode 100644 config/local-multi/apiml-1.yml create mode 100644 config/local-multi/apiml-2.yml create mode 100644 docker-compose.yml create mode 100644 integration-tests/src/test/java/org/zowe/apiml/integration/ha/AuthenticationHaTest.java create mode 100644 integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasServiceAvailableEvent.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 46f85f75d4..1e6c737f1f 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -78,6 +78,7 @@ jobs: - uses: ./.github/actions/setup - name: Run Modulith CI Tests + timeout-minutes: 4 run: > ENV_CONFIG=docker-modulith ./gradlew runStartUpCheck :integration-tests:runContainerModulithTests --info -Ddiscoverableclient.instances=1 -Denvironment.config=-docker-modulith -Denvironment.modulith=true @@ -97,6 +98,250 @@ jobs: - uses: ./.github/actions/teardown + CITestsModulithSAFProviderHA: + needs: PublishJibContainers + runs-on: ubuntu-latest + container: ubuntu:latest + timeout-minutes: 15 + + services: + apiml: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_AUTH_JWT_CUSTOMAUTHHEADER: customJwtHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMUSERHEADER: customUserHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader + APIML_SECURITY_X509_ENABLED: true + SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 + SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient + APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken + APIML_SECURITY_AUTH_PROVIDER: saf + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml + logbackService: ZWEAGW1 + apiml-2: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_AUTH_JWT_CUSTOMAUTHHEADER: customJwtHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMUSERHEADER: customUserHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader + APIML_SECURITY_X509_ENABLED: true + SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 + SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient + APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken + APIML_SECURITY_AUTH_PROVIDER: saf + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml-2 + logbackService: ZWEAGW2 + api-catalog-services: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 + api-catalog-services-2: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services-2 + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml-2:10011/eureka,https://apiml:10011/eureka + APIML_SERVICE_GATEWAYHOSTNAME: https://apiml-2:10010 + caching-service: + image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - uses: ./.github/actions/setup + + - name: Run Startup Check + if: always() + timeout-minutes: 4 + run: > + ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=1 + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Run Modulith CI Tests with SAF Provider in HA + run: > + ENV_CONFIG=docker-modulith-ha ./gradlew :integration-tests:runContainerSAFProviderTests --info + -Ddiscoverableclient.instances=1 -Denvironment.config=-docker-modulith-ha -Denvironment.modulith=true + -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} + - uses: ./.github/actions/dump-jacoco + if: always() + - name: Store results + uses: actions/upload-artifact@v4 + if: always() + with: + name: CITestsModulithSAFProviderHA-${{ env.JOB_ID }} + path: | + integration-tests/build/reports/** + results/** + integration-tests/build/test-results/runContainerSAFProviderTests/binary/** + integration-tests/build/test-results/runStartUpCheck/binary/** + + - uses: ./.github/actions/teardown + + CITestsModulithHA: + needs: PublishJibContainers + container: ubuntu:latest + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + matrix: + type: ["normal", "gateway-chaotic", "discoverableclient-chaotic", "websocket-chaotic"] + services: + apiml: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_AUTH_JWT_CUSTOMAUTHHEADER: customJwtHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMUSERHEADER: customUserHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader + APIML_SECURITY_X509_ENABLED: true + SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 + SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient + APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml + logbackService: ZWEAGW1 + APIML_HEALTH_PROTECTED: false + apiml-2: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_AUTH_JWT_CUSTOMAUTHHEADER: customJwtHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMUSERHEADER: customUserHeader + APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader + APIML_SECURITY_X509_ENABLED: true + SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 + SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient + APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml-2 + logbackService: ZWEAGW2 + APIML_HEALTH_PROTECTED: false + api-catalog-services: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 + api-catalog-services-2: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services-2 + APIML_HEALTH_PROTECTED: false + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 + caching-service: + image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + discoverable-client-2: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: discoverable-client-2 + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - uses: ./.github/actions/setup + + - name: Run Startup Check + if: always() + timeout-minutes: 4 + run: > + ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=2 + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Run Normal HA Tests in Modulith + if: ${{ 'normal' == matrix.type }} + run: > + ./gradlew runHATests --info -Denvironment.config=-docker-modulith-ha + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Run Gateway Service Chaotic HA Tests in Modulith + if: ${{ 'gateway-chaotic' == matrix.type }} + run: > + ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.GatewayChaoticTest + --info -Denvironment.config=-docker-modulith-ha + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Run Discoverable Client Chaotic HA Tests in Modulith + if: ${{ 'discoverableclient-chaotic' == matrix.type }} + run: > + ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.SouthboundServiceChaoticTest + --info -Denvironment.config=-docker-modulith-ha + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Run WebSocket Chaotic HA Tests in Modulith + if: ${{ 'websocket-chaotic' == matrix.type }} + run: > + ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.WebSocketChaoticTest + --info -Denvironment.config=-docker-modulith-ha + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Correct Permisions + run: | + chmod 755 -R .gradle + + - name: Store results + uses: actions/upload-artifact@v4 + if: always() + with: + name: CITestsModulithHA-${{ env.JOB_ID }}-${{ matrix.type }} + path: | + integration-tests/build/reports/** + integration-tests/build/test-results/** + + - uses: ./.github/actions/teardown + CITests: needs: PublishJibContainers runs-on: ubuntu-latest @@ -155,6 +400,7 @@ jobs: - uses: ./.github/actions/setup - name: Run CI Tests + timeout-minutes: 4 run: > ./gradlew runStartUpCheck :integration-tests:runContainerTests --info -Denvironment.config=-docker -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} @@ -171,6 +417,96 @@ jobs: - uses: ./.github/actions/teardown + CITestsSAFProviderHA: + needs: PublishJibContainers + runs-on: ubuntu-latest + container: ubuntu:latest + timeout-minutes: 15 + + services: + api-catalog-services-2: + image: ghcr.io/balhar-jakub/api-catalog-services-standalone:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services-2 + APIML_HEALTH_PROTECTED: false + api-catalog-services: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + caching-service: + image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + discovery-service: + image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} + discovery-service-2: + image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: discovery-service-2 + APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service-2:10011/eureka + gateway-service: + image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: gateway-service + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ + logbackService: ZWEAGW1 + gateway-service-2: + image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: gateway-service-2 + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ + logbackService: ZWEAGW2 + zaas-service: + image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_X509_ENABLED: true + APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true + APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates + APIML_SECURITY_AUTH_PROVIDER: saf + zaas-service-2: + image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_X509_ENABLED: true + APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true + APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates + APIML_SECURITY_AUTH_PROVIDER: saf + APIML_SERVICE_HOSTNAME: zaas-service-2 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - uses: ./.github/actions/setup + + - name: Run Startup Check + if: always() + timeout-minutes: 4 + run: > + ./gradlew runStartUpCheck --info -Denvironment.config=-ha -Ddiscoverableclient.instances=1 + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Run CI Tests With SAF Provider + run: > + ./gradlew :integration-tests:runContainerSAFProviderTests --info -Denvironment.config=-ha + -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} + - uses: ./.github/actions/dump-jacoco + if: always() + - name: Store results + uses: actions/upload-artifact@v4 + if: always() + with: + name: CITestsSAFProviderHA-${{ env.JOB_ID }} + path: | + integration-tests/build/reports/** + results/** + + - uses: ./.github/actions/teardown + Oauth2Integration: needs: PublishJibContainers runs-on: ubuntu-latest @@ -228,6 +564,7 @@ jobs: APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SERVICE_FORWARDCLIENTCERTENABLED: true APIML_SECURITY_X509_CERTIFICATESURL: https://central-gateway-service:10010/gateway/certificates + logbackService: ZWEAGW1 gateway-service-2: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: @@ -352,6 +689,7 @@ jobs: APIML_SECURITY_X509_ENABLED: true APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates + logbackService: ZWEAAZ1 gateway-service: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: @@ -360,6 +698,7 @@ jobs: APIML_SERVICE_EXTERNALURL: https://gateway-service:10010 APIML_GATEWAY_REGISTRY_ENABLED: true APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER + logbackService: ZWEAGW1 mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} @@ -378,6 +717,7 @@ jobs: APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/ + logbackService: ZWEAAZ2 gateway-service-2: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: @@ -400,6 +740,7 @@ jobs: - uses: ./.github/actions/setup - name: Run CI Tests + timeout-minutes: 4 run: > ./gradlew runStartUpCheck :integration-tests:runGatewayCentralRegistryTest --info -Denvironment.config=-docker -Dgateway.instances=2 -Ddiscoverableclient.instances=0 @@ -743,7 +1084,7 @@ jobs: - uses: ./.github/actions/teardown - CITestsHA: + DeterministicHALoadBalancing: needs: PublishJibContainers container: ubuntu:latest runs-on: ubuntu-latest @@ -754,15 +1095,6 @@ jobs: image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} volumes: - /api-defs:/api-defs - env: - APIML_HEALTH_PROTECTED: false - api-catalog-services-2: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: api-catalog-services-2 - APIML_HEALTH_PROTECTED: false caching-service: image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} discoverable-client: @@ -794,11 +1126,13 @@ jobs: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ + APIML_ROUTING_INSTANCEIDHEADER: true gateway-service-2: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_HOSTNAME: gateway-service-2 APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ + APIML_ROUTING_INSTANCEIDHEADER: true logbackService: ZWEAGW2 steps: - uses: actions/checkout@v4 @@ -807,14 +1141,30 @@ jobs: - uses: ./.github/actions/setup + - name: Setup Docker + if: ${{ false }} # Debug of containers + run: | + apt update + apt install -y apt-transport-https ca-certificates curl software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable" + apt update + apt-cache policy docker-ce + apt install -y docker-ce + - name: Run HA Tests run: > - ./gradlew runHATests --info -Denvironment.config=-ha + ./gradlew runStartUpCheck runDeterministicLbHaTests --info + -Denvironment.config=-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - + - name: Get Container Logs + if: ${{ false }} # Debug of containers + run: | + docker ps -a + docker ps -q | xargs -L 1 docker logs - name: Correct Permisions run: | chmod 755 -R .gradle @@ -823,13 +1173,13 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: CITestsHA-${{ env.JOB_ID }} + name: DeterministicLbHaTest-${{ env.JOB_ID }} path: | integration-tests/build/reports/** - uses: ./.github/actions/teardown - DeterministicHALoadBalancing: + DeterministicHALoadBalancingModulith: needs: PublishJibContainers container: ubuntu:latest runs-on: ubuntu-latest @@ -840,43 +1190,40 @@ jobs: image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} volumes: - /api-defs:/api-defs + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 caching-service: image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka discoverable-client-2: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_HOSTNAME: discoverable-client-2 + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - discovery-service: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service:10011/eureka,https://discovery-service-2:10011/eureka - discovery-service-2: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs env: - APIML_SERVICE_HOSTNAME: discovery-service-2 - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service-2:10011/eureka,https://discovery-service:10011/eureka - zaas-service: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - zaas-service-2: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + apiml: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml APIML_ROUTING_INSTANCEIDHEADER: true - gateway-service-2: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} + logbackService: ZWEAGW1 + apiml-2: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} env: - APIML_SERVICE_HOSTNAME: gateway-service-2 - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml-2 APIML_ROUTING_INSTANCEIDHEADER: true logbackService: ZWEAGW2 steps: @@ -897,11 +1244,21 @@ jobs: apt-cache policy docker-ce apt install -y docker-ce + - name: Run Startup Check + if: always() + timeout-minutes: 4 + run: > + ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=2 + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + - name: Run HA Tests run: > - ./gradlew runStartUpCheck runDeterministicLbHaTests --info - -Denvironment.config=-ha -Ddiscoverableclient.instances=2 - -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD + ./gradlew runDeterministicLbHaTests --info + -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=2 + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} @@ -918,7 +1275,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: DeterministicLbHaTest-${{ env.JOB_ID }} + name: DeterministicLbHaTestModulith-${{ env.JOB_ID }} path: | integration-tests/build/reports/** @@ -1021,21 +1378,108 @@ jobs: - uses: ./.github/actions/teardown - # CITestsChaoticHAModulith: + StickySessionHALoadBalancingModulith: + needs: PublishJibContainers + container: ubuntu:latest + runs-on: ubuntu-latest + timeout-minutes: 15 + + services: + api-catalog-services: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + caching-service: + image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_CUSTOMMETADATA_APIML_LB_TYPE: authentication + discoverable-client-2: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: discoverable-client-2 + APIML_SERVICE_CUSTOMMETADATA_APIML_LB_TYPE: authentication + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} + apiml: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml + APIML_SERVICE_CUSTOMMETADATA_APIML_LB_TYPE: authentication + logbackService: ZWEAGW1 + apiml-2: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_HOSTNAME: apiml-2 + APIML_SERVICE_CUSTOMMETADATA_APIML_LB_TYPE: authentication + logbackService: ZWEAGW2 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - uses: ./.github/actions/setup + + - name: Setup Docker + if: ${{ false }} # Debug of containers + run: | + apt update + apt install -y apt-transport-https ca-certificates curl software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable" + apt update + apt-cache policy docker-ce + apt install -y docker-ce + + - name: Run HA Tests + run: > + ./gradlew runStickySessionLbHaTests --info -Denvironment.config=-docker-modulith-ha + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + - name: Get Container Logs + if: ${{ false }} # Debug of containers + run: | + docker ps -a + docker ps -q | xargs -L 1 docker logs + - name: Correct Permisions + run: | + chmod 755 -R .gradle + # Coverage results are not stored in this job as it would not provide much additional data + - name: Store results + uses: actions/upload-artifact@v4 + if: always() + with: + name: StickySessionLbHaTestModulith-${{ env.JOB_ID }} + path: | + integration-tests/build/reports/** + + - uses: ./.github/actions/teardown - CITestsChaoticHA: + CITestsHA: needs: PublishJibContainers container: ubuntu:latest runs-on: ubuntu-latest timeout-minutes: 15 strategy: matrix: - type: ["discovery", "gateway", "discoverableclient", "websocket"] + type: ["normal", "discovery-chaotic", "gateway-chaotic", "discoverableclient-chaotic", "websocket-chaotic"] services: api-catalog-services: image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} volumes: - /api-defs:/api-defs + api-catalog-services-2: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services-2 + APIML_HEALTH_PROTECTED: false caching-service: image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} discoverable-client: @@ -1086,6 +1530,7 @@ jobs: - name: Run Startup Check if: always() + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD @@ -1093,8 +1538,17 @@ jobs: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + - name: Run Normal HA Tests + if: ${{ 'normal' == matrix.type }} + run: > + ./gradlew runHATests --info -Denvironment.config=-ha + -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + - name: Run Discovery Service Chaotic HA Tests - if: ${{ 'discovery' == matrix.type }} + if: ${{ 'discovery-chaotic' == matrix.type }} run: > ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.DiscoveryChaoticTest --info -Denvironment.config=-ha -Ddiscoverableclient.instances=1 @@ -1104,7 +1558,7 @@ jobs: ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - name: Run Gateway Service Chaotic HA Tests - if: ${{ 'gateway' == matrix.type }} + if: ${{ 'gateway-chaotic' == matrix.type }} run: > ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.GatewayChaoticTest --info -Denvironment.config=-ha @@ -1114,7 +1568,7 @@ jobs: ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - name: Run Discoverable Client Chaotic HA Tests - if: ${{ 'discoverableclient' == matrix.type }} + if: ${{ 'discoverableclient-chaotic' == matrix.type }} run: > ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.SouthboundServiceChaoticTest --info -Denvironment.config=-ha @@ -1124,7 +1578,7 @@ jobs: ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - name: Run WebSocket Chaotic HA Tests - if: ${{ 'websocket' == matrix.type }} + if: ${{ 'websocket-chaotic' == matrix.type }} run: > ./gradlew :integration-tests:runChaoticHATests --tests org.zowe.apiml.integration.ha.WebSocketChaoticTest --info -Denvironment.config=-ha @@ -1141,7 +1595,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: CITestsChaoticHA-${{ env.JOB_ID }}-${{ matrix.type }} + name: CITestsHA-${{ env.JOB_ID }}-${{ matrix.type }} path: | integration-tests/build/reports/** @@ -1307,6 +1761,7 @@ jobs: api-catalog-ui/frontend/node_modules key: my-cache-${{ runner.os }}-${{ hashFiles('api-catalog-ui/frontend/*.json') }} - name: Run startup check + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info --scan -Denvironment.config=-ha -Ddiscoverableclient.instances=1 - name: Show status when APIML is not ready yet @@ -1345,6 +1800,121 @@ jobs: path: api-catalog-ui/frontend/cypress/screenshots - uses: ./.github/actions/teardown + E2EUITestsModulith: + # This stage needs to use modulith with gateway-service hostname due to the current OIDC provider setup + # Where only gateway-service is an allowed hostname for redirects + needs: PublishJibContainers + runs-on: ubuntu-latest + container: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1 + timeout-minutes: 30 + + services: + api-catalog-services: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services + APIML_SERVICE_DISCOVERYSERVICEURLS: https://gateway-service:10011/eureka + APIML_SERVICE_GATEWAYHOSTNAME: https://gateway-service:10010 + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://gateway-service:10011/eureka + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://gateway-service:10011/eureka + + gateway-service: # To avoid updating Oauth provider admin settings + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_DISCOVERY_ALLPEERSURLS: https://gateway-service:10011/eureka + APIML_SERVICE_HOSTNAME: gateway-service + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OKTA_CLIENTID: ${{ secrets.OKTA_CLIENT_ID }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OKTA_CLIENTSECRET: ${{ secrets.OKTA_CLIENT_PASSWORD }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_ISSUER: ${{ secrets.OKTA_ISSUER }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_AUTHORIZATIONURI: ${{ secrets.OKTA_AUTH_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_TOKENURI: ${{ secrets.OKTA_TOKEN_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_USERINFOURI: ${{ secrets.OKTA_USER_INFO_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_USERNAMEATTRIBUTE: sub + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_JWKSETURI: ${{ secrets.OKTA_JWKSET_URI }} + APIML_SECURITY_OIDC_COOKIE_SAMESITE: None + APIML_HEALTH_PROTECTED: false + logbackService: ZWEAGW1 + gateway-service-2: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_DISCOVERY_ALLPEERSURLS: https://gateway-service-2:10011/eureka + APIML_SERVICE_HOSTNAME: gateway-service-2 + APIML_SERVICE_APIMLID: apiml2 + APIML_HEALTH_PROTECTED: false + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://gateway-service:10011/eureka + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: / + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: / + logbackService: ZWEAGW2 + APIML_SECURITY_X509_ENABLED: true + APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true + APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service-2:10010/gateway/certificates + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - uses: ./.github/actions/setup + - name: Install npm dependencies API Catalog + uses: bahmutov/npm-install@v1 + with: + install-command: npm ci --legacy-peer-deps + working-directory: api-catalog-ui/frontend + - name: Cache NPM and Cypress 📦 + uses: actions/cache@v4 + with: + path: | + ~/.cache/Cypress + api-catalog-ui/frontend/node_modules + key: my-cache-${{ runner.os }}-${{ hashFiles('api-catalog-ui/frontend/*.json') }} + - name: Run startup check + timeout-minutes: 4 + run: > + ./gradlew runStartUpCheck --info --scan -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=1 -Denvironment.modulith=true -Denvironment.gwCount=1 + -Dgateway.host=gateway-service,gateway-service-2 -Ddiscovery.host=gateway-service -Ddiscovery.additionalHost=gateway-service-2 + - name: Show status when APIML is not ready yet + if: failure() + shell: bash + run: | + apt update + apt install -y apt-transport-https ca-certificates curl software-properties-common + curl -k -s https://apiml:10010/application/health + curl -k -s https://apiml-2:10010/application/health + - name: Cypress run API Catalog + run: | + cd api-catalog-ui/frontend + export CYPRESS_OKTA_USERNAME=${{ secrets.OKTA_WINNIE_USER }} + export CYPRESS_OKTA_PASSWORD=${{ secrets.OKTA_WINNIE_PASS }} + npm run cy:e2e:ci:modulith + - name: Dump CGW jacoco data + run: > + java -jar ./scripts/jacococli.jar dump --address gateway-service --port 6310 --destfile ./results/gateway-service.exec + - name: Correct Permisions + run: | + chmod 755 -R .gradle + - name: Store results + uses: actions/upload-artifact@v4 + if: always() + with: + name: E2EUITestsModulith-${{ env.JOB_ID }} + path: | + results/** + + - name: Upload screenshots API Catalog + uses: actions/upload-artifact@v4 + if: always() + with: + name: cypress-snapshots-modulith + path: api-catalog-ui/frontend/cypress/screenshots + - uses: ./.github/actions/teardown + CITestsServicePrefixReplacer: needs: PublishJibContainers container: ubuntu:latest @@ -1395,8 +1965,8 @@ jobs: PublishResults: needs: [ - CITests,CITestsWithInfinispan,CITestsZaas,GatewayProxy,GatewayServiceRouting,CITestsChaoticHA, - CITestsModulith + CITests,CITestsWithInfinispan,CITestsZaas,GatewayProxy,GatewayServiceRouting,CITestsHA, + CITestsModulith,CITestsModulithHA ] runs-on: ubuntu-latest timeout-minutes: 20 @@ -1431,7 +2001,7 @@ jobs: path: ContainerCITestsZaas - uses: actions/download-artifact@v4 with: - name: CITestsChaoticHA-${{ env.JOB_ID }}-websocket + name: CITestsHA-${{ env.JOB_ID }}-websocket-chaotic path: citestschaoticha - uses: actions/download-artifact@v4 with: diff --git a/README.md b/README.md index d81190c2cc..5727712ef7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,26 @@ The following tools are required to build and develop the API Mediation Layer: npm run api-layer ``` +### Using docker + +These steps require docker or equivalent + +1. Build local Jib images: + + Example: + + ```shell + ./gradlew :apiml:jibDockerBuild :api-catalog-services:jibDockerBuild :caching-service:jibDockerBuild -Pzowe.docker.debug=true + ``` + + **Note:** zowe.docker.debug enables Java remote debugging port + +2. Use docker-compose: + + ```shell + docker-compose up + ``` + ## Security By default, the API Mediation Layer for local development uses mock zOSMF as the authentication provider. For development purposes, log in using the default setting `USER` for the username, and `validPassword` as the password diff --git a/api-catalog-ui/frontend/cypress.modulith.config.js b/api-catalog-ui/frontend/cypress.modulith.config.js new file mode 100644 index 0000000000..d4d5941b4a --- /dev/null +++ b/api-catalog-ui/frontend/cypress.modulith.config.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + env: { + catalogHomePage: 'https://localhost:10010/apicatalog/ui/v1', + loginUrl: 'https://localhost:10010/apicatalog/api/v1/auth/login', + gatewayOktaRedirect: + 'https://localhost:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Flocalhost%3A10010%2Fapplication', + username: 'USER', + password: 'validPassword', + }, + viewportWidth: 1400, + viewportHeight: 980, + chromeWebSecurity: false, + reporter: 'junit', + defaultCommandTimeout: 30000, + reporterOptions: { + mochaFile: 'test-results/e2e/output-[hash].xml', + }, + video: false, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + // eslint-disable-next-line global-require + return require('./cypress/plugins/index.js')(on, config); + }, + excludeSpecPattern: ['cypress/e2e/detail-page/multiple-gateway-services.cy.js'], + }, +}); diff --git a/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js b/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js index 547ba3a00d..e37e3ce4c5 100644 --- a/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js @@ -7,9 +7,11 @@ * * Copyright Contributors to the Zowe Project. */ -/* eslint-disable spaced-comment */ +/* eslint-disable no-undef */ /// +const isModulith = Cypress.env('modulith'); + describe('>>> Dashboard test', () => { it('dashboard test', () => { cy.login(Cypress.env('username'), Cypress.env('password')); @@ -46,7 +48,12 @@ describe('>>> Dashboard test', () => { cy.get('#search > div > div > input').as('search').type('API Gateway'); - cy.get('.grid-tile').should('have.length', 2); + let expectedGatewaysCount = 2; + if (isModulith) { + expectedGatewaysCount = 1; + } + + cy.get('.grid-tile').should('have.length', expectedGatewaysCount); // FIXME modulith does not support multitenancy yet cy.get('.clear-text-search').click(); @@ -63,7 +70,7 @@ describe('>>> Dashboard test', () => { it('should keep session persistent by navigating to dashboard if valid token is provided', () => { const requestBody = { username: Cypress.env('username'), - password: Cypress.env('password') + password: Cypress.env('password'), }; cy.request({ @@ -74,8 +81,11 @@ describe('>>> Dashboard test', () => { expect(resp.status).to.eq(204); expect(resp.headers).to.have.property('set-cookie'); - const rawCookie = resp.headers['set-cookie'].find(cookie => cookie.startsWith('apimlAuthenticationToken=')); - expect(rawCookie).to.exist; + const rawCookie = resp.headers['set-cookie'].find((cookie) => + cookie.startsWith('apimlAuthenticationToken=') + ); + // eslint-disable-next-line no-unused-expressions + expect(rawCookie).to.not.be.empty; const cookieValue = rawCookie.split(';')[0].split('=')[1]; @@ -89,5 +99,4 @@ describe('>>> Dashboard test', () => { cy.contains('The service is running').should('exist'); }); }); - }); diff --git a/api-catalog-ui/frontend/cypress/e2e/dashboard/wizard-dialog.cy.js b/api-catalog-ui/frontend/cypress/e2e/dashboard/wizard-dialog.cy.js index 4287f3822f..82dcab32af 100644 --- a/api-catalog-ui/frontend/cypress/e2e/dashboard/wizard-dialog.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/dashboard/wizard-dialog.cy.js @@ -7,7 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ -/* eslint-disable spaced-comment */ +/* eslint-disable no-undef */ /// describe('>>> Wizard Dialog test', () => { diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js index d8e0a64feb..038a9c640a 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js @@ -7,10 +7,12 @@ * * Copyright Contributors to the Zowe Project. */ -/* eslint-disable spaced-comment */ +/* eslint-disable no-undef */ /// +const isModulith = Cypress.env('modulith'); + describe('>>> Detail page test', () => { it('Detail page test', () => { cy.login(Cypress.env('username'), Cypress.env('password')); @@ -116,6 +118,11 @@ describe('>>> Detail page test', () => { cy.get('#search > div > div > input').as('search').type('API Gateway'); - cy.get('.grid-tile').should('have.length', 2).should('contain', 'API Gateway'); + let expectedGatewaysCount = 2; + if (isModulith) { + expectedGatewaysCount = 1; + } + + cy.get('.grid-tile').should('have.length', expectedGatewaysCount).should('contain', 'API Gateway'); // FIXME in modulith multi tenancy is not working }); }); diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js index cb9a231761..4c946e2809 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js @@ -7,8 +7,10 @@ * * Copyright Contributors to the Zowe Project. */ -/// +/* eslint-disable no-undef */ + +/// describe('>>> Multi-tenancy deployment test', () => { it('Detail page test', () => { diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index c84c733e1d..4df1ce952e 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -7,8 +7,13 @@ * * Copyright Contributors to the Zowe Project. */ + +/* eslint-disable no-undef */ + /// +const isModulith = Cypress.env('modulith'); + // api-diff-form is now a floating window. const PATH_TO_VERSION_SELECTORS = '.api-diff-form > div:nth-child(2) > div > div'; const PATH_TO_VERSION_SELECTORS2 = '.api-diff-form > div:nth-child(4) > div > div'; @@ -34,9 +39,16 @@ describe('>>> Service version compare Test', () => { cy.get('div.MuiTabs-root.custom-tabs.MuiTabs-vertical > div.MuiTabs-scroller.MuiTabs-scrollable > div').should( 'exist' ); + + // FIXME modulith mode does not support multi tenancy yet + let expectedServicesCount = 16; + if (isModulith) { + expectedServicesCount = 15; + } + cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div .find('a.MuiTab-root') // Find all the anchor elements within the div - .should('have.length', 16); // Check if there are 16 anchor elements within the div + .should('have.length', expectedServicesCount); // Check if there are 16 anchor elements within the div cy.contains('Compare API Versions').should('exist'); }); @@ -60,7 +72,6 @@ describe('>>> Service version compare Test', () => { cy.get('.api-diff-form > button').should('contain.text', 'Show'); }); - it('Should display version in selector', () => { cy.get('#compare-button > span.MuiButton-label > p').should('contain.text', 'Compare API Versions').click(); diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js index 70f63954f2..df329447cc 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js @@ -7,6 +7,9 @@ * * Copyright Contributors to the Zowe Project. */ + +/* eslint-disable no-undef */ + describe('>>> Service version change Test', () => { beforeEach(() => { cy.login(Cypress.env('username'), Cypress.env('password')); diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js index 35a64652e0..a13c786125 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js @@ -1,41 +1,39 @@ /* + * 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 * - * * 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. + * SPDX-License-Identifier: EPL-2.0 * + * Copyright Contributors to the Zowe Project. */ -describe("Swagger rendering", () => { +/* eslint-disable no-undef */ - beforeEach("Login to API Catalog", () => { +describe('Swagger rendering', () => { + beforeEach('Login to API Catalog', () => { cy.login(Cypress.env('username'), Cypress.env('password')); cy.contains('Version: '); }); [ { - "serviceName": "API Gateway", - "serviceId": "gateway", - "serviceHomepage": `${Cypress.env('catalogHomePage')}`.split("/apicatalog")[0] + serviceName: 'API Gateway', + serviceId: 'gateway', + serviceHomepage: `${Cypress.env('catalogHomePage')}`.split('/apicatalog')[0], }, { - "serviceName": "API Catalog", - "serviceId": "apicatalog", - "serviceHomepage": `${Cypress.env('catalogHomePage')}`.split("/apicatalog")[0] + serviceName: 'API Catalog', + serviceId: 'apicatalog', + serviceHomepage: `${Cypress.env('catalogHomePage')}`.split('/apicatalog')[0], }, { - "serviceName": "Mock zOSMF", - "serviceId": "mockzosmf", - "serviceHomepage": ":10013/" + serviceName: 'Mock zOSMF', + serviceId: 'mockzosmf', + serviceHomepage: ':10013/', }, ].forEach((service) => { - it("Rendering Swagger for " + service.serviceName, () => { - + it(`Rendering Swagger for ${service.serviceName}`, () => { cy.get('#grid-container').contains(service.serviceName).click(); cy.get('div.tabs-swagger').should('exist'); @@ -43,46 +41,35 @@ describe("Swagger rendering", () => { cy.get('div.tabs-swagger > div.serviceTab > div.header > h4').should('contain', service.serviceName); cy.get('div.tabs-swagger > div.serviceTab > div.header > a:nth-child(2)') - .as('serviceHomepage').should("contain", "Service Homepage"); + .as('serviceHomepage') + .should('contain', 'Service Homepage'); - cy.get('@serviceHomepage').should('have.attr', 'title', 'Open Service Homepage') + cy.get('@serviceHomepage').should('have.attr', 'title', 'Open Service Homepage'); - cy.get('div.tabs-swagger') - .find('.apiInfo-item') - .get('h6:nth-Child(1)') - .as('basePath'); + cy.get('div.tabs-swagger').find('.apiInfo-item').get('h6:nth-Child(1)').as('basePath'); - cy.get('@basePath') - .get('label') - .should('contain', "API Base Path:"); + cy.get('@basePath').get('label').should('contain', 'API Base Path:'); - let regexContent = `^\/${service.serviceId}\/api`; + let regexContent = `^/${service.serviceId}/api`; if (service.serviceId === 'gateway') { regexContent = '/'; } const regex = new RegExp(regexContent); cy.get('@basePath') - .get('#apiBasePath').invoke("text").should(text => { - expect(text).to.match(regex); - }); + .get('#apiBasePath') + .invoke('text') + .should((text) => { + expect(text).to.match(regex); + }); - cy.get('div.tabs-swagger') - .find('.apiInfo-item') - .get('h6:nth-Child(2)') - .as('sId'); + cy.get('div.tabs-swagger').find('.apiInfo-item').get('h6:nth-Child(2)').as('sId'); - cy.get('@sId') - .get('label') - .should('contain', "Service ID:"); + cy.get('@sId').get('label').should('contain', 'Service ID:'); - cy.get('@sId') - .get('#serviceId') - .should('contain', service.serviceId); + cy.get('@sId').get('#serviceId').should('contain', service.serviceId); - cy.get('#swagger-label') - .should('exist') - .should('contain', 'Swagger'); + cy.get('#swagger-label').should('exist').should('contain', 'Swagger'); cy.get('#swaggerContainer').as('swaggerContainer'); @@ -92,36 +79,23 @@ describe("Swagger rendering", () => { .get('div.swagger-ui > div:nth-child(2) > div.information-container') .should('exist'); - cy.get('@swaggerContainer') - .get('div.information-container > section div.info .main') - .as('mainInfo'); + cy.get('@swaggerContainer').get('div.information-container > section div.info .main').as('mainInfo'); cy.get('@mainInfo').should('exist'); - cy.get('@mainInfo') - .get('h2') - .should('have.class', 'title'); + cy.get('@mainInfo').get('h2').should('have.class', 'title'); - cy.get('@mainInfo') - .get('h2') - .find('pre.version') - .should('exist'); + cy.get('@mainInfo').get('h2').find('pre.version').should('exist'); - cy.get('@swaggerContainer') - .get('div.swagger-ui > div:nth-child(2) > div.wrapper') - .should('exist'); + cy.get('@swaggerContainer').get('div.swagger-ui > div:nth-child(2) > div.wrapper').should('exist'); cy.get('@swaggerContainer') .get('div.wrapper > section > div > div.apiInfo-item > p > label') .as('instanceUrlLabel'); - cy.get('@instanceUrlLabel') - .should('exist') - .should('contain', 'Instance URL:'); + cy.get('@instanceUrlLabel').should('exist').should('contain', 'Instance URL:'); - cy.get('@swaggerContainer') - .get('div.swagger-ui > div:nth-child(2) > div.wrapper') - .should('exist'); + cy.get('@swaggerContainer').get('div.swagger-ui > div:nth-child(2) > div.wrapper').should('exist'); cy.get('@swaggerContainer') .get('div.wrapper > section > div > div > div.opblock-tag-section') @@ -129,26 +103,18 @@ describe("Swagger rendering", () => { cy.get('@operationBlock').should('exist'); - cy.get('@operationBlock') - .find('.operation-tag-content') - .should('exist'); + cy.get('@operationBlock').find('.operation-tag-content').should('exist'); - cy.get('@operationBlock') - .get('.operation-tag-content') - .find('.opblock') - .should('exist'); + cy.get('@operationBlock').get('.operation-tag-content').find('.opblock').should('exist'); cy.get('@operationBlock') .get('.operation-tag-content > .opblock > .opblock-summary > button:first-child') .should('exist'); - cy.get('@operationBlock') - .get('button:first-child') - .should('have.class', 'opblock-summary-control'); + cy.get('@operationBlock').get('button:first-child').should('have.class', 'opblock-summary-control'); cy.get('@serviceHomepage').click(); cy.url().should('contain', service.serviceHomepage); }); - }) + }); }); - diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js index 7c222dfd87..a36279fc00 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js @@ -8,6 +8,8 @@ * Copyright Contributors to the Zowe Project. */ +/* eslint-disable no-undef */ + describe('>>> Swagger Try Out and Code Snippets Test', () => { beforeEach(() => { cy.login(Cypress.env('username'), Cypress.env('password')); diff --git a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js index a2d9183590..58f6f2252e 100644 --- a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js @@ -7,7 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ -/* eslint-disable spaced-comment */ +/* eslint-disable no-undef */ /// const expectedKeyWords = ['name', 'getAllBooks', 'Effective Java', "Hitchhiker's Guide to the Galaxy", 'Down Under']; @@ -21,8 +21,6 @@ const PATH_TO_DEFAULT_QUERY = '#graphiql-session > div:nth-child(1) > div > div:nth-child(1) > section > div.graphiql-editor > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code > div > pre > span > span'; const PATH_TO_RUN_QUERY_BUTTON = '#graphiql-session > div:nth-child(1) > div > div:nth-child(1) > section > div.graphiql-toolbar > button'; -const PATH_TO_ADD_TAB_BUTTON = - '#graphiql-container > div > div.graphiql-main > div.graphiql-sessions > div.graphiql-session-header > div > button'; const PATH_TO_REMOVE_SPECIFIC_TAB_BUTTON = '#graphiql-container > div > div.graphiql-main > div.graphiql-sessions > div.graphiql-session-header > ul > li.graphiql-tab.graphiql-tab-active > button.graphiql-un-styled.graphiql-tab-close'; const PATH_TO_VARIABLES_INPUT_TEXTAREA = diff --git a/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js b/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js index b1b89a9e8e..e5009a1f21 100644 --- a/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js @@ -7,11 +7,10 @@ * * Copyright Contributors to the Zowe Project. */ -/* eslint-disable spaced-comment */ +/* eslint-disable no-undef */ /// describe('>>> Login bad test', () => { - it('should not display header', () => { cy.visit(`${Cypress.env('catalogHomePage')}/`); cy.get('.header').should('not.exist'); diff --git a/api-catalog-ui/frontend/cypress/e2e/login/login-oauth2.cy.js b/api-catalog-ui/frontend/cypress/e2e/login/login-oauth2.cy.js index 05f1b8f2bb..47c38d25e9 100644 --- a/api-catalog-ui/frontend/cypress/e2e/login/login-oauth2.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/login/login-oauth2.cy.js @@ -7,23 +7,20 @@ * * Copyright Contributors to the Zowe Project. */ -/* eslint-disable spaced-comment */ +/* eslint-disable no-undef */ describe('>>> Login through Okta OK', () => { - - it('should log in user and check session cookie', () => { - cy.visit(`${Cypress.env('gatewayOktaRedirect')}`); const username = Cypress.env('OKTA_USERNAME'); - if(!username) { - cy.log("System env CYPRESS_OKTA_USERNAME is not set"); + if (!username) { + cy.log('System env CYPRESS_OKTA_USERNAME is not set'); } const password = Cypress.env('OKTA_PASSWORD'); - if(!password) { - cy.log("System env CYPRESS_OKTA_PASSWORD is not set"); + if (!password) { + cy.log('System env CYPRESS_OKTA_PASSWORD is not set'); } cy.get('form span.o-form-input-name-username input').type(username); @@ -32,12 +29,8 @@ describe('>>> Login through Okta OK', () => { cy.get('form input.button-primary').should('not.be.disabled'); cy.get('form input.button-primary').click(); - cy.url().should('contain', '/application'); cy.getCookie('apimlAuthenticationToken').should('exist'); - }); - - }); diff --git a/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js b/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js index a13ffc667f..e0cd67e280 100644 --- a/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js @@ -7,11 +7,10 @@ * * Copyright Contributors to the Zowe Project. */ -/* eslint-disable spaced-comment */ +/* eslint-disable no-undef */ /// describe('>>> Login ok page test', () => { - it('should not display header', () => { cy.visit(`${Cypress.env('catalogHomePage')}/#/`); cy.get('.header').should('not.exist'); diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 1aee84f104..0d50216971 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -62,7 +62,8 @@ "postbuild": "rimraf --glob build/**/*.map", "test": "react-app-rewired test --runInBand --silent --watchAll=false --env=jsdom components/* utils/* reducers/* epics/* actions/* selectors/* ErrorBoundary/* helpers/* --reporters=default --reporters=jest-html-reporter --coverage", "cy:open": "cypress open", - "cy:e2e:ci": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --env catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/apicatalog/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", + "cy:e2e:ci": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --env moudlith=false,catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/apicatalog/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", + "cy:e2e:ci:modulith": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --config-file cypress.modulith.config.js --env modulith=true,catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/apicatalog/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", "cy:e2e:localhost": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headless", "cy:e2e:localhost-debug": "DEBUG=cypress:net* cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headless", "cy:e2e:localhost-headful": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headed", diff --git a/api-catalog-ui/frontend/src/actions/refresh-static-apis-actions.jsx b/api-catalog-ui/frontend/src/actions/refresh-static-apis-actions.jsx index dca1e7cb20..102111ea00 100644 --- a/api-catalog-ui/frontend/src/actions/refresh-static-apis-actions.jsx +++ b/api-catalog-ui/frontend/src/actions/refresh-static-apis-actions.jsx @@ -61,8 +61,11 @@ export function refreshedStaticApi() { return res.json(); }) .then((data) => { - fetchHandler(data); - dispatch(refreshStaticApisSuccess()); + fetchHandler(data) + .catch((errors) => { + console.log(`refresh static apis returned warnings: ${JSON.stringify(errors)}`) + }) + .then(_ => dispatch(refreshStaticApisSuccess())); }) .catch((error) => { console.error("Error refreshing static APIs:", error); @@ -89,7 +92,8 @@ export function clearError() { function fetchHandler(res) { const errors = []; if (res && !res.errors) { - return Promise.reject(res.messages[0]); + errors.push(res.messages[0]) + return Promise.reject(errors); } if (res && res.errors && res.errors.length !== 0) { res.errors.forEach((item) => { @@ -97,5 +101,5 @@ function fetchHandler(res) { }); return Promise.reject(errors); } - return res; + return Promise.resolve(); } diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java index 50e0689059..8d2b2ad8cf 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java @@ -358,8 +358,8 @@ private static String taskId(String requestType, InstanceInfo info) { return taskId(requestType, info.getAppName(), info.getId()); } - private static int getLeaseRenewalOf(InstanceInfo info) { - return (info.getLeaseInfo() == null ? Lease.DEFAULT_DURATION_IN_SECS : info.getLeaseInfo().getRenewalIntervalInSecs()) * 1000; + private static long getLeaseRenewalOf(InstanceInfo info) { + return (info.getLeaseInfo() == null ? (long) Lease.DEFAULT_DURATION_IN_SECS : info.getLeaseInfo().getRenewalIntervalInSecs()) * 1000L; } @Slf4j diff --git a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java index 2d4fcb76c8..035338719c 100644 --- a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java +++ b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/SslContext.java @@ -37,6 +37,10 @@ public class SslContext { public static RestAssuredConfig apimlRootCert; // API ML root certificate, cannot be used for client authentication public static RestAssuredConfig selfSignedUntrusted; public static RestAssuredConfig tlsWithoutCert; + + public static SSLContext sslClientCertValid; + public static SSLContext sslClientCertApiml; + private static AtomicBoolean isInitialized = new AtomicBoolean(false); private static AtomicReference configurer = new AtomicReference<>(); @@ -65,24 +69,24 @@ public synchronized static void prepareSslAuthentication(SslContextConfigurer pr log.info("SSLContext is constructing. This should happen only once."); TrustStrategy trustStrategy = (X509Certificate[] chain, String authType) -> true; - SSLContext sslContext = SSLContextBuilder + sslClientCertValid = SSLContextBuilder .create() .loadKeyMaterial(ResourceUtils.getFile(providedConfigurer.getKeystoreLocalhostJks()), providedConfigurer.getKeystorePassword(), providedConfigurer.getKeystorePassword(), (Map aliases, Socket socket) -> "apimtst") .loadTrustMaterial(null, trustStrategy) .build(); - clientCertValid = RestAssuredConfig.newConfig().sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(sslContext, hostnameVerifier))); + clientCertValid = RestAssuredConfig.newConfig().sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(sslClientCertValid, hostnameVerifier))); log.debug("Loaded {}[apimtst]", providedConfigurer.getKeystoreLocalhostJks()); - SSLContext sslContext2 = SSLContextBuilder + sslClientCertApiml = SSLContextBuilder .create() .loadKeyMaterial(ResourceUtils.getFile(providedConfigurer.getKeystore()), providedConfigurer.getKeystorePassword(), providedConfigurer.getKeystorePassword()) .loadTrustMaterial(null, trustStrategy) .build(); - clientCertApiml = RestAssuredConfig.newConfig().sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(sslContext2, hostnameVerifier))); + clientCertApiml = RestAssuredConfig.newConfig().sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(sslClientCertApiml, hostnameVerifier))); log.debug("Loaded {}", providedConfigurer.getKeystore()); diff --git a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java index c2c39c7d07..82942f7e02 100644 --- a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java +++ b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java @@ -16,6 +16,7 @@ import org.springframework.cloud.netflix.eureka.server.EurekaController; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; +import org.zowe.apiml.gateway.config.GatewayHealthIndicator; @SpringBootApplication( exclude = { ReactiveOAuth2ClientAutoConfiguration.class }, @@ -36,6 +37,10 @@ type = FilterType.REGEX, pattern = ".*Application" ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = GatewayHealthIndicator.class + ), @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, classes = EurekaController.class diff --git a/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java b/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java index 3167e681d0..c667b0f00a 100644 --- a/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java +++ b/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java @@ -277,7 +277,7 @@ public Mono> asgStatusUpdate( return just(convertResponse(asgResource.statusUpdate(asgName, newStatus, isReplication))); } - @PostMapping + @PostMapping({ "/peerreplication/batch/", "/peerreplication/batch" }) public Mono> batchReplication( @RequestBody String replicationListString ) throws IOException { diff --git a/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java new file mode 100644 index 0000000000..463477a9e4 --- /dev/null +++ b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java @@ -0,0 +1,96 @@ +/* + * 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; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.Status; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.zaas.ZaasServiceAvailableEvent; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.springframework.boot.actuate.health.Status.DOWN; +import static org.springframework.boot.actuate.health.Status.UP; + +/** + * This class contributes the apiml component health indication to the main /application/health + * controlled by class {@link ApimlHealthCheckHandler} in the common package. + * + * Note: Name is kept as GatewayHealthIndicator for backwards compatibility + */ +@Component +@RequiredArgsConstructor +public class GatewayHealthIndicator extends AbstractHealthIndicator { + + private static final ApimlLogger apimlLog = ApimlLogger.of(GatewayHealthIndicator.class, YamlMessageServiceInstance.getInstance()); + private final DiscoveryClient discoveryClient; + + @Value("${apiml.catalog.serviceId:}") + private String apiCatalogServiceId; + + private AtomicBoolean discoveryAvailable = new AtomicBoolean(false); + private AtomicBoolean zaasAvailable = new AtomicBoolean(false); + + private AtomicBoolean startedInformationPublished = new AtomicBoolean(false); + + @Override + protected void doHealthCheck(Builder builder) throws Exception { + var anyCatalogIsAvailable = apiCatalogServiceId != null && !apiCatalogServiceId.isEmpty(); + var apiCatalogUp = !this.discoveryClient.getInstances(apiCatalogServiceId).isEmpty(); + + // Keeping for backwards compatibility, in modulith the amount of gateways is the amount of authentication services available + int gatewayCount = this.discoveryClient.getInstances(CoreService.GATEWAY.getServiceId()).size(); + int zaasCount = gatewayCount; + + builder.status(toStatus(discoveryAvailable.get() && zaasAvailable.get())) + .withDetail(CoreService.DISCOVERY.getServiceId(), toStatus(discoveryAvailable.get()).getCode()) + .withDetail(CoreService.ZAAS.getServiceId(), toStatus(zaasAvailable.get()).getCode()) + .withDetail("gatewayCount", gatewayCount) + .withDetail("zaasCount", zaasCount); + + if (anyCatalogIsAvailable) { + builder.withDetail(CoreService.API_CATALOG.getServiceId(), toStatus(apiCatalogUp).getCode()); + } + + if (!startedInformationPublished.get() && discoveryAvailable.get() && apiCatalogUp && zaasAvailable.get()) { + apimlLog.log("org.zowe.apiml.common.mediationLayerStarted"); + startedInformationPublished.set(true); + } + } + + @EventListener + public void onApplicationEvent(ZaasServiceAvailableEvent event) { + zaasAvailable.set(true); + } + + @EventListener + public void onApplicationEvent(EurekaRegistryAvailableEvent event) { + discoveryAvailable.set(true); + } + + boolean isStartedInformationPublished() { + return startedInformationPublished.get(); + } + + private Status toStatus(boolean up) { + return up ? UP : DOWN; + } + +} diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index 5561b091fd..90e81d5a90 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -14,11 +14,18 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.LeaseInfo; import com.netflix.discovery.CacheRefreshedEvent; +import com.netflix.discovery.EurekaClientConfig; import com.netflix.discovery.shared.Application; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; import jakarta.annotation.PostConstruct; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.Context; @@ -56,7 +63,14 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Timer; +import java.util.TimerTask; @EnableScheduling @Configuration @@ -68,6 +82,9 @@ public class ModulithConfig { private final ApplicationContext applicationContext; private final Map instances = new HashMap<>(); private final GatewayEurekaInstanceConfigBean eurekaInstanceGw; + private final EurekaClientConfig eurekaConfig; + + private final Timer timer = new Timer("PeerReplicated-StaticServices"); @Value("${server.ssl.enabled:true}") private boolean https; @@ -90,12 +107,12 @@ ApplicationInfo applicationInfo() { private InstanceInfo getInstanceInfo(String serviceId) { var leaseInfo = LeaseInfo.Builder.newBuilder() - .setDurationInSecs(Integer.MAX_VALUE) - .setRegistrationTimestamp(System.currentTimeMillis()) - .setRenewalTimestamp(System.currentTimeMillis()) - .setRenewalIntervalInSecs(Integer.MAX_VALUE) - .setServiceUpTimestamp(System.currentTimeMillis()) - .build(); + .setDurationInSecs(Integer.MAX_VALUE) + .setRegistrationTimestamp(System.currentTimeMillis()) + .setRenewalTimestamp(System.currentTimeMillis()) + .setRenewalIntervalInSecs(Integer.MAX_VALUE) + .setServiceUpTimestamp(System.currentTimeMillis()) + .build(); var scheme = https ? "https" : "http"; @@ -109,31 +126,31 @@ private InstanceInfo getInstanceInfo(String serviceId) { } return InstanceInfo.Builder.newBuilder() - .setInstanceId(String.format("%s:%s:%d", hostname, serviceId, port)) - .setAppName(serviceId) - .setHostName(hostname) - .setHomePageUrl(null, String.format("%s://%s:%d", scheme, hostname, port)) - .setStatus(InstanceInfo.InstanceStatus.UP) - .setIPAddr(ipAddress) - .setPort(port) - .setSecurePort(port) - .enablePort(InstanceInfo.PortType.SECURE, https) - .enablePort(InstanceInfo.PortType.UNSECURE, !https) - .setVIPAddress(serviceId) - .setDataCenterInfo(() -> DataCenterInfo.Name.MyOwn) - .setLeaseInfo(leaseInfo) - .setLastUpdatedTimestamp(System.currentTimeMillis()) - .setMetadata(metadata) - .setVIPAddress(serviceId) - .build(); + .setInstanceId(String.format("%s:%s:%d", hostname, serviceId, port)) + .setAppName(serviceId) + .setHostName(hostname) + .setHomePageUrl(null, String.format("%s://%s:%d", scheme, hostname, port)) + .setStatus(InstanceInfo.InstanceStatus.UP) + .setIPAddr(ipAddress) + .setPort(port) + .setSecurePort(port) + .enablePort(InstanceInfo.PortType.SECURE, https) + .enablePort(InstanceInfo.PortType.UNSECURE, !https) + .setVIPAddress(serviceId) + .setDataCenterInfo(() -> DataCenterInfo.Name.MyOwn) + .setLeaseInfo(leaseInfo) + .setLastUpdatedTimestamp(System.currentTimeMillis()) + .setMetadata(metadata) + .setVIPAddress(serviceId) + .build(); } private ApimlInstanceRegistry getRegistry() { return Optional.ofNullable(EurekaServerContextHolder.getInstance()) - .map(EurekaServerContextHolder::getServerContext) - .map(EurekaServerContext::getRegistry) - .map(ApimlInstanceRegistry.class::cast) - .orElse(null); + .map(EurekaServerContextHolder::getServerContext) + .map(EurekaServerContext::getRegistry) + .map(ApimlInstanceRegistry.class::cast) + .orElse(null); } @PostConstruct @@ -146,14 +163,26 @@ void createLocalInstances() { @EventListener public void onApplicationEvent(EurekaRegistryAvailableEvent event) { ApimlInstanceRegistry registry = getRegistry(); - instances.forEach((key, value) -> registry.registerStatically(instances.get(key), CoreService.GATEWAY.getServiceId().equals(key))); + instances.forEach((key, value) -> registry.registerStatically(instances.get(key), false, CoreService.GATEWAY.getServiceId().equalsIgnoreCase(key))); var jwtSec = applicationContext.getBean(JwtSecurity.class); if (!jwtSec.getZosmfListener().isZosmfReady()) { jwtSec.getZosmfListener().getZosmfRegisteredListener().onEvent(new CacheRefreshedEvent()); } - } + log.info("Initialize timer for static services peer-replicated heartbeats"); + + // This timer calls Eureka registry's peerReplicate method to accumulate all heartbeats of statically-onboarded services once + timer.scheduleAtFixedRate(new TimerTask() { + + @Override + public void run() { + registry.peerAwareHeartbeat(instances.get(CoreService.GATEWAY.getServiceId())); + } + + }, eurekaConfig.getInstanceInfoReplicationIntervalSeconds() * 1000L, eurekaConfig.getInstanceInfoReplicationIntervalSeconds() * 1000L); + + } @Bean ReactiveDiscoveryClient registryReactiveDiscoveryClient(DiscoveryClient registryDiscoveryClient) { @@ -195,12 +224,12 @@ public List getInstances(String serviceId) { return Collections.emptyList(); } return Optional.ofNullable(registry.getApplication(StringUtils.upperCase(serviceId))) - .map(Application::getInstances) - .orElse(Collections.emptyList()) - .stream() - .map(EurekaServiceInstance::new) - .map(ServiceInstance.class::cast) - .toList(); + .map(Application::getInstances) + .orElse(Collections.emptyList()) + .stream() + .map(EurekaServiceInstance::new) + .map(ServiceInstance.class::cast) + .toList(); } @Override @@ -210,10 +239,10 @@ public List getServices() { return Collections.emptyList(); } return registry.getApplications().getRegisteredApplications() - .stream() - .map(Application::getName) - .distinct() - .toList(); + .stream() + .map(Application::getName) + .distinct() + .toList(); } }; } @@ -227,7 +256,6 @@ MessageService messageService() { messageService.loadMessages("/discovery-log-messages.yml"); messageService.loadMessages("/gateway-log-messages.yml"); - messageService.loadMessages("/apiml-log-messages.yml"); messageService.loadMessages("/zaas-log-messages.yml"); return messageService; } @@ -235,10 +263,9 @@ MessageService messageService() { @Bean @Primary TomcatReactiveWebServerFactory tomcatReactiveWebServerWithFiltersFactory( - HttpHandler httpHandler, - List preFluxFilters, - List servletContextAwareListeners - ) { + HttpHandler httpHandler, + List preFluxFilters, + List servletContextAwareListeners) { return new TomcatReactiveWebServerFactory() { @Override @@ -255,21 +282,23 @@ protected void configureContext(Context context) { } /** - * Create a custom Tomcat connector with same customizations as the main external (GW) connector to handle + * Create a custom Tomcat connector with same customizations as the main + * external (GW) connector to handle * "legacy" connections in v3 meant to go to Eureka / Discovery Service * - * @param internalDiscoveryPort port that will handle legacy Discovery Service connections + * @param internalDiscoveryPort port that will handle legacy Discovery Service + * connections * @return */ @Bean WebServerFactoryCustomizer internalPortCustomizer( - @Value("${apiml.internal-discovery.port:10011}") int internalDiscoveryPort - ) { + @Value("${apiml.internal-discovery.port:10011}") int internalDiscoveryPort) { return factory -> { var connector = new Connector(); try { - Method method = TomcatReactiveWebServerFactory.class.getDeclaredMethod("customizeConnector", Connector.class); + Method method = TomcatReactiveWebServerFactory.class.getDeclaredMethod("customizeConnector", + Connector.class); method.setAccessible(true); method.invoke(factory, connector); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e) { @@ -288,7 +317,8 @@ static class ServletWithFilters extends TomcatHttpHandlerAdapter { private final Servlet servlet; private final FilterChain filterChain; - public ServletWithFilters(HttpHandler httpHandler, TomcatHttpHandlerAdapter servlet, Collection filters) { + public ServletWithFilters(HttpHandler httpHandler, TomcatHttpHandlerAdapter servlet, + Collection filters) { super(httpHandler); this.servlet = servlet; diff --git a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java index ba548da7de..7103239f22 100644 --- a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java @@ -135,20 +135,19 @@ SecurityWebFilterChain discoveryServiceClientCertificateFilterChain(ServerHttpSe notInUnauthenticatedPaths, exchange -> exchange.getRequest().getURI().getPath().startsWith("/eureka/") ? MatchResult.match() : MatchResult.notMatch() // Prevents matching /eureka (mapping for homepage in modulith) )) - .authorizeExchange(authorizeExchangeSpec -> - authorizeExchangeSpec - .anyExchange().authenticated() - ) + .authorizeExchange(authorizeExchangeSpec -> { + if (verifySslCertificatesOfServices) { + authorizeExchangeSpec + .anyExchange().authenticated(); + } else { + authorizeExchangeSpec.anyExchange().permitAll(); + } + }) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .formLogin(ServerHttpSecurity.FormLoginSpec::disable); if (verifySslCertificatesOfServices) { return x509SecurityConfig(http).build(); - } else { - http - .authorizeExchange(exchange -> exchange - .anyExchange().permitAll() - ); } return http.build(); diff --git a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java index abca4577a0..14acd6d03e 100644 --- a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java +++ b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java @@ -23,7 +23,11 @@ import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.ClientResponse; import org.zowe.apiml.constants.ApimlConstants; -import org.zowe.apiml.gateway.filters.*; +import org.zowe.apiml.gateway.filters.AbstractAuthSchemeFactory; +import org.zowe.apiml.gateway.filters.ErrorHeaders; +import org.zowe.apiml.gateway.filters.RequestCredentials; +import org.zowe.apiml.gateway.filters.ZaasInternalErrorException; +import org.zowe.apiml.gateway.filters.ZaasSchemeTransform; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; @@ -32,6 +36,7 @@ import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.schema.source.AuthSourceService; +import org.zowe.apiml.zaas.security.service.schema.source.PATAuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import reactor.core.publisher.Mono; @@ -39,7 +44,11 @@ import java.io.IOException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.*; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Optional; import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; import static org.zowe.apiml.security.common.filter.CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE; @@ -118,6 +127,10 @@ public Mono> pas if (authSource.isEmpty()) { return createMissingAuthenticationErrorMessage(); } + updateServiceId(authSource, request); + if (!authSourceService.isValid(authSource.get())) { + return createInvalidAuthenticationErrorMessage(); + } var authSourceParsed = authSourceService.parse(authSource.get()); var ticket = passTicketService.generate(authSourceParsed.getUserId(), applicationName); @@ -132,6 +145,14 @@ public Mono> pas } } + private void updateServiceId(Optional authSource, RequestCredentialsHttpServletRequestAdapter request) { + authSource + .filter(PATAuthSource.class::isInstance) + .map(PATAuthSource.class::cast) + .filter(as -> as.getDefaultServiceId() == null) + .ifPresent(as -> as.setDefaultServiceId(request.getServiceId())); + } + @Override public Mono> safIdt(RequestCredentials requestCredentials) { var applicationName = requestCredentials.getApplId(); @@ -145,6 +166,10 @@ public Mono> if (authSource.isEmpty()) { return createMissingAuthenticationErrorMessage(); } + updateServiceId(authSource, request); + if (!authSourceService.isValid(authSource.get())) { + return createInvalidAuthenticationErrorMessage(); + } var authSourceParsed = authSourceService.parse(authSource.get()); String safIdToken = tokenCreationService.createSafIdTokenWithoutCredentials(authSourceParsed.getUserId(), applicationName); @@ -164,6 +189,10 @@ public Mono> if (authSource.isEmpty()) { return createMissingAuthenticationErrorMessage(); } + updateServiceId(authSource, request); + if (!authSourceService.isValid(authSource.get())) { + return createInvalidAuthenticationErrorMessage(); + } var authSourceParsed = authSourceService.parse(authSource.get()); var response = zosmfService.exchangeAuthenticationForZosmfToken(authSource.get().getRawSource().toString(), authSourceParsed); @@ -182,7 +211,10 @@ public Mono> if (authSource.isEmpty()) { return createMissingAuthenticationErrorMessage(); } - + updateServiceId(authSource, request); + if (!authSourceService.isValid(authSource.get())) { + return createInvalidAuthenticationErrorMessage(); + } var token = authSourceService.getJWT(authSource.get()); var response = ZaasTokenResponse.builder().cookieName(COOKIE_AUTH_NAME).token(token).build(); return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); @@ -200,6 +232,10 @@ private static class RequestCredentialsHttpServletRequestAdapter implements Http @Delegate(excludes = Exclude.class) private HttpServletRequest request; + public String getServiceId() { + return requestCredentials.getServiceId(); + } + @Override public Cookie[] getCookies() { return Optional.ofNullable(requestCredentials.getCookies()) 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 cc439b4c72..effe13e562 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java @@ -27,8 +27,11 @@ import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.security.common.error.AccessTokenInvalidBodyException; import org.zowe.apiml.security.common.error.AccessTokenMissingBodyException; +import org.zowe.apiml.security.common.error.ZosAuthenticationException; import reactor.core.publisher.Mono; +import java.util.Optional; + import static org.apache.http.HttpStatus.*; @Slf4j @@ -99,4 +102,10 @@ public Mono handleBadCredentialsException(ServerWebExchange exchange, BadC return setBodyResponse(exchange, SC_UNAUTHORIZED, "org.zowe.apiml.security.login.invalidCredentials"); } + @ExceptionHandler(ZosAuthenticationException.class) + public Mono handleZosAuthenticationException(ServerWebExchange exchange, ZosAuthenticationException ex) { + log.debug("Zos Authentication Exception: {}", ex.getMessage()); + return setBodyResponse(exchange, ex.getPlatformError().responseCode.value(), Optional.ofNullable(ex.getPlatformError()).map(e -> e.errorMessage).orElse(null), ex.getMessage()); + } + } diff --git a/apiml/src/main/resources/apiml-log-messages.yml b/apiml/src/main/resources/apiml-log-messages.yml deleted file mode 100644 index 23bbc9c357..0000000000 --- a/apiml/src/main/resources/apiml-log-messages.yml +++ /dev/null @@ -1,24 +0,0 @@ -messages: - # Info messages - # 000-099 - - # General messages - # 100-199 - - # HTTP,Protocol messages - # 400-499 - - # TLS,Certificate messages - # 500-599 - - # Various messages - # 600-699 - - # Service specific messages - # 700-999 - - key: org.zowe.apiml.common.unauthorized - number: ZWEAO402 - type: ERROR - text: "The request has not been applied because it lacks valid authentication credentials." - reason: "The accessed resource requires authentication. The request is missing valid authentication credentials or the token expired." - action: "Review the product documentation for more details about acceptable authentication. Verify that your credentials are valid and contact security administrator to obtain valid credentials." diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 9492e0b9df..327720f3c9 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -13,6 +13,7 @@ eureka: server: max-threads-for-peer-replication: 2 useReadOnlyResponseCache: false + peer-node-read-timeout-ms: 5000 spring: cloud: gateway: @@ -224,6 +225,9 @@ management: access: none health: showDetails: always + health: + diskspace: + enabled: false endpoints: web: base-path: /application @@ -235,6 +239,7 @@ spring.config.activate.on-profile: debug logging: level: com.netflix: INFO # Update to DEBUG + com.netflix.eureka: DEBUG com.netflix.discovery.shared.transport.decorator: DEBUG com.sun.jersey.server.impl.application.WebApplicationImpl: INFO javax.net.ssl: ERROR diff --git a/apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java b/apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java new file mode 100644 index 0000000000..c12f5d045e --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java @@ -0,0 +1,169 @@ +/* + * 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; + +import com.netflix.eureka.EurekaServerConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; +import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.zaas.ZaasServiceAvailableEvent; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GatewayHealthIndicatorTest { + + @Mock private DiscoveryClient discoveryClient; + + private GatewayHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + healthIndicator = new GatewayHealthIndicator(discoveryClient); + ReflectionTestUtils.setField(healthIndicator, "apiCatalogServiceId", CoreService.API_CATALOG.getServiceId()); + } + + private DefaultServiceInstance getDefaultServiceInstance(String serviceId, String hostname, int port) { + return new DefaultServiceInstance( + hostname + ":" + serviceId + ":" + port, + serviceId, hostname, port, true + ); + } + + @Nested + class WhenCatalogAndDiscoveryAreAvailable { + + @BeforeEach + void setUp() { + healthIndicator.onApplicationEvent(new EurekaRegistryAvailableEvent(mock(EurekaServerConfig.class))); + healthIndicator.onApplicationEvent(new ZaasServiceAvailableEvent("dummy")); + } + + @Test + void testStatusIsUp() throws Exception { + when(discoveryClient.getInstances(CoreService.API_CATALOG.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.API_CATALOG.getServiceId(), "host", 10014))); + when(discoveryClient.getInstances(CoreService.GATEWAY.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.GATEWAY.getServiceId(), "host", 10010))); + + Health.Builder builder = new Health.Builder(); + healthIndicator.doHealthCheck(builder); + assertEquals(Status.UP, builder.build().getStatus()); + } + + } + + @Nested + class WhenDiscoveryIsNotAvailable { + + @BeforeEach + void setUp() { + healthIndicator.onApplicationEvent(new ZaasServiceAvailableEvent("dummy")); + } + + @Test + void thenStatusIsDown() throws Exception { + when(discoveryClient.getInstances(CoreService.API_CATALOG.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.API_CATALOG.getServiceId(), "host", 10014))); + when(discoveryClient.getInstances(CoreService.GATEWAY.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.GATEWAY.getServiceId(), "host", 10010))); + + Health.Builder builder = new Health.Builder(); + healthIndicator.doHealthCheck(builder); + assertEquals(Status.DOWN, builder.build().getStatus()); + } + + } + + @Nested + class WhenZaasIsNotAvailable { + + @BeforeEach + void setUp() { + healthIndicator.onApplicationEvent(new EurekaRegistryAvailableEvent(mock(EurekaServerConfig.class))); + } + + @Test + void thenStatusIsDown() throws Exception { + when(discoveryClient.getInstances(CoreService.API_CATALOG.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.API_CATALOG.getServiceId(), "host", 10014))); + when(discoveryClient.getInstances(CoreService.GATEWAY.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.GATEWAY.getServiceId(), "host", 10010))); + + Health.Builder builder = new Health.Builder(); + healthIndicator.doHealthCheck(builder); + assertEquals(Status.DOWN, builder.build().getStatus()); + } + + } + + @Nested + class GivenCustomCatalogProvider { + + @Test + void whenHealthIsRequested_thenStatusIsUp() throws Exception { + var customCatalogServiceId = "customCatalog"; + ReflectionTestUtils.setField(healthIndicator, "apiCatalogServiceId", customCatalogServiceId); + + when(discoveryClient.getInstances(customCatalogServiceId)).thenReturn( + Collections.singletonList(getDefaultServiceInstance(customCatalogServiceId, "host", 10014))); + when(discoveryClient.getInstances(CoreService.GATEWAY.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.GATEWAY.getServiceId(), "host", 10010))); + + Health.Builder builder = new Health.Builder(); + healthIndicator.doHealthCheck(builder); + + String code = (String) builder.build().getDetails().get(CoreService.API_CATALOG.getServiceId()); + assertEquals("UP", code); + } + + } + + @Nested + class GivenEverythingIsHealthy { + + @BeforeEach + void setUp() { + healthIndicator.onApplicationEvent(new EurekaRegistryAvailableEvent(mock(EurekaServerConfig.class))); + healthIndicator.onApplicationEvent(new ZaasServiceAvailableEvent("dummy")); + } + + @Test + void whenHealthRequested_onceLogMessageAboutStartup() throws Exception { + when(discoveryClient.getInstances(CoreService.API_CATALOG.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.API_CATALOG.getServiceId(), "host", 10014))); + when(discoveryClient.getInstances(CoreService.GATEWAY.getServiceId())).thenReturn( + Collections.singletonList(getDefaultServiceInstance(CoreService.DISCOVERY.getServiceId(), "host", 10011))); + + Health.Builder builder = new Health.Builder(); + healthIndicator.doHealthCheck(builder); + + assertTrue(healthIndicator.isStartedInformationPublished()); + } + + } + +} diff --git a/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java b/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java index 40ad2e4625..c05c72bb6f 100644 --- a/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java +++ b/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java @@ -10,12 +10,13 @@ package org.zowe.apiml; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.zowe.apiml.gateway.filters.RequestCredentials; import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.passticket.PassTicketException; import org.zowe.apiml.passticket.PassTicketService; import org.zowe.apiml.ticket.TicketResponse; @@ -24,6 +25,7 @@ import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.schema.source.AuthSourceService; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; +import reactor.test.StepVerifier; import javax.management.ServiceNotFoundException; @@ -46,15 +48,30 @@ class ZaasSchemeTransformApiTest { private AuthSourceService authSourceService; private PassTicketService passTicketService; private ZaasSchemeTransformApi transformApi; - private final MessageService messageService = new YamlMessageService("/apiml-log-messages.yml"); + TokenCreationService tokenCreationService; + ZosmfService zosmfService; + RequestCredentials requestCredentials; + AuthSource authSource; + private static MessageService messageService; + + @BeforeAll + static void messageService() { + messageService = YamlMessageServiceInstance.getInstance(); + messageService.loadMessages("/zaas-log-messages.yml"); + } + + private static final String INVALID_AUTH_MSG = "ZWEAO402E The request has not been applied because it lacks valid authentication credentials."; + private static final String MISSING_AUTH_MSG = "ZWEAG160E No authentication provided in the request"; @BeforeEach void setUp() { authSourceService = mock(AuthSourceService.class); + when(authSourceService.isValid(any())).thenReturn(true); + tokenCreationService = mock(TokenCreationService.class); + zosmfService = mock(ZosmfService.class); passTicketService = mock(PassTicketService.class); - ZosmfService zosmfService = mock(ZosmfService.class); - TokenCreationService tokenCreationService = mock(TokenCreationService.class); - + requestCredentials = mockCredentials(); + authSource = mock(AuthSource.class); transformApi = new ZaasSchemeTransformApi( authSourceService, passTicketService, @@ -64,276 +81,294 @@ void setUp() { ); } + @Nested - class GivenPassticket { - @Test - void thenReturnsExpectedTicket() throws PassTicketException { - RequestCredentials credentials = mockCredentials(); + class GivenPassticketScheme { + @Nested + class GivenValidAuth { - AuthSource authSource = mock(AuthSource.class); - AuthSource.Parsed parsed = mock(AuthSource.Parsed.class); - when(parsed.getUserId()).thenReturn("USER1"); + @BeforeEach + void setup() { + var parsed = mock(AuthSource.Parsed.class); + when(parsed.getUserId()).thenReturn("USER1"); + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); + when(authSourceService.parse(authSource)).thenReturn(parsed); - when(parsed.getUserId()).thenReturn("USER1"); - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.parse(authSource)).thenReturn(parsed); + } - when(passTicketService.generate("USER1", "app1")).thenReturn("ticket123"); - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.parse(authSource)).thenReturn(parsed); - when(passTicketService.generate("USER1", "app1")).thenReturn("ticket123"); + @Test + void thenReturnsExpectedTicket() throws PassTicketException { - var result = transformApi.passticket(credentials).block(); + when(passTicketService.generate("USER1", "app1")).thenReturn("ticket123"); - assertNotNull(result); - TicketResponse response = result.getBody(); - assertNotNull(response); - assertEquals("USER1", response.getUserId()); - assertEquals("ticket123", response.getTicket()); - assertEquals("app1", response.getApplicationName()); - } + StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { + assertNotNull(result); + TicketResponse response = result.getBody(); + assertNotNull(response); + assertEquals("USER1", response.getUserId()); + assertEquals("ticket123", response.getTicket()); + assertEquals("app1", response.getApplicationName()); + }).verifyComplete(); + } - @Test - void whenAuthSourceMissing_returnsMissingAuthError() { - RequestCredentials credentials = mockCredentials(); - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); + @Test + void whenTicketGenerationFails_writeErrorHeader() throws PassTicketException { - var result = transformApi.passticket(credentials).block(); + when(passTicketService.generate("USER1", "app1")).thenThrow(new RuntimeException("boom")); - assertNotNull(result); - assertNull(result.getBody()); - assertTrue(result.getHeaders().header("x-zowe-error").isEmpty()); + StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { + assertEquals("boom", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + }).verifyComplete(); + } } - @Test - void whenTicketGenerationFails_writeErrorHeader() throws PassTicketException { - RequestCredentials credentials = mockCredentials(); - AuthSource authSource = mock(AuthSource.class); - AuthSource.Parsed parsed = mock(AuthSource.Parsed.class); - - when(parsed.getUserId()).thenReturn("USER1"); - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.parse(authSource)).thenReturn(parsed); - when(passTicketService.generate("USER1", "app1")).thenThrow(new RuntimeException("boom")); - - var result = transformApi.passticket(credentials).block(); - assertEquals("boom", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); - } + @Nested + class GivenInvalidRequest { - @Test - void whenApplicationNameIsMissing_inPassticket_thenReturnsError() { - RequestCredentials credentials = mockCredentialsWithAppId(null); + @Test + void whenAuthSourceMissing_returnsMissingAuthError() { + RequestCredentials credentials = mockCredentials(); + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); + + StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + assertNotNull(result); + assertNull(result.getBody()); + assertTrue(result.getHeaders().header("x-zowe-error").isEmpty()); + }).verifyComplete(); + + } - var result = transformApi.passticket(credentials).block(); - assertNotNull(result); - assertNull(result.getBody()); + @Test + void whenAuthSourceInvalid_writeErrorHeader() throws PassTicketException { + + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); + when(authSourceService.isValid(authSource)).thenReturn(false); + + StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { + assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); + } + + @Test + void whenApplicationNameIsMissing_inPassticket_thenReturnsError() { + when(requestCredentials.getApplId()).thenReturn(null); + StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { + assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); + } + } } @Nested - class GivenSafIdt { + class GivenSafIdtScheme { + + @Test void whenMissingAppId_returnsError() { - RequestCredentials credentials = mockCredentials(); - - var result = transformApi.safIdt(credentials).block(); + when(requestCredentials.getApplId()).thenReturn(null); - assertNotNull(result); - assertNull(result.getBody()); + StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { + assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); } - @Test - void whenValidAuthSource_returnsToken() throws PassTicketException { - RequestCredentials credentials = mockCredentials(); + @Nested + class GivenValidAuth { - var authSource = mock(AuthSource.class); - var parsed = mock(AuthSource.Parsed.class); - when(parsed.getUserId()).thenReturn("USER1"); + @BeforeEach + void setup() { - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.parse(authSource)).thenReturn(parsed); + var parsed = mock(AuthSource.Parsed.class); + when(parsed.getUserId()).thenReturn("USER1"); + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); + when(authSourceService.parse(authSource)).thenReturn(parsed); - var tokenCreationService = mock(TokenCreationService.class); - when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) - .thenReturn("saf-idt"); + } - transformApi = new ZaasSchemeTransformApi( - authSourceService, - passTicketService, - mock(ZosmfService.class), - tokenCreationService, - messageService - ); + @Test + void whenValidUser_returnsToken() throws PassTicketException { - var result = transformApi.safIdt(credentials).block(); + when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) + .thenReturn("saf-idt"); - assertNotNull(result); - assertEquals("saf-idt", result.getBody().getToken()); - } + StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { + assertNotNull(result); + assertEquals("saf-idt", result.getBody().getToken()); + }).verifyComplete(); + } - @Test - void whenSafIdTokenCreationFails_returnsError() throws Exception { - RequestCredentials credentials = mockCredentials(); + @Test + void whenSafIdTokenCreationFails_returnsError() { - var authSource = mock(AuthSource.class); - var parsed = mock(AuthSource.Parsed.class); - when(parsed.getUserId()).thenReturn("USER1"); + when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) + .thenThrow(new RuntimeException("Simulated SAF IDT failure")); - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.parse(authSource)).thenReturn(parsed); + StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { + assertEquals("Simulated SAF IDT failure", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); + } + } - TokenCreationService tokenCreationService = mock(TokenCreationService.class); - when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) - .thenThrow(new RuntimeException("Simulated SAF IDT failure")); + @Nested + class GivenInvalidRequest { - transformApi = new ZaasSchemeTransformApi( - authSourceService, - passTicketService, - mock(ZosmfService.class), - tokenCreationService, - messageService - ); + @Test + void whenAuthSourceInvalid_returnsError() { - var result = transformApi.safIdt(credentials).block(); + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); + when(authSourceService.isValid(authSource)).thenReturn(false); - assertNotNull(result); - assertNull(result.getBody()); - } + StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { + assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); + } - @Test - void whenApplicationNameIsMissing_inSafIdt_thenReturnsError() { - RequestCredentials credentials = mockCredentialsWithAppId(" "); // blank + @Test + void whenApplicationNameIsMissing_inSafIdt_thenReturnsError() { + RequestCredentials credentials = mockCredentialsWithAppId(" "); // blank + + StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); - var result = transformApi.safIdt(credentials).block(); + } - assertNotNull(result); - assertNull(result.getBody()); } } @Nested - class GivenZoweJwt { - @Test - void thenReturnsJwt() { - RequestCredentials credentials = mockCredentials(); + class GivenZoweJwtScheme { - var authSource = mock(AuthSource.class); - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.getJWT(authSource)).thenReturn("jwt-token"); + @Nested + class GivenValidAuth { - var result = transformApi.zoweJwt(credentials).block(); + @BeforeEach + void setup() { - assertNotNull(result); - ZaasTokenResponse response = result.getBody(); - assertNotNull(response); - assertEquals("jwt-token", response.getToken()); - } + var parsed = mock(AuthSource.Parsed.class); + when(parsed.getUserId()).thenReturn("USER1"); + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); + when(authSourceService.parse(authSource)).thenReturn(parsed); - @Test - void whenMissingAuthSource_returnsError() { - RequestCredentials credentials = mockCredentials(); + } - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); - var result = transformApi.zoweJwt(credentials).block(); + @Test + void thenReturnsJwt() { + + when(authSourceService.getJWT(authSource)).thenReturn("jwt-token"); + + StepVerifier.create(transformApi.zoweJwt(requestCredentials)).assertNext(result -> { + assertNotNull(result); + ZaasTokenResponse response = result.getBody(); + assertNotNull(response); + assertEquals("jwt-token", response.getToken()); + }).verifyComplete(); + } - assertNotNull(result); - assertNull(result.getBody()); + @Test + void whenJwtRetrievalFails_returnsErrorResponse() { + when(authSourceService.getJWT(authSource)).thenThrow(new RuntimeException("boom")); + + StepVerifier.create(transformApi.zoweJwt(requestCredentials)).assertNext(result -> { + assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); + } } @Test - void whenJwtRetrievalFails_returnsErrorResponse() { - RequestCredentials credentials = mockCredentials(); - AuthSource authSource = mock(AuthSource.class); - - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.getJWT(authSource)).thenThrow(new RuntimeException("boom")); + void whenMissingAuthSource_returnsError() { - var result = transformApi.zoweJwt(credentials).block(); + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); - assertNotNull(result); - assertNull(result.getBody()); + StepVerifier.create(transformApi.zoweJwt(requestCredentials)).assertNext(result -> { + assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); } - } - @Nested - class GivenZosmf { - @Test - void whenValidAuthSource_returnsTokenResponse() throws ServiceNotFoundException { - RequestCredentials credentials = mockCredentials(); + } - var authSource = mock(AuthSource.class); - var parsed = mock(AuthSource.Parsed.class); - when(authSource.getRawSource()).thenReturn("raw".toCharArray()); - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.parse(authSource)).thenReturn(parsed); + @Nested + class GivenZosmfScheme { - ZaasTokenResponse mockResponse = ZaasTokenResponse.builder().token("zosmf-token").build(); + @Nested + class GivenValidAuth { - ZosmfService zosmfService = mock(ZosmfService.class); - when(zosmfService.exchangeAuthenticationForZosmfToken(anyString(), eq(parsed))) - .thenReturn(mockResponse); + AuthSource.Parsed parsed; + ZosmfService zosmfService; - transformApi = new ZaasSchemeTransformApi( - authSourceService, - passTicketService, - zosmfService, - mock(TokenCreationService.class), - messageService - ); + @BeforeEach + void setup() { - var result = transformApi.zosmf(credentials).block(); + parsed = mock(AuthSource.Parsed.class); + zosmfService = mock(ZosmfService.class); - assertNotNull(result); - assertEquals("zosmf-token", result.getBody().getToken()); - } + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); + when(authSourceService.parse(authSource)).thenReturn(parsed); + when(authSource.getRawSource()).thenReturn("raw".toCharArray()); + transformApi = new ZaasSchemeTransformApi( + authSourceService, + passTicketService, + zosmfService, + mock(TokenCreationService.class), + messageService + ); - @Test - void whenAuthSourceMissing_returnsMissingAuthError() { - RequestCredentials credentials = mockCredentials(); + } - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); + @Test + void whenValidAuthSource_returnsTokenResponse() throws ServiceNotFoundException { - var result = transformApi.zosmf(credentials).block(); + ZaasTokenResponse mockResponse = ZaasTokenResponse.builder().token("zosmf-token").build(); - assertNotNull(result); - assertNull(result.getBody()); - } + when(zosmfService.exchangeAuthenticationForZosmfToken(anyString(), eq(parsed))) + .thenReturn(mockResponse); - @Test - void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundException { - RequestCredentials credentials = mockCredentials(); + StepVerifier.create(transformApi.zosmf(requestCredentials)).assertNext(result -> { - var authSource = mock(AuthSource.class); - var parsed = mock(AuthSource.Parsed.class); - when(authSource.getRawSource()).thenReturn("raw".toCharArray()); + assertNotNull(result); + assertEquals("zosmf-token", result.getBody().getToken()); + }).verifyComplete(); + } - when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); - when(authSourceService.parse(authSource)).thenReturn(parsed); + @Test + void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundException { - ZosmfService zosmfService = mock(ZosmfService.class); - when(zosmfService.exchangeAuthenticationForZosmfToken(any(), any())) - .thenThrow(new RuntimeException("Simulated failure")); + when(zosmfService.exchangeAuthenticationForZosmfToken(any(), any())) + .thenThrow(new RuntimeException("Error returned from zosmf")); + StepVerifier.create(transformApi.zosmf(requestCredentials)).assertNext(result -> { + assertEquals("Error returned from zosmf", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); + } + } - transformApi = new ZaasSchemeTransformApi( - authSourceService, - passTicketService, - zosmfService, - mock(TokenCreationService.class), - messageService - ); + @Test + void whenAuthSourceMissing_returnsMissingAuthError() { - var result = transformApi.zosmf(credentials).block(); + when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); + StepVerifier.create(transformApi.zosmf(requestCredentials)).assertNext(result -> { + assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + assertNull(result.getBody()); + }).verifyComplete(); - assertNotNull(result); - assertNull(result.getBody()); } + } private RequestCredentials mockCredentialsWithAppId(String appId) { diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java index f02e49136f..c44f5db764 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java @@ -19,6 +19,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; import org.zowe.apiml.ApimlApplication; +import org.zowe.apiml.gateway.config.GatewayHealthIndicator; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -27,15 +28,23 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@ComponentScan(basePackages = "org.zowe.apiml", excludeFilters = { - @ComponentScan.Filter( - type = FilterType.REGEX, - pattern = ".*Application" - ), - @ComponentScan.Filter( - type = FilterType.ASSIGNABLE_TYPE, - classes = EurekaController.class - )}) +@ComponentScan( + basePackages = "org.zowe.apiml", + excludeFilters = { + @ComponentScan.Filter( + type = FilterType.REGEX, + pattern = ".*Application" + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = EurekaController.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = GatewayHealthIndicator.class + ) + } +) @SpringBootTest(classes = { ApimlApplication.class, FreeMarkerConfigurer.class, diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/EurekaEndpointsTests.java b/apiml/src/test/java/org/zowe/apiml/acceptance/EurekaEndpointsTests.java index d5dfcbb683..c8d632391f 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/EurekaEndpointsTests.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/EurekaEndpointsTests.java @@ -13,6 +13,8 @@ import com.sun.net.httpserver.Headers; import freemarker.template.TemplateException; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.zowe.apiml.EurekaDashboardController; import org.zowe.apiml.gateway.MockService; @@ -58,4 +60,20 @@ void testEurekaHomePage() throws TemplateException, IOException { .statusCode(200); } + @Test + void testEurekaPeerNodeReplica() { + given() + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body( + """ + {"replicationList":[{"appName":"GATEWAY","id":"apiml-2:gateway:10010","lastDirtyTimestamp":1751961556383,"status":"UP","instanceInfo":{"instanceId":"apiml-2:gateway:10010","hostName":"apiml-2","app":"GATEWAY","ipAddr":"127.0.0.1","status":"UP","overriddenStatus":"UNKNOWN","port":{"$":10010,"@enabled":"false"},"securePort":{"$":10010,"@enabled":"true"},"countryId":1,"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"} + """ + ) + .when() + .post(URI.create("https://localhost:10011/eureka/peerreplication/batch/")) + .then() + .statusCode(403); + } + } diff --git a/apiml/src/test/java/org/zowe/apiml/controller/ApimlExceptionHandlerTest.java b/apiml/src/test/java/org/zowe/apiml/controller/ApimlExceptionHandlerTest.java new file mode 100644 index 0000000000..359fbb9d7d --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/controller/ApimlExceptionHandlerTest.java @@ -0,0 +1,69 @@ +/* + * 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.controller; + +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.junit.jupiter.MockitoExtension; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.security.common.auth.saf.PlatformReturned; +import org.zowe.apiml.security.common.error.ZosAuthenticationException; +import reactor.core.publisher.Mono; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ApimlExceptionHandlerTest { + + private ApimlExceptionHandler exceptionHandler; + + @BeforeEach + void setUp() { + exceptionHandler = spy(new ApimlExceptionHandler(null, null, null) { + @Override + public Mono setBodyResponse(ServerWebExchange exchange, int responseCode, String messageCode, Object... args) { + return Mono.empty(); + } + }); + } + + @Nested + class GivenApimlExceptionHandler { + + @Nested + class WhenZosAuthenticationException { + + @Test + void thenProvideDetails() { + var request = MockServerHttpRequest.get("https://localhost/some/url").build(); + var exchange = MockServerWebExchange.from(request); + var ex = new ZosAuthenticationException(PlatformReturned.builder() + .errno(139) + .errnoMsg("ABC") + .build()); + + exceptionHandler.handleZosAuthenticationException(exchange, ex); + + verify(exceptionHandler).setBodyResponse(eq(exchange), eq(500), eq("org.zowe.apiml.security.platform.errno.ERROR"), anyString()); + } + + } + + } + +} diff --git a/config/local-multi/apiml-1.yml b/config/local-multi/apiml-1.yml new file mode 100644 index 0000000000..9f9556636b --- /dev/null +++ b/config/local-multi/apiml-1.yml @@ -0,0 +1,73 @@ +spring.profiles.active: debug +logging: + level: + org.springframework.web.socket.client.standard.AnnotatedEndpointConnectionManager: DEBUG + org.springframework.web.reactive.socket.client.StandardWebSocketClient: TRACE + com.netflix.eureka: DEBUG + com.netflix.discovery.DiscoveryClient: DEBUG + com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient: DEBUG + # org.apache.tomcat.websocket.WsWebSocketContainer: TRACE + # org.apache.tomcat: DEBUG + # org.apache.coyote: DEBUG + +server: + max-http-request-header-size: 16348 + webSocket: + requestBufferSize: 16348 + ssl: + clientAuth: want + keyAlias: localhost + keyPassword: password + keyStore: keystore/localhost/localhost.keystore.p12 + keyStorePassword: password + keyStoreType: PKCS12 + trustStore: keystore/localhost/localhost.truststore.p12 + trustStorePassword: password + trustStoreType: PKCS12 + +apiml: + cache: + storage: + location: /tmp + service: + discoveryServiceUrls: https://localhost:10011/eureka,https://localhost:10021/eureka + internal-discovery: + port: 10011 + health: + protected: false + gateway: + servicesToLimitRequestRate: discoverableclient + cookieNameForRateLimiter: apimlAuthenticationToken + discovery: + allPeersUrls: https://localhost:10011/eureka,https://localhost:10021/eureka + security: + # ssl: + # verifySslCertificatesOfServices: false + x509: + enabled: true + certificatesUrl: https://localhost:10010/gateway/certificates + allowTokenRefresh: true + webfinger: + fileLocation: config/local/webfinger.yml + personalAccessToken: + enabled: true + oidc: + enabled: false + clientId: + clientSecret: + registry: zowe.okta.com + identityMapperUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/zss/api/v1/certificate/dn + identityMapperUser: APIMTST + jwks: + uri: + auth: + jwt: + customAuthHeader: customJwtHeader + passticket: + customUserHeader: customUserHeader + customAuthHeader: customPassticketHeader + provider: dummy + zosmf: + serviceId: mockzosmf # Replace me with the correct z/OSMF service id + + diff --git a/config/local-multi/apiml-2.yml b/config/local-multi/apiml-2.yml new file mode 100644 index 0000000000..5da1e595a2 --- /dev/null +++ b/config/local-multi/apiml-2.yml @@ -0,0 +1,69 @@ +spring.profiles.active: diag +logging: + level: + org.springframework.web.socket.client.standard.AnnotatedEndpointConnectionManager: DEBUG + org.springframework.web.reactive.socket.client.StandardWebSocketClient: TRACE + # org.apache.tomcat.websocket.WsWebSocketContainer: TRACE + # org.apache.tomcat: DEBUG + # org.apache.coyote: DEBUG + +server: + max-http-request-header-size: 16348 + webSocket: + requestBufferSize: 16348 + ssl: + clientAuth: want + keyAlias: localhost-multi + keyPassword: password + keyStore: keystore/localhost/localhost-multi.keystore.p12 + keyStorePassword: password + keyStoreType: PKCS12 + trustStore: keystore/localhost/localhost-multi.truststore.p12 + trustStorePassword: password + trustStoreType: PKCS12 + +apiml: + service: + port: 10020 + hostname: localhost2 + discoveryServiceUrls: https://localhost:10011/eureka,https://localhost2:10021/eureka + internal-discovery: + port: 10021 + health: + protected: false + gateway: + servicesToLimitRequestRate: discoverableclient + cookieNameForRateLimiter: apimlAuthenticationToken + discovery: + allPeersUrls: https://localhost:10011/eureka,https://localhost2:10021/eureka + security: + # ssl: + # verifySslCertificatesOfServices: false + x509: + enabled: true + certificatesUrl: https://localhost:10010/gateway/certificates + allowTokenRefresh: true + webfinger: + fileLocation: config/local/webfinger.yml + personalAccessToken: + enabled: true + oidc: + enabled: false + clientId: + clientSecret: + registry: zowe.okta.com + identityMapperUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/zss/api/v1/certificate/dn + identityMapperUser: APIMTST + jwks: + uri: + auth: + jwt: + customAuthHeader: customJwtHeader + passticket: + customUserHeader: customUserHeader + customAuthHeader: customPassticketHeader + provider: dummy + zosmf: + serviceId: mockzosmf # Replace me with the correct z/OSMF service id + + diff --git a/config/local/apiml-service.yml b/config/local/apiml-service.yml index 439d3b473e..563962a57e 100644 --- a/config/local/apiml-service.yml +++ b/config/local/apiml-service.yml @@ -20,6 +20,8 @@ server: # redisratelimiter: TRACE apiml: + internal-discovery: + port: 10011 health: protected: false gateway: 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 f6f23e6edb..2d98b8da27 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 @@ -11,6 +11,7 @@ package org.zowe.apiml.discovery; import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; import com.netflix.eureka.EurekaServerConfig; @@ -62,6 +63,7 @@ public class ApimlInstanceRegistry extends InstanceRegistry { private MethodHandle register2ArgsMethodHandle; private MethodHandle register3ArgsMethodHandle; private MethodHandle cancelMethodHandle; + private MethodHandle replicateToPeersMethodHandle; private final ApplicationContext appCntx; private final EurekaConfig.Tuple tuple; @@ -148,6 +150,11 @@ private void init() { Field registryField = AbstractInstanceRegistry.class.getDeclaredField("registry"); registryField.setAccessible(true); this.registry = (ConcurrentHashMap>>) registryField.get(this); + + Method replicateToPeers = PeerAwareInstanceRegistryImpl.class.getDeclaredMethod("replicateToPeers", Action.class, String.class, String.class, InstanceInfo.class, InstanceStatus.class, boolean.class); + replicateToPeers.setAccessible(true); + + replicateToPeersMethodHandle = MethodHandles.lookup().unreflect(replicateToPeers); } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { throw new IllegalArgumentException(EXCEPTION_MESSAGE, e); } @@ -165,7 +172,22 @@ protected int resolveInstanceLeaseDurationRewritten(final InstanceInfo info) { } } - public void registerStatically(InstanceInfo instanceInfo, boolean isReplication) { + public void peerAwareHeartbeat(InstanceInfo instanceInfo) { + try { + replicateToPeersMethodHandle.invokeWithArguments(this, Action.Heartbeat, instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null, false); + } catch (Throwable e) { + throw new IllegalStateException(EXCEPTION_MESSAGE, e); + } + } + + /** + * Register a service statically + * + * @param instanceInfo InstanceInfo of the registered instance + * @param isReplication Whether the registration information source is a replication event + * @param peerReplicate Whether to peer replicate the newly registered instance + */ + public void registerStatically(InstanceInfo instanceInfo, boolean isReplication, boolean peerReplicate) { // the maximum lease duration time (Eureka bug: overflow of int during conversion to ms) int leaseDuration = Integer.MAX_VALUE / 1000; @@ -174,6 +196,11 @@ public void registerStatically(InstanceInfo instanceInfo, boolean isReplication) int backup = expectedNumberOfClientsSendingRenews; try { register(instanceInfo, leaseDuration, isReplication); + if (peerReplicate) { + replicateToPeersMethodHandle.invokeWithArguments(this, Action.Register, instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null, isReplication); + } + } catch (Throwable e) { + throw new IllegalStateException(EXCEPTION_MESSAGE, e); } finally { expectedNumberOfClientsSendingRenews = backup; } @@ -197,6 +224,9 @@ public boolean isExpired(long additionalLeaseMs) { staticRegistrationIds.add(instanceInfo.getInstanceId()); } + /** + * Does not do peer replica + */ @Override public void register(InstanceInfo info, int leaseDuration, boolean isReplication) { info = changeServiceId(info); diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java index 0a40eebb60..437964a69e 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java @@ -15,7 +15,6 @@ import com.netflix.eureka.EurekaServerContextHolder; import com.netflix.eureka.registry.InstanceRegistry; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.zowe.apiml.discovery.ApimlInstanceRegistry; @@ -43,7 +42,6 @@ public class StaticServicesRegistrationService { private final List staticInstances = new CopyOnWriteArrayList<>(); - @Autowired public StaticServicesRegistrationService(ServiceDefinitionProcessor serviceDefinitionProcessor, MetadataDefaultsService metadataDefaultsService) { this.serviceDefinitionProcessor = serviceDefinitionProcessor; this.metadataDefaultsService = metadataDefaultsService; @@ -99,7 +97,7 @@ StaticRegistrationResult registerServices(String staticApiDefinitionsDirectories for (InstanceInfo instanceInfo : result.getInstances()) { result.getRegisteredServices().add(instanceInfo.getInstanceId()); staticInstances.add(instanceInfo); - registry.registerStatically(instanceInfo, false); + registry.registerStatically(instanceInfo, false, false); } return result; 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 512d893d42..89975c17a4 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 @@ -47,6 +47,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -288,7 +289,9 @@ class WhenStaticallyRegistration { @Test @SuppressWarnings("unchecked") - void test() throws Throwable { + void givenStaticRegistration_thenSuccessful() throws Throwable { + var methodHandle = mock(MethodHandle.class); + ReflectionTestUtils.setField(apimlInstanceRegistry, "replicateToPeersMethodHandle", methodHandle); var currentStaticIds = (Set) ReflectionTestUtils.getField(apimlInstanceRegistry, "staticRegistrationIds"); assertTrue(currentStaticIds.isEmpty()); @@ -297,8 +300,9 @@ void test() throws Throwable { Map> leaseMap = new HashMap<>(); when(registry.get(anyString())).thenReturn(leaseMap); + doReturn(new Object()).when(methodHandle).invokeWithArguments(any(), any(), any(), any(), any(), any(), any()); - apimlInstanceRegistry.registerStatically(standardInstance, false); + apimlInstanceRegistry.registerStatically(standardInstance, false, true); assertFalse(currentStaticIds.isEmpty()); assertFalse(leaseMap.isEmpty()); @@ -306,6 +310,22 @@ void test() throws Throwable { } + @Nested + class HeartbeatPeerReplicate { + + @Test + void givenPeerReplicaHeartbeat_thenSuccess() throws Throwable { + var methodHandle = mock(MethodHandle.class); + ReflectionTestUtils.setField(apimlInstanceRegistry, "replicateToPeersMethodHandle", methodHandle); + var instance = mock(InstanceInfo.class); + doReturn(new Object()).when(methodHandle).invokeWithArguments(any(), any(), any(), any(), any(), any(), any()); + apimlInstanceRegistry.peerAwareHeartbeat(instance); + + verify(methodHandle, times(1)).invokeWithArguments(any(), any(), any(), any(), any(), any(), any()); + } + + } + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenCancelRegistration { diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java index bb3b0a3533..814791b3a3 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java @@ -72,7 +72,7 @@ void setUp() { when(event.getInstanceInfo()).thenReturn(x.getArgument(0)); eurekaInstanceRegisteredListener.listen(event); return mockRegistry; - }).when(mockRegistry).registerStatically(any(), anyBoolean()); + }).when(mockRegistry).registerStatically(any(), anyBoolean(), anyBoolean()); EurekaServerContext mockEurekaServerContext = mock(EurekaServerContext.class); when(mockEurekaServerContext.getRegistry()).thenReturn(mockRegistry); diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java index 11915de450..2eca509c76 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java @@ -153,7 +153,7 @@ void testRenewInstances() { StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); registrationService.registerServices(directory); - verify(mockRegistry, times(1)).registerStatically(instance, false); + verify(mockRegistry, times(1)).registerStatically(instance, false, false); } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..469920d496 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,102 @@ +# Docker compose for API ML modulith +version: '3.8' + +services: + apiml: + image: ghcr.io/zowe/apiml:latest + ports: + - "10010:10010" # External access + - "10011:10011" # External access + - "5130:5130" # For Java remote debugging + volumes: + - /api-defs:/api-defs # Update to an existing local directory + networks: + - apiml_net + - apiml_shared + container_name: apiml + environment: + - APIML_SECURITY_AUTH_PROVIDER=saf + - APIML_SERVICE_HOSTNAME=apiml + - APIML_DISCOVERY_ALLPEERSURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka + - logbackService=ZWEAGW1 + discoverable-client: + image: ghcr.io/zowe/discoverable-client:latest + networks: + - apiml_net + container_name: discoverable-client + environment: + - APIML_SERVICE_DISCOVERYSERVICEURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka + - APIML_SERVICE_HOSTNAME=discoverable-client + discoverable-client-2: + image: ghcr.io/zowe/discoverable-client:latest + networks: + - apiml2_net + container_name: discoverable-client-2 + environment: + - APIML_SERVICE_DISCOVERYSERVICEURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka + - APIML_SERVICE_HOSTNAME=discoverable-client-2 + + api-catalog-services: + image: ghcr.io/zowe/api-catalog-services:latest + networks: + - apiml_net + - apiml_shared + container_name: api-catalog-services + volumes: + - /Users/pc891986/Projects/api-layer-v3/config/docker/api-defs:/api-defs + environment: + - APIML_SERVICE_DISCOVERYSERVICEURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka + - APIML_SERVICE_HOSTNAME=api-catalog-services + + apiml-2: + image: ghcr.io/zowe/apiml:latest + ports: + - "10020:10010" + - "10021:10011" + - "5011:5130" + networks: + - apiml2_net + - apiml_shared + volumes: + - /Users/pc891986/Projects/api-layer-v3/config/docker/api-defs:/api-defs + container_name: apiml-2 + environment: + - APIML_SECURITY_AUTH_PROVIDER=saf + - APIML_SERVICE_HOSTNAME=apiml-2 + - APIML_DISCOVERY_ALLPEERSURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka + - logbackService=ZWEAGW2 + caching-service: + image: ghcr.io/zowe/caching-service:latest + networks: + - apiml_net + - apiml_shared + container_name: caching-service + environment: + - APIML_SERVICE_HOSTNAME=caching-service + - APIML_SERVICE_DISCOVERYSERVICEURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka + caching-service-2: + image: ghcr.io/zowe/caching-service:latest + networks: + - apiml_net + - apiml_shared + container_name: caching-service-2 + environment: + - APIML_SERVICE_HOSTNAME=caching-service-2 + - APIML_SERVICE_DISCOVERYSERVICEURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka + mock-services: + image: ghcr.io/zowe/mock-services:latest + networks: + - apiml_net + container_name: mock-services + environment: + - APIML_SERVICE_HOSTNAME=mock-services + - APIML_SERVICE_DISCOVERYSERVICEURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka +networks: + apiml_net: + driver: bridge + + apiml2_net: + driver: bridge + + apiml_shared: + driver: bridge diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java index a9a1f84962..7be21cd967 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java @@ -14,10 +14,12 @@ import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Component; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; +import org.zowe.apiml.product.compatibility.ApimlHealthCheckHandler; import org.zowe.apiml.product.constants.CoreService; import static org.springframework.boot.actuate.health.Status.DOWN; @@ -25,10 +27,14 @@ /** * Gateway health information (/application/health) + * This class contributes Gateway's information to {@link ApimlHealthCheckHandler} + * */ @Component +@ConditionalOnMissingBean(name = "modulithConfig") public class GatewayHealthIndicator extends AbstractHealthIndicator { - private final DiscoveryClient discoveryClient; + + protected final DiscoveryClient discoveryClient; private String apiCatalogServiceId; private final ApimlLogger apimlLog = ApimlLogger.of(GatewayHealthIndicator.class, @@ -41,7 +47,6 @@ public GatewayHealthIndicator(DiscoveryClient discoveryClient, this.apiCatalogServiceId = apiCatalogServiceId; } - @Override protected void doHealthCheck(Health.Builder builder) { boolean anyCatalogIsAvailable = apiCatalogServiceId != null && !apiCatalogServiceId.isEmpty(); @@ -65,7 +70,13 @@ protected void doHealthCheck(Health.Builder builder) { builder.withDetail(CoreService.API_CATALOG.getServiceId(), toStatus(apiCatalogUp).getCode()); } - if (!startedInformationPublished && discoveryUp && apiCatalogUp && zaasUp) { + if (discoveryUp && apiCatalogUp && zaasUp) { + onFullyUp(); + } + } + + private void onFullyUp() { + if (!startedInformationPublished) { apimlLog.log("org.zowe.apiml.common.mediationLayerStarted"); startedInformationPublished = true; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java index b452aa7943..4632d4270a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java @@ -22,7 +22,7 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.util.pattern.PathPatternParser; import org.zowe.apiml.util.CorsUtils; -import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; @@ -47,21 +47,21 @@ void initCorsConfigurationSource() { } @EventListener(RefreshRoutesEvent.class) - public void onRefreshRoutesEvent(RefreshRoutesEvent event) { - discoveryClient.getServices() - .flatMap(service -> discoveryClient.getInstances(service).collectList()) - .flatMap(Flux::fromIterable) - .toIterable() - .forEach(serviceInstance -> - corsUtils.setCorsConfiguration( - serviceInstance.getServiceId().toLowerCase(), - serviceInstance.getMetadata(), + public Mono onRefreshRoutesEvent(RefreshRoutesEvent event) { + return discoveryClient.getServices() + .flatMap(discoveryClient::getInstances) + .map(instance -> { + corsUtils.setCorsConfiguration( + instance.getServiceId().toLowerCase(), + instance.getMetadata(), (prefix, serviceId, config) -> { - serviceId = serviceInstance.getMetadata().getOrDefault(APIML_ID, serviceInstance.getServiceId().toLowerCase()); + serviceId = instance.getMetadata().getOrDefault(APIML_ID, instance.getServiceId().toLowerCase()); urlBasedCorsConfigurationSource.registerCorsConfiguration("/" + serviceId + "/**", config); } - ) - ); + ); + return instance; + }).then(); + } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java index a74cbb2dde..44f5760dba 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java @@ -258,7 +258,7 @@ protected ServerHttpRequest updateHeadersForError(ServerWebExchange exchange, St } @Data - protected abstract static class AbstractConfig { + public abstract static class AbstractConfig { // service ID of the target service private String serviceId; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/CorsPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/CorsPerServiceTest.java index a779887d22..79b02d656d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/CorsPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/CorsPerServiceTest.java @@ -12,8 +12,8 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; +import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; import java.io.IOException; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java index 6732cd3bd9..25bf721001 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java @@ -13,7 +13,10 @@ import org.apache.logging.log4j.util.TriConsumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.gateway.config.GlobalCorsProperties; @@ -25,6 +28,7 @@ import org.zowe.apiml.constants.EurekaMetadataDefinition; import org.zowe.apiml.util.CorsUtils; import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; import java.util.Collections; import java.util.HashMap; @@ -33,15 +37,20 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +@ExtendWith(MockitoExtension.class) class ServiceCorsUpdaterTest { private static final String SERVICE_ID = "myserviceid"; private static final String APIML_ID = "apimlid"; private CorsUtils corsUtils = spy(new CorsUtils(true, Collections.emptyList())); - private ReactiveDiscoveryClient discoveryClient = mock(ReactiveDiscoveryClient.class); + + @Mock private ReactiveDiscoveryClient discoveryClient; private ServiceCorsUpdater serviceCorsUpdater; @@ -70,13 +79,15 @@ private ServiceInstance createServiceInstance(String serviceId) { return serviceInstance; } + @SuppressWarnings("unchecked") private TriConsumer getCorsLambda(Consumer> metadataProcessor) { - ServiceInstance serviceInstance = createServiceInstance(SERVICE_ID); + var serviceInstance = createServiceInstance(SERVICE_ID); metadataProcessor.accept(serviceInstance.getMetadata()); doReturn(Flux.just(SERVICE_ID)).when(discoveryClient).getServices(); - serviceCorsUpdater.onRefreshRoutesEvent(new RefreshRoutesEvent(this)); + StepVerifier.create(serviceCorsUpdater.onRefreshRoutesEvent(new RefreshRoutesEvent(this))) + .verifyComplete(); ArgumentCaptor> lambdaCaptor = ArgumentCaptor.forClass(TriConsumer.class); verify(corsUtils).setCorsConfiguration(anyString(), any(), lambdaCaptor.capture()); diff --git a/gradle/jib.gradle b/gradle/jib.gradle index 07e0f1a46a..ff41b73a71 100644 --- a/gradle/jib.gradle +++ b/gradle/jib.gradle @@ -1,7 +1,7 @@ def setJib(componentName, javaAgentPort, debugPort, applicationPorts) { def imageTag = project.hasProperty("zowe.docker.tag") ? project.getProperty("zowe.docker.tag"): "latest" def imageName = project.hasProperty("zowe.docker.container") ? "${project.getProperty("zowe.docker.container")}${componentName}:${imageTag}" : "ghcr.io/zowe/${componentName}:${imageTag}" - def javaAgentOptions = project.hasProperty("zowe.docker.debug") ? ['-javaagent:/jacocoagent.jar=output=tcpserver,address=*,port=' + javaAgentPort, '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=' + debugPort]: ['-javaagent:/jacocoagent.jar=output=tcpserver,address=*,port=' + javaAgentPort] + def javaAgentOptions = project.hasProperty("zowe.docker.debug") ? ['-javaagent:/jacocoagent.jar=output=tcpserver,address=*,port=' + javaAgentPort, '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:' + debugPort]: ['-javaagent:/jacocoagent.jar=output=tcpserver,address=*,port=' + javaAgentPort] def addOpensOptions = ['--add-opens=java.base/java.nio.channels.spi=ALL-UNNAMED', '--add-opens=java.base/java.util=ALL-UNNAMED', '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', '--add-opens=java.base/java.lang=ALL-UNNAMED', '--add-opens=java.base/java.lang.invoke=ALL-UNNAMED', '--add-opens=java.base/javax.net.ssl=ALL-UNNAMED', '-Dspring.profiles.include=dev,debug'] jib.from.image = 'ibm-semeru-runtimes:open-17.0.14_7-jre-jammy' diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index ceba004e88..cba14a6362 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -342,7 +342,7 @@ task runContainerModulithTests(type: Test) { task runContainerTests(type: Test) { group "Integration tests" - description "Run only tests without long tests" + description "Run only tests without long tests. These tests run in Github Actions CITests workflow" outputs.cacheIf { false } def envConfig = System.getenv("ENV_CONFIG") ? System.getenv("ENV_CONFIG") : "docker" @@ -374,6 +374,22 @@ task runContainerTests(type: Test) { } } +task runContainerSAFProviderTests(type: Test) { + group "Integration tests" + description "Run only tests that verify SAF (non z/OSMF) authentication scenarios" + + outputs.cacheIf { false } + def envConfig = System.getenv("ENV_CONFIG") ? System.getenv("ENV_CONFIG") : "docker" + systemProperty "environment.config", "-$envConfig" + + systemProperties System.properties + useJUnitPlatform { + includeTags( + 'SAFProviderTest' + ) + } +} + task runBaseTests(type: Test) { group "integration tests" description "Run base tests" diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java index 6f14c3fe4f..8862542b96 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java @@ -41,10 +41,12 @@ * will be produced. *

* Also verify that the invalid credentials will be properly rejected. + * */ @SAFAuthTest @Tag("SAFProviderTest") class SafLoginTest implements TestWithStartedInstances { + @BeforeAll static void switchToTestedProvider() { RestAssured.useRelaxedHTTPSValidation(); @@ -52,8 +54,10 @@ static void switchToTestedProvider() { @Nested class WhenUserAuthenticatesTwice { + @Nested class ReturnTwoDifferentValidTokens { + @ParameterizedTest(name = "givenValidCredentialsInBody {index} {0} ") @MethodSource("org.zowe.apiml.integration.authentication.providers.LoginTest#loginUrlsSource") void givenValidCredentialsInBody(URI loginUrl) { @@ -62,7 +66,9 @@ void givenValidCredentialsInBody(URI loginUrl) { assertThat(jwtToken1, is(not(jwtToken2))); } + } + } @Nested diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLogoutTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLogoutTest.java index 9f4e9fd7f4..ede55a5382 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLogoutTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLogoutTest.java @@ -36,8 +36,10 @@ static void switchToTestedProvider() { @Nested class WhenUserLogsOutTwice { + @Nested class SecondCallReturnUnauthorized { + @ParameterizedTest(name = "givenValidToken {index} {0} ") @MethodSource("org.zowe.apiml.integration.authentication.providers.LogoutTest#logoutUrlsSource") void givenValidToken(String logoutUrl) { @@ -48,11 +50,14 @@ void givenValidToken(String logoutUrl) { assertLogout(logoutUrl, jwt, SC_NO_CONTENT); assertLogout(logoutUrl, jwt, SC_UNAUTHORIZED); } + } + } @Nested class WhenUserLogsOutOnceWithMultipleTokens { + @Nested class VerifySecondTokenIsValid { @ParameterizedTest(name = "givenTwoValidTokens {index} {0} ") @@ -71,6 +76,9 @@ void givenTwoValidTokens(String logoutUrl) { logoutOnGateway(logoutUrl, jwt2); } + } + } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java index 9e82b08298..fdbab3663a 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java @@ -17,6 +17,7 @@ import io.restassured.response.ValidatableResponseOptions; import org.apache.http.HttpHeaders; import org.apache.http.message.BasicNameValuePair; +import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; @@ -60,12 +61,12 @@ public class PassticketSchemeTest implements TestWithStartedInstances { public static Stream getTokens() { return Stream.of( - Arguments.of("validJwt", SC_OK), - Arguments.of("noSignJwt", SC_OK), - Arguments.of("publicKeySignedJwt", SC_OK), - Arguments.of("changedRealmJwt", SC_OK), - Arguments.of("changedUserJwt", SC_OK), - Arguments.of("personalAccessToken", SC_OK) + Arguments.of("validJwt", SC_OK, Matchers.blankOrNullString()), + Arguments.of("noSignJwt", SC_OK, startsWith("ZWEAG160E")), + Arguments.of("publicKeySignedJwt", SC_OK, startsWith("ZWEAG160E")), + Arguments.of("changedRealmJwt", SC_OK, startsWith("ZWEAO402E")), + Arguments.of("changedUserJwt", SC_OK, startsWith("ZWEAG160E")), + Arguments.of("personalAccessToken", SC_OK, Matchers.blankOrNullString()) ); } @@ -101,11 +102,13 @@ class GivenGatewayUrlTests { void givenValidJWT_thenTranslateToPassticket() { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), REQUEST_INFO_ENDPOINT); verifyPassTicketHeaders( + //@formatter:off given() .cookie(COOKIE_NAME, jwt) .when() .get(scgUrl) .then() + //@formatter:on ); } @@ -113,6 +116,7 @@ void givenValidJWT_thenTranslateToPassticket() { @Tag("GatewayServiceRouting") void givenNoJWT_thenErrorHeaderIsCreated() { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), REQUEST_INFO_ENDPOINT); + //@formatter:off given() .when() .get(scgUrl) @@ -120,6 +124,7 @@ void givenNoJWT_thenErrorHeaderIsCreated() { .statusCode(SC_OK) .body("headers.x-zowe-auth-failure", startsWith("ZWEAG160E")) .header(ApimlConstants.AUTH_FAIL_HEADER, startsWith("ZWEAG160E")); + //@formatter:on } } @@ -134,11 +139,13 @@ class ResultContainsPassticketAndNoJwt { @InfinispanStorageTest void givenJwtInBearerHeader(String token, String cookie, Header header) { verifyPassTicketHeaders( + //@formatter:off given() .header(header) .when() .get(requestUrl) .then() + //@formatter:on ); } @@ -149,11 +156,13 @@ void givenJwtInBearerHeader(String token, String cookie, Header header) { void givenJwtInCookie(String token, String cookie) { verifyPassTicketHeaders( + //@formatter:off given() .cookie(cookie, token) .when() .get(requestUrl) .then() + //@formatter:on ); } @@ -161,11 +170,13 @@ void givenJwtInCookie(String token, String cookie) { @Test void givenBasicAuth() { verifyPassTicketHeaders( + //@formatter:off given() .auth().preemptive().basic(USERNAME, new String(PASSWORD)) .when() .get(requestUrl) .then() + //@formatter:on ); } @@ -175,12 +186,14 @@ void givenBasicAuth() { void givenJwtInHeaderAndCookie(String token, String cookie, Header header) { verifyPassTicketHeaders( + //@formatter:off given() .cookie(cookie, token) .header(header) .when() .get(requestUrl) .then() + //@formatter:on ); } @@ -191,21 +204,23 @@ void givenJwtInHeaderAndCookie(String token, String cookie, Header header) { void givenBasicAndJwtInCookie(String token, String cookie) { verifyPassTicketHeaders( + //@formatter:off given() .auth().preemptive().basic(USERNAME, new String(PASSWORD)) .cookie(cookie, token) .when() .get(requestUrl) .then() + //@formatter:on ); } - @ParameterizedTest(name = "call passticket service with {0} to receive response code {2}") + @ParameterizedTest(name = "call passticket service with {0} to receive response code {1}") @MethodSource("org.zowe.apiml.integration.authentication.schemes.PassticketSchemeTest#getTokens") @InfinispanStorageTest @TestsNotMeantForZowe - void whenCallPassTicketService(String tokenType, int status) throws JSONException { + void whenCallPassTicketService(String tokenType, int status, Matcher matcher) throws JSONException { String token = getToken(tokenType); //@formatter:off @@ -214,6 +229,8 @@ void whenCallPassTicketService(String tokenType, int status) throws JSONExceptio .when() .get(discoverablePassticketUrl) .then() + .header(ApimlConstants.AUTH_FAIL_HEADER, matcher) + .log().all() .statusCode(is(status)); //@formatter:on } @@ -227,12 +244,14 @@ class VerifyPassTicketIsOk { @MethodSource("org.zowe.apiml.integration.authentication.schemes.PassticketSchemeTest#accessTokens") @InfinispanStorageTest void givenCorrectToken(String token, String cookie) { + //@formatter:off given() .cookie(cookie, token) .when() .get(discoverablePassticketUrl) .then() .statusCode(is(SC_OK)); + //@formatter:on } } @@ -252,6 +271,7 @@ void givenIssuedForIncorrectApplId(String token, String cookie) { new BasicNameValuePair("applId", "XBADAPPL") ); + //@formatter:off given() .cookie(cookie, token) .when() @@ -259,7 +279,7 @@ void givenIssuedForIncorrectApplId(String token, String cookie) { .then() .statusCode(is(SC_INTERNAL_SERVER_ERROR)) .body("message", containsString(expectedMessage)); - + //@formatter:on } } @@ -268,6 +288,7 @@ class StorePassTicketInHeader { @Test void givenCustomHeader() { + //@formatter:off given() .cookie(COOKIE_NAME, jwt) .when() @@ -276,6 +297,7 @@ void givenCustomHeader() { .body("headers.custompassticketheader", Matchers.notNullValue()) .body("headers.customuserheader", Matchers.notNullValue()) .statusCode(200); + //@formatter:on } } @@ -289,6 +311,7 @@ class PassticketMisconfiguration { "/dcnopassticket/api/v1/request,200,When APPLID is not set then passticket is not set and the Gateway returns 200" }) void givenJwt(String url, int responseCode, String description) { + //@formatter:off given() .cookie(COOKIE_NAME, jwt) .when() @@ -296,6 +319,7 @@ void givenJwt(String url, int responseCode, String description) { .then() .body("headers.authorization", Matchers.nullValue()) .statusCode(responseCode); + //@formatter:on } } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/AuthenticationHaTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/AuthenticationHaTest.java new file mode 100644 index 0000000000..5ddcf10a62 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/AuthenticationHaTest.java @@ -0,0 +1,75 @@ +/* + * 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.ha; + +import io.restassured.RestAssured; +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.zowe.apiml.util.SecurityUtils; +import org.zowe.apiml.util.categories.HATest; +import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.config.GatewayServiceConfiguration; + +import static org.zowe.apiml.util.SecurityUtils.assertIfLogged; +import static org.zowe.apiml.util.SecurityUtils.getConfiguredSslConfig; +import static org.zowe.apiml.util.requests.Endpoints.ROUTED_LOGOUT; + +/** + * In initial version, basic logout test to verify token invalidation in HA scenarios + * + */ +@HATest +@Tag("SAFProviderTest") +class AuthenticationHaTest { + + private static final GatewayServiceConfiguration GATEWAY_CONF = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + + @BeforeEach + void setUp() { + RestAssured.useRelaxedHTTPSValidation(); + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + } + + @Nested + class GivenMultipleInstances { + + @Nested + class WhenUserLogOut { + + @Test + void thenTokenIsInvalidatedInBoth() { + var jwt = SecurityUtils.gatewayToken(); + + assertIfLogged(jwt, true); + + // Logout on any instance + SecurityUtils.logoutOnGateway(SecurityUtils.getGatewayUrl(getGatewayHosts()[0], ROUTED_LOGOUT), jwt); + + // Verify token is invalid in one or more Gateway instances + var gatewayHosts = getGatewayHosts(); + for (String host : gatewayHosts) { + SecurityUtils.assertIfLogged(jwt, false, host); + } + + } + + } + + } + + // assume only two gateway (or apiml) instances + private String[] getGatewayHosts() { + return GATEWAY_CONF.getHost().split(","); + } + +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/EurekaReplicationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/EurekaReplicationTest.java index b6ac686c8b..6154742d44 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/EurekaReplicationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/EurekaReplicationTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.zowe.apiml.util.TestWithStartedInstances; import org.zowe.apiml.util.categories.HATest; import org.zowe.apiml.util.requests.Apps; @@ -28,6 +29,7 @@ */ @HATest class EurekaReplicationTest implements TestWithStartedInstances { + private HADiscoveryRequests haDiscoveryRequests = new HADiscoveryRequests(); /** @@ -35,9 +37,16 @@ class EurekaReplicationTest implements TestWithStartedInstances { */ @Nested class GivenMultipleEurekaInstances { + @Nested class WhenLookingForEurekas { + @Test + @DisabledIfSystemProperty( + disabledReason = "In Modulith, Discovery Service is only one in each API ML instance", + named = "environment.modulith", + matches = "true" + ) void eurekaReplicasAreVisible() { assumeTrue(haDiscoveryRequests.existing() > 1); @@ -45,7 +54,11 @@ void eurekaReplicasAreVisible() { for (Integer registeredToInstance : instances) { assertThat(registeredToInstance, is(haDiscoveryRequests.existing())); } + } + } + } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayMultipleInstancesTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayMultipleInstancesTest.java index ad22e6a3da..9122cdd578 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayMultipleInstancesTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/GatewayMultipleInstancesTest.java @@ -29,6 +29,7 @@ */ @HATest public class GatewayMultipleInstancesTest { + private final HAGatewayRequests haGatewayRequests = new HAGatewayRequests(); private final HADiscoveryRequests haDiscoveryRequests = new HADiscoveryRequests(); @@ -39,8 +40,10 @@ void setUp() { @Nested class GivenMultipleGatewayInstances { + @Nested class WhenSendingRequest { + @Test void gatewayInstancesAreUp() { assumeTrue(haGatewayRequests.existing() > 1); @@ -54,6 +57,9 @@ void gatewayInstancesAreRegistered() { assertThat(haDiscoveryRequests.getAmountOfRegisteredInstancesForService(0, Apps.GATEWAY), is(2)); } + } + } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java b/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java index 4bc62a1e72..0df05a9af1 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java +++ b/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java @@ -19,11 +19,15 @@ import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.zowe.apiml.util.config.ConfigReader; import org.zowe.apiml.util.config.Credentials; import org.zowe.apiml.util.config.DiscoverableClientConfiguration; +import org.zowe.apiml.util.config.DiscoveryServiceConfiguration; import org.zowe.apiml.util.config.GatewayServiceConfiguration; +import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.http.HttpClientUtils; import org.zowe.apiml.util.http.HttpRequestUtils; @@ -42,17 +46,21 @@ @Slf4j public class ApiMediationLayerStartupChecker { + private static final boolean IS_MODULITH_ENABLED = Boolean.parseBoolean(System.getProperty("environment.modulith")); + private final GatewayServiceConfiguration gatewayConfiguration; private final DiscoverableClientConfiguration discoverableClientConfiguration; + private final DiscoveryServiceConfiguration discoveryServiceConfiguration; private final Credentials credentials; private final List servicesToCheck = new ArrayList<>(); private final String healthEndpoint = "/application/health"; - private static final boolean IS_MODULITH_ENABLED = Boolean.parseBoolean(System.getProperty("environment.modulith")); + public ApiMediationLayerStartupChecker() { gatewayConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); credentials = ConfigReader.environmentConfiguration().getCredentials(); discoverableClientConfiguration = ConfigReader.environmentConfiguration().getDiscoverableClientConfiguration(); + discoveryServiceConfiguration = ConfigReader.environmentConfiguration().getDiscoveryServiceConfiguration(); servicesToCheck.add(new Service("Gateway", "$.status")); if (!IS_MODULITH_ENABLED) { @@ -123,11 +131,14 @@ private boolean areAllServicesUp() { Integer amountOfActiveGateways = context.read("$.components.gateway.details.gatewayCount"); + var expectedGatewayCount = Integer.getInteger("environment.gwCount", gatewayConfiguration.getInstances()); + boolean isValidAmountOfGatewaysUp = amountOfActiveGateways != null && - amountOfActiveGateways >= gatewayConfiguration.getInstances(); + amountOfActiveGateways >= expectedGatewayCount; log.debug("There are {} gateways", amountOfActiveGateways); if (!isValidAmountOfGatewaysUp) { log.debug("Expecting at least {} gateways", gatewayConfiguration.getInstances()); + callEurekaApps(); return false; } // Consider properly the case with multiple gateway services running on different ports. @@ -140,18 +151,39 @@ private boolean areAllServicesUp() { requestToGateway.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString(String.format("%s:%s", credentials.getUser(), credentials.getPassword()).getBytes())); var response = HttpClientUtils.client().execute(requestToGateway); if (response.getStatusLine().getStatusCode() != 200) { + log.debug("Response from gateway at {} was: {}", requestToGateway.getURI(), response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : "undefined"); throw new IOException(); } } } - return areAllServicesUp && isTestApplicationUp; + var result = areAllServicesUp && isTestApplicationUp; + if (!result) { + log.debug("API ML is not ready, check which services are missing in the above messages"); + } + return result; } catch (PathNotFoundException | IOException e) { log.warn("Check failed on retrieving the information from document: {}", e.getMessage()); return false; } } + private void callEurekaApps() { + HttpGet requestToEurekaApps = new HttpGet(HttpRequestUtils.getUriFromService(discoveryServiceConfiguration, "/eureka/apps")); + CloseableHttpClient client = HttpClients.custom().setSSLContext(SslContext.sslClientCertValid).build(); + try (client) { + var response = client.execute(requestToEurekaApps); + var entity = response.getEntity(); + if (entity != null) { + log.debug("eureka/apps: {}", EntityUtils.toString(entity)); + } else { + log.debug("eureka/apps entity is null"); + } + } catch (Exception e) { + log.error("Cannot call Eureka apps", e); + } + } + private boolean isAuthUp() { HttpGet requestToZaas; if (!IS_MODULITH_ENABLED) { diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java index 7be5d738ee..9c1dd056ca 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java @@ -135,12 +135,22 @@ protected static String getUsername() { //@formatter:off + public static String getGatewayUrl(String host, String path) { + return getGatewayUrl(host, path, GATEWAY_PORT); + } + public static String getGatewayUrl(String path) { return getGatewayUrl(path, GATEWAY_PORT); } public static String getGatewayUrl(String path, int port) { - return String.format("%s://%s:%d%s", GATEWAY_SCHEME, GATEWAY_HOST, port, path); + // chose one if comma-splitted, HA tests should handle multi-valued hosts + var gatewayHost = GATEWAY_HOST.split(",")[0]; + return getGatewayUrl(gatewayHost, path, port); + } + + public static String getGatewayUrl(String host, String path, int port) { + return String.format("%s://%s:%d%s", GATEWAY_SCHEME, host, port, path); } public static String getGatewayLogoutUrl(String path) { @@ -552,18 +562,29 @@ public static void assertIfLogged(String jwt, boolean logged) { given() .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt) - .when() + .when() .get(HttpRequestUtils.getUriFromGateway(ROUTED_QUERY)) - .then() + .then() + .statusCode(status.value()); + } + + public static void assertIfLogged(String jwt, boolean logged, String gatewayHost) { + final HttpStatus status = logged ? HttpStatus.OK : HttpStatus.UNAUTHORIZED; + + given() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt) + .when() + .get(HttpRequestUtils.getUriFromGateway(ROUTED_QUERY, gatewayHost)) + .then() .statusCode(status.value()); } public static void assertLogout(String url, String jwtToken, int expectedStatusCode) { given() .cookie(COOKIE_NAME, jwtToken) - .when() + .when() .post(url) - .then() + .then() .statusCode(is(expectedStatusCode)); } diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/categories/SAFAuthTest.java b/integration-tests/src/test/java/org/zowe/apiml/util/categories/SAFAuthTest.java index 35b79a88ef..d3b0053da8 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/categories/SAFAuthTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/categories/SAFAuthTest.java @@ -20,7 +20,7 @@ import static java.lang.annotation.ElementType.TYPE; /** - * Tests intended to verify SAF authentication, through either of the providers. + * Tests intended to verify SAF authentication, through ANY of the providers (i.e. both z/OSMF and SAF). */ @Tag("SAFAuthTest") @Target({ TYPE, METHOD }) 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 00af09f902..7d3ddaeccc 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 @@ -29,9 +29,10 @@ @Slf4j public class ConfigReader { + public static final boolean IS_MODULITH_ENABLED = Boolean.getBoolean("environment.modulith"); + private static final String PASSWORD = "password"; private static String configurationFile; - private static final boolean IS_MODULITH_ENABLED = Boolean.parseBoolean(System.getProperty("environment.modulith")); static { configurationFile = "environment-configuration" + System.getProperty("environment.config", "") + ".yml"; diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java b/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java index 3395da6c16..fced3dc4f9 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java @@ -22,6 +22,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.*; +import java.util.function.Function; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -94,13 +95,20 @@ public static URI getUri(String scheme, String host, int port, String endpoint, } public static URI getUriFromService(ServiceConfiguration serviceConfiguration, String endpoint, NameValuePair...arguments) { - String scheme = serviceConfiguration.getScheme(); - var host = serviceConfiguration.getHost(); - var hostnameTokenizer = new StringTokenizer(host, ","); - host = hostnameTokenizer.nextToken(); - if (serviceConfiguration instanceof GatewayServiceConfiguration s && StringUtils.isNotBlank(s.getDvipaHost())) { - host = s.getDvipaHost(); - } + return getUriFromService(serviceConfiguration, endpoint, sc -> { + var host = sc.getHost(); + var hostnameTokenizer = new StringTokenizer(host, ","); + host = hostnameTokenizer.nextToken(); // take first + if (sc instanceof GatewayServiceConfiguration s && StringUtils.isNotBlank(s.getDvipaHost())) { + host = s.getDvipaHost(); + } + return host; + }, arguments); + } + + public static URI getUriFromService(ServiceConfiguration serviceConfiguration, String endpoint, Function hostSelector, NameValuePair...arguments) { + var scheme = serviceConfiguration.getScheme(); + var host = hostSelector.apply(serviceConfiguration); int port = serviceConfiguration.getPort(); return getUri(scheme, host, port, endpoint, arguments); } @@ -109,6 +117,10 @@ public static URI getUriFromGateway(String endpoint, NameValuePair...arguments) return getUriFromService(ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(), endpoint, arguments); } + public static URI getUriFromGateway(String endpoint, String gatewayHostname, NameValuePair...arguments) { + return getUriFromService(ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(), endpoint, s -> gatewayHostname, arguments); + } + public static URI getUriFromZaas(String endpoint, NameValuePair...arguments) { return getUriFromService(ConfigReader.environmentConfiguration().getZaasConfiguration(), endpoint, arguments); } diff --git a/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml b/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml new file mode 100644 index 0000000000..f2548c9a1d --- /dev/null +++ b/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml @@ -0,0 +1,65 @@ +credentials: + # Mainframe credentials are required - see /integration-tests/README.md for instructions + user: USER + password: validPassword + clientUser: APIMTST +gatewayServiceConfiguration: + scheme: https + host: apiml,apiml-2 + port: 10010 + internalPorts: 10010 + externalPort: 10010 + instances: 2 + servicesEndpoint: gateway/api/v1/services + bucketCapacity: 20 +centralGatewayServiceConfiguration: + scheme: https + host: central-gateway-service + port: 10010 +discoveryServiceConfiguration: + scheme: https + user: user + password: user + host: apiml + additionalHost: apiml-2 + additionalPort: 10011 + port: 10011 + instances: 2 +discoverableClientConfiguration: + scheme: https + port: 10012 + applId: ZOWEAPPL + host: discoverable-client,discoverable-client-2 + instances: 1 +apiCatalogServiceConfiguration: + scheme: https + url: + host: api-catalog-services,api-catalog-services-2 + port: 10014 + instances: 2 +cachingServiceConfiguration: + url: +tlsConfiguration: + keyAlias: localhost + keyPassword: password + keyStoreType: PKCS12 + keyStore: ../keystore/docker/all-services.keystore.p12 + clientKeystore: ../keystore/client_cert/client-certs.p12 + clientCN: APIMTST + keyStorePassword: password + trustStoreType: PKCS12 + trustStore: ../keystore/docker/all-services.truststore.p12 + trustStorePassword: password +zosmfServiceConfiguration: + scheme: https + host: mock-services + port: 10013 + serviceId: mockzosmf +auxiliaryUserList: + value: 'unauthorized,USER1,validPassword;servicesinfo-authorized,USER,validPassword;servicesinfo-unauthorized,USER1,validPassword' +idpConfiguration: + host: https://dev-95727686.okta.com +safIdtConfiguration: + enabled: true +oidcConfiguration: + clientId: diff --git a/keystore/docker/all-services.keystore.p12 b/keystore/docker/all-services.keystore.p12 index 2bb7e9d5cae897b87f9c03a91bbbd40ce2517cd2..038113fd09faf901e203d383d125ac1b2d38ba25 100644 GIT binary patch delta 5771 zcmV;67If*UBi=1mFoG7^0s#Xsf)+1+)eU zDuzgg_YDCD0ic2fr38Wnp)i64oiKt0nFb3ghDe6@4FL=a0Ro_c1wb%@1w51f1t1&p zwKVuP`&S~LXB9B}$VFc+(qn=G0w)jx0U(iz9e*%r=%K#8ju4rORP&NkQD0YZEt*TTqT;KSA| zSL-CyMal1(7L~JfHk$(B^GTwJv27 zOBHGgp(KsLShN31NlWHlHZ+CEq0Js2BHxeX(I+c<}7dJ=UH9GHzZu6M4z+J z2l!)wqt;@x=Ut?-cuLQcL}whmVio0f3n&H&npX_@0w%#9d*>51k%U7i`F}HO^2Cwg z-03)k6RC-!EAk}Y0eRHA(qu@(>Og;j?SJHM-D68{75fMgC!^RSOD7NCwcfa`%?>Ta zR+m(aZl9oeVwUC1p(ryEKkv4E6!ynkHQk6p^Y))g?6B}iE19bpXQVA|2Q58_AN_1; z@9>^FgasHd7M`#Rok4K>!G9yrpBMjzjTf&uwamjLdr)g<73H`Uj-EKnoxGq$ULKBn zJIZq*$rf z8yAhrvV-22VAMbtPGcICbJ3nsldv93%kho7En?xvh&3k!xOA}rHGc&sTe?09XJ$z9 zi_*o$h&6Wwpzt{&vA#1-{7wnZw!~k>j9z#ztJ+J>&h%tdp`HM!I5y{v$K2dEp;b|t z?rng;fpS&Ep2%}u`26nOH79>+2)dB!$Q9m3gQcr7mhuI5c><@(H;0) zL$HG9Jt0E5J;#nIXgId=6O}X!=<`+SA4RbiS-^PMV50o;6I4V`R{5)$hMlc};2`Yw2 zhW8Bt2^29D9+RB~6hI*c2`Yw2hW8Bt2^BFE1QJwfZDk-aH#IRdH8(LaF*r0ZFoF=l z1_>&LNQUvn*G5HP;2ZDLPN!GAQ5gA&8MNgGH64yneP zy{s)1ga%C4npib3Ug;e0ZL8uJ{TzZ|;23%PE)0DLZdW`*P(dc%%$D%{5e#K=QyRO3 zF1qY6NjoxlI2Lan7w%AIH7SO@3qg5q_y*?p?ANzMq<<)R|&)^ zn04kl0RCf2Y>$i|;R{FUs?4xk zU_m&BfQZEKP=zFe{5(#TwY8cQ3rNFsT!N*R|07MrWyQB2ygS3j4+{`c8<9(`8-9_@ zhfIm3_J4Jp47{nwV+ccwmtCtLPqXDNcib>=v!K*Cv-1Byx*%+V(WdQmM{^2j{u?|x z5RVlL|3>?dW4zzLT;Yf^gjhCTl6pO*nd#!Y?{7FeAfwsaxy`A>k?$Rs#T!5&tWudC zM__-a7kD>~h(ibBz@5qpF)cigHSU$ipUUp}Xn&go{_F<=`J@LtyRd^+%Tg*hZH(m~ z5llmzdjaQkTPVK0CuM!4y69}qwn9I6H4&)A8J&PNr1MVv$|* zXA-j=!Ks@{pGv-K$vBe+E7pQ3_OScc!7)F&x_4tZv@2eld?I^)L9Xf0;FJ}%tc;2V zqJRIy8Co72`105$izp#y&G+`eJF+Y}N@Ehwz=*Ee;IJR)Q-VNf<6~Qvfe7i|s3Vkx zgc{e~lM36Gkb=Uhf;oD$H}djM*efkCVW-c}SozittR>vwcIfk<34~FYMK!CK>8P6$ zdxmvfw7+_|CzL&eLgT8WUu9(`Q6u^3wX^mp0%_aU{+~5Q!Wey}0EF@No4vhEbANtd zitQtNl*MeRSLY1~;}M?i0Z?)+5RO&-4QF*(RaA!OZr$Grvqbs?6SnX(F?h-L=|}mQ z*|a0|fyU!#WMGP5`H^OBlsC*r{zNse>~^|qDsBY}`xav;xw>Gy15Y?D_gkR=p~yAE z7(W`G0sztoPPqNhun2oltjCe2)qjiW@1;|}A4lllQW+NwUSHc|)##cp&F2=Y6^>U_ z6M5!W&HoQJd_!x(UVx%432V>LtskLBxlGCI#Era%DU%_8f>L()QQJsSv*;8rI%xU* zNwoi`rBHRyCTSr-$6t~-rGU8Fwp|*MXux3#4^#m*@~~{x83QozKmtIoSbs%nR@ji9 zaC@USY!Qbs%sK(Ugy4STEef=PUBcI@V(T`3$$&RGrm6`x3~8mmgWA@;j>?mdwoT&CO$Dg2)oRId8 z@RgLHbz{3X>j=uUslzE57Jppw`J~0ErbU%_eUBrbB0ioO^#`+`8~2B&%THWAHpC z4-kyySc*}#dz|@bf!AJ1<+K-19c#!(eW(vke$Ds^cNr1l$bVOu9?@ajJU_;4!3{p~ z6l~sDPH~A`))dG|cp`BNs@x?q$j03Q~XQ{rbn>} zHjIPmaJ|EKyuz6%GenLL9+NE+6I9uYWK$v?skIWhx6ieF#lok+v;B zj#+J|6P>mu;)V#G7n0Wcg&thP+vfIv__E-EkJ%(M2`LxAKnsWjHnqlm7SjY`u|N7} zWXQyb2iyGmO~z=HIsMVBBZ}PZHcf;M|3+0NRqvo)_-B;&uIa7gv3xEvhi+lb+8HeI z-qSCxB7d5u(2V>*BJf=2PC}x&{MhaasM>U*YLqD*e z9f(#Fun_ifnp?OhtsJ50Li>)32NNh`XJed%UsVZ4vUHD2u^jC%#}E7M4>fx$1-2Vd zXjqqVh}e4ZVwSoWJfwYEcuKIGT9Nz_6Bel|)PE6Zoz%aEP5TS(uLfRSFvZ0W&BEj6ue^QC~%MF5FwZvNt#+FjxTV_vkJu#yJio7tejB&O;P%G`5#Yjcwy@~kD`SaH z5lAPe9Jg71`CK7{+f{?B6w=1h#k>klTx*M4iO;GJ22cSW5fe3jK-iK3AW2Ar!6hhO zyb-QJ5#MN!7Sw$PodwVbM2F0dB-g-6M}Hh^VjqFybzeLs`-h5}-{!aV94R5$LY)iE z@jQxe4+oD<>I?93wOwIu^n*fkbeR3 zB%sGNvH7E{rSxKFTZlOpVQS8DpoP12&Bb^_O%HUUKRlieW94Riy?lQN3IklhbrGB3 zhfs4RA#HR2R1SIJnUQwH5i;5p`U{!_{r1$$M+DN}oN#|t4@NMXj?vzZ0=~8vp**z4c!r z)CW0N{_lHIrA|)}T+*PPm=qBmAuoc;Im@*;BUPkP%SkfeDvEZx$ypi>MipNh;d@%( z+4y)~C;RbdXAFuf&$DECAKstWn&=gl<#onGThgd9avZeSf%7k#4$&DjbBi z>8nzf`-)CJrRN0!(L9r7Fr5-T9YGcJ$UzN}05Lp;mkkWi&*xbuir4B|^WFH*%BQ_$ zRpZ=0AmBt?Iq-i(;5Wp|Vyb~lAANlHCDy6HM?-o1A_JnCo^&sLw$kfMByILlR36Kw z@;p^_&Jx#SqjMZ*B!6tF(fVmcAfODT3`t|wi#m8hSjlA}5B^^RmH&<5o6S6g(kXotJyxU)NH9$k>Y{sFb>x-dLAB=x}!6*?AKr%TxN zq*TDCQHbs&Dz$N4hNL)uCm&sUY42x?gE6=U9Ge5jN7$`GE3GXS`f8%}el~v)w<~)~ zin`oOUacXmn}1DlG$CsHM1F{_z=EK}->tZ_|R1AZ~hS-!1q-+xLM7QI%=u!>vVqF9lAn^pl` zlW2>x?TN%sY$;xB9>?7k2&Q!kEooUWU3dlvl$qFxDBb+az;lrHiDmZ_lW(2iI1IjM z>?qtgD4HTMM5V^kMfZR5ra%cF^_e@zBMu_ z{YK<6Kz|Fa&RmYqZD0Cq#|k;>Sh$UKe<4mj!RF@Ymy~aUsS0w@7YA@Anfrm~WnZqG z^g#F>b69BIo<7VWmf=o24~p#{zC8+&rKLykFW^jr-tX$#TfS!k&kc~o-zgQHJ#P-F z(a3cW-UqBAX=qI0$IGu%g#(!ZMD*TrRied*d4KpXMnhne8>iz{8l67dBC=z_=Go1C zBMZ}MMP%60T6TzJG4xQl5Qq;qMKMSco=hDwoVs)MOgIwKs8z&o>`j}@s660iESaPF zI?Od@%siR97RWf$c-FS7iRIC07RIDj#p8PNeN>*@A1n!q#saV>G;BIPfL9qO=$v|X z)PLQpGeriFm+NnWzN8$qtQ=~nb!4CXPpv}T-s>=2;p!ZK-IPz;UTV+&f^XB`l$O0Z z=}R(T8h`sVlyu92uRj(LJUV{L+V36`1T)mpVRwlsR|4qr(h%=a*4D;N2r`J+t)Of3 zO6&cq)D*_A;FAasDw-tr-pylk&CZ<}hJO<7q@+Jgf?_mN6S;nCPJApmUJep2;G*;n zUbFSOJN-v{mdBhFs}FYQs-5&$1PA{~=pT+?u3Z9nJqS}yZ)yB?x-H#E&d8eyF%cCI zuV08|_LI5K3GtNG`u`ii9iS?Aa|&U)3VVKe1zHgiX3_t&%ZQ0Jl3)pZ^nq$DB!5C! zG;72tXm^oR^_H{li`Zg}*re|AOG_#vVHG!MoCt5_p~sm1R7`94*26B*_u3Y?;N8t( zvo)|P16VHPBhScmq4!$qTk-l%vxR>E+9tUnaKT_1Lz;Nkn&ulT`dvK`M945bFd;Ar z1_dh)0|FWa00b1H+BCggi9(S|(B*NO1QhDZP4F5XKb3fcVPEOK4ab}h JaN7a`2mpPj&XxcG delta 4452 zcmY+GWmMD+x5Z(YAyi@{1O^Z!9cqx2hKFupXi-WUDPch3k3;8Bg3>J|-67J9N=bJ} zcQ};9>%Hr)=Uw;1S$nPDK4*X2>n!jVvjPY-v;c%p@K;_ElE6b%LeEI?iSV=0P#rKD zs__@AAkdJC|7St6!DvYOU;N?kBoW^IzpHz9@Il#V$kV@281dgQ2q{7rA@FYi+qUBF9p!qmh!27w5HVhSJR{Kh5c1w8c(zDA>-{TKUrhoS4IVCx7ji!S z;Oku$d?ROoOX0SfDBLOIJnK3U9ns1278PfIE2diN7a7RBO2~WQzNvbhY)|81T)e@) zv$CDT>)wKKZ23;0^gebo)JJBt6j6Mm$2^CN=NHI3r{?HbgtFpb+GR{;1S+19lmHpG zaO5q;zJvkcR;Un$CYIr2jcFf${ z!a`}mIUJsDEw+8-ZfthNM3QrKqRvCi+2t5m=y)3C)chYqcBnhtL z!<5f{eDd7TQHu?CE$rwQ{~fE}zvr_d?xc8t&aHu;^KAvP#Ke-%CHb<-3LDlSzh!>j z(_o*OMhG(QPq$CTCimw)=&&0@1@fx7tG&pS#vhH1N5kYzI0RPZfS7e}wQ2o=jCQOZ zVUKp76^xc>pq=7aE5EdDU$>P~ekQ*O=vNS~39F+0LzSa-;u*q(wXn3s=;9I+OY;j?vCap=|g@L-ttMxp{S~*_K{6STZ-2B)vX`{ao`qEJ= z%Ex!kamZ?``V1p21FLTUXRfA79$Syn(P6g63eR)yz0eL zUhNf)_RF7mUBt>PZC{m{7Vv*0qh-mvpteAiTVO$9h@xEqa9?C!bdrX`CnwQ?2#tR1wj>x0v;`&PlTwq zXtuR*3Cu;-8}10^94i9-{^3EGq^A8qz3lM`7%R$LI{(COMpA)OE<1^6L(hp*tDQcV z5Wc|d!w2L@mek9EFD@!E)ztjyr+s=)BGiuY_~JXyC@Q*?jUT0-VSY=$dRpe^Fu*3I zRAoiJZXl7OCD~wXxpYp9#n_D!fPW^;pD_B*L8Dz zj^SM$^5z>#HXFzRIoZ+3JpU1`oJH45PAPi);{));hXT<*-F!3hv=f@IAM7)30{&WM zC2k^rkww6ps2Zdux$ZB;Pn~}4*Ek$nZUht|Ok4eWUs#tbRV~vpYE_j#Ai3(+(RIi< z>6`I_M@X^9V^9*gwUx5dYxl1 z^}mc-fCfwzb%1T27z-NID1GSfCF}0d>!CD@K;Mo3ca4ZJ8+{l37X^Uu@%{=b@f^ZU zTe%C9l3JAi$a$of=nuAUZ>oQc`rn$3Wd>S>zASM}G7ke89*_A!%N&8|yLhrTn8TrL zwieiFkpJ^Xycn{;Eyxz@N8{n93rdN^{-Y6_VZGR-?-T$CuGrt4k<_frZ)@O`_R^Fx zpi|3gIK?a7SvT?Gl6`Eh_{<~AmDifwcmoe#^VMZC>$C9Emi4)nf411eVsQ5?zXGQC z#k1KImWv(Ka^jF1(#{Cm5JjIAU1ib=-fUYsbdT!!IN5~*?XyuuZB6LK%{B}b=aOYT zk(w3rbBm8y#4P07&l`qO5Sfd%^|XeMvMP_ymE)j{f@a5Z+%=g#nT+#GWND+=5OxCu zZ+foyl{-m1{5&RCC*itQ`-W=Z1n{;W{bNzDp<*a0^k0ZdWW>z(iG)SDI~ zU!D=lT2AZim~Be3#1gW+b=?#dc?c*JA*iLjPC%!+UE~34k}@OPwmWXzgV1G%9_eC6 z-a@~iqGpc|_^Jlw!}@O@hw<<=5-I2o@^7{dKl^Oh*oWI9_`I79Xm{ASRLiH(&akiu zOj_)!pa?72=$P`bfMmS}zie$TWO0CoLoJLV3^I z8tO^kfVAiSEJ9ajAJ}S0xDyh5=7POe$8_lw!l>UIuk+^rn3aR+&ekWx$}ck}B(bTc zHHDwlYj=L&S|vAu14Rn2NC|DL12;pSLPcgtBDz#IEU8wig88P7JIkEznQ4OAEJGq@ z3K{Hnw@WES^ z6#DRT^RXRm;H?X@j6-cWVH2;>KP>ayPz7pQ$S3p;QOC%9tRD^QC(s0Q&Iabw_b6*Xfs!FS~Lrzc#Vpw=J z^%KB{I9W=Jc&1a%H8=2rC@<+q#!43aNf|E9mM>CkH?|P*>)g(}-Sd|WO$3))13|UD zn=_n!-%rj#itfZw0!|^-hj|^J8D*quV9T42l%YE>VY0Q>0n!(a>kjRBHHf zh05G5j60)08V~22do%E42qOUZb@DPi5CxKy*M1M(ASY2eG3QN8u)#tc6*$Ry*S+3)(X!efRdj+JH%X?0y5E}n?@a}(j&+sjd-lVD^nzx(}9c}PCON!ZTl@|8G>qD z9_wieoC9<8{&o*I#$)U=!WpIn)X6_w5o~A0Tf9&*Iord#wPO~D8S{@O0n8h~9?n;# zOV!xiMSttpsl>*89Qi&U11Ddz{7|NuutKvw27SIx>&I0}V2i`0 zXN!7-5jPlLv1-Vxhu+U6l*>_8OIjM4>U*R9qqM=`>W)R`MxmKyIX9q*RO1xkQL*Sa z);aKWB)hCVfltw(qg6Df50{OzM$N`DcRcv($?+S3PL0m*gK#-R-s0_f&j-n)f93dh;FvysMTpMg0wi4opai1U0~ydc@RRZm7k8J2C#}+c6cAu0Ps2ci^An-ws9o z6Y<(>N)&|+e)i#9tz8k!`oF+8u2V%%vjVeV)_+P)>-Dm=<`h;*@B>FocL!@gV;~-Uy5dT;wnu?1_6-Cmw#M)Jd5d@-tF#%Xxju< z6pTzchPq{VL%v4@7Lq2x-q1fIf7uk$k=^Q? cla .signWith(jwtSecurityInitializer.getJwtSecret(), jwtSecurityInitializer.getSignatureAlgorithm()).compact(); } - @SuppressWarnings("java:S5659") // It is checking the signature securely - https://github.com/zowe/api-layer/issues/3191 + @SuppressWarnings("java:S5659") + // It is checking the signature securely - https://github.com/zowe/api-layer/issues/3191 public QueryResponse parseJwtWithSignature(String jwt) throws SignatureException { try { Jwt parsedJwt = (Jwt) Jwts.parser() @@ -162,7 +150,7 @@ public QueryResponse parseJwtWithSignature(String jwt) throws SignatureException * Method will invalidate jwtToken. It could be called from two reasons: * - on logout phase (distribute = true) * - from another ZAAS instance to notify about change (distribute = false) - * + *

* Note: This method should not be called from modulith-mode * * @param jwtToken token to invalidate @@ -234,7 +222,7 @@ public Boolean invalidateJwtTokenGateway(String jwtToken, boolean distribute, Ap * Obtain URL to use to invalidate a JWT * * @param instanceInfo Registration data for the authentication service used - * @param jwtToken JWT token to invalidate + * @param jwtToken JWT token to invalidate * @return */ protected String getInvalidateUrl(InstanceInfo instanceInfo, String jwtToken) { @@ -306,19 +294,14 @@ private Claims validateAndParseLocalJwtToken(String jwtToken) { @Cacheable(value = CACHE_VALIDATION_JWT_TOKEN, key = "#jwtToken", condition = "#jwtToken != null") public TokenAuthentication validateJwtToken(String jwtToken) { QueryResponse queryResponse = parseJwtToken(jwtToken); - boolean isValid; - switch (queryResponse.getSource()) { - case ZOWE: + boolean isValid = switch (queryResponse.getSource()) { + case ZOWE -> { validateAndParseLocalJwtToken(jwtToken); - isValid = true; - break; - case ZOSMF: - isValid = zosmfService.validate(jwtToken); - break; - default: - throw new TokenNotValidException("Unknown token type."); - } - + yield true; + } + case ZOSMF -> zosmfService.validate(jwtToken); + default -> throw new TokenNotValidException("Unknown token type."); + }; TokenAuthentication tokenAuthentication = new TokenAuthentication(queryResponse.getUserId(), jwtToken, TokenAuthentication.Type.JWT); // without a proxy cache aspect is not working, thus it is necessary get bean from application context final boolean authenticated = !meAsProxy.isInvalidated(jwtToken); @@ -348,7 +331,7 @@ public TokenAuthentication createTokenAuthentication(String user, String jwtToke * This method get all invalidated JWT token in the cache and distributes them to instance of ZAAS with name * in argument toInstanceId. If instance cannot be find it return false. A notification can throw an runtime * exception. In all other cases all invalidated token are distributed and method returns true. - * + *

* Node: This method should not be used in modulith-mode * * @param toInstanceId instanceId of ZAAS where invalidated JWT token should be sent @@ -419,6 +402,7 @@ public QueryResponse parseQueryResponse(Claims claims) { /** * This method resolves the token origin directly by decoding token claims. + * * @param jwtToken the JWT token * @return AuthSource.Origin value based on the iss token claim. */ diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/dummy/DummyAuthenticationProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/dummy/DummyAuthenticationProviderTest.java index d07b39aec6..3ef46e95b5 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/dummy/DummyAuthenticationProviderTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/dummy/DummyAuthenticationProviderTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -38,7 +39,8 @@ static void setup() { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); UserDetailsService userDetailsService = new InMemoryUserDetailsService(encoder); AuthenticationService authenticationService = mock(AuthenticationService.class); - dummyAuthenticationProvider = new DummyAuthenticationProvider(encoder, userDetailsService, authenticationService); + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + dummyAuthenticationProvider = new DummyAuthenticationProvider(encoder, userDetailsService, authenticationService, publisher); } From 2add61e28cf07402534a80882ab5bd88216451b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Tue, 15 Jul 2025 10:30:38 +0200 Subject: [PATCH 016/152] chore: swagger-ui-react 5.22.0 upgrade (#4214) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/config-overrides.js | 4 + api-catalog-ui/frontend/package-lock.json | 534 +++++++++++--------- api-catalog-ui/frontend/package.json | 6 +- 3 files changed, 290 insertions(+), 254 deletions(-) diff --git a/api-catalog-ui/frontend/config-overrides.js b/api-catalog-ui/frontend/config-overrides.js index 0668055372..c6437a588f 100644 --- a/api-catalog-ui/frontend/config-overrides.js +++ b/api-catalog-ui/frontend/config-overrides.js @@ -49,6 +49,10 @@ module.exports = { '/node_modules/@swagger-api/apidom-reference/src/resolve/strategies/openapi-3-1/index.cjs', '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1': '/node_modules/@swagger-api/apidom-reference/src/dereference/strategies/openapi-3-1/index.cjs', + '@swagger-api/apidom-json-pointer/modern': + '/node_modules/@swagger-api/apidom-json-pointer/src/index.cjs', + '@swaggerexpert/json-pointer/evaluate/realms/apidom': + '/node_modules/@swaggerexpert/json-pointer/cjs/evaluate/realms/apidom/index.cjs', 'cheerio/lib/utils': '/node_modules/cheerio', }; config.setupFiles = ['./jest.polyfills.js']; diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index 2f59ff9c00..9f300eee6b 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -55,7 +55,7 @@ "rxjs": "7.8.2", "sass": "1.89.0", "stream": "0.0.3", - "swagger-ui-react": "5.21.0", + "swagger-ui-react": "5.22.0", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -124,8 +124,8 @@ "yaml": "2.8.0" }, "engines": { - "node": "=20.19.2", - "npm": "=10.9.2" + "node": "=20.19.3", + "npm": "=10.9.3" } }, "node_modules/@adobe/css-tools": { @@ -2195,13 +2195,12 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.27.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz", - "integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.0.tgz", + "integrity": "sha512-nlIXnSqLcBij8K8TtkxbBJgfzfvi75V1pAKSM7dUXejGw12vJAqez74jZrHTsJ3Z+Aczc5Q/6JgNjKRMsVU44g==", "license": "MIT", "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" + "core-js-pure": "^3.43.0" }, "engines": { "node": ">=6.9.0" @@ -5929,13 +5928,13 @@ } }, "node_modules/@swagger-api/apidom-ast": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.30.tgz", - "integrity": "sha512-5Wj3zdt0dxS9ERVk4qSuqDIsMQ8dP2vop8b494OpJ/O2W261yCV39Z+vN+PqeJ2NiKDRMlJ+QoQ1uVfKwEo8Kg==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.44.tgz", + "integrity": "sha512-+IOyUl0Gl125t3/ULi6Bc7HbvSMHqOSXmDqL9qAYrOxIaQVasHgIq+sye6g/3TsqAE8xZ7gtfsaA53UlclotFg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-error": "^1.0.0-beta.30", + "@swagger-api/apidom-error": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -5943,56 +5942,54 @@ } }, "node_modules/@swagger-api/apidom-core": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.30.tgz", - "integrity": "sha512-pDnUhXIKKUvmeezQfwKLL05rkOH1L7ueiy5ja5ob9y2w4r+HXDID7qHtDGeRxKZoIt4E3Sd1K37OjcE9fNcknQ==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.44.tgz", + "integrity": "sha512-6TXqyO/aJv1QgY/iTiNVI0ECedKIMIOM5KlblWZfm1uy4djNy1FA8Z3x49WESNlYRklEiYHZ/3HVthoLa6hSOA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-ast": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", + "@swagger-api/apidom-ast": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "minim": "~0.23.8", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", - "short-unique-id": "^5.0.2", + "short-unique-id": "^5.3.2", "ts-mixer": "^6.0.3" } }, "node_modules/@swagger-api/apidom-error": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.30.tgz", - "integrity": "sha512-hVDx0kUF1DTyaEXwmsF3wpJClEfnH0pxjEubqtvHpjjeTMgZzmKc5azbYtvgBX3uUpGHyQZyG/O9g94/wIhhMA==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.44.tgz", + "integrity": "sha512-GwLzYvDsXPmhJYKibS86RS6Kkdj7h2bFrTuqQ2f9FV9mdgG7EOh3yC6tNdd0Q/WvcFzd5i+APNX0PrYg/kfLiw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.20.7" } }, "node_modules/@swagger-api/apidom-json-pointer": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.30.tgz", - "integrity": "sha512-G+BDNXU/ARJCbJiFq1A6dh6pNDDp1J0jPfKeIHjsD8aZoRdpJC0F3F7onm8TjQm2cnvAi4B7vPOKzjWrYN1VWw==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.44.tgz", + "integrity": "sha512-2/E3+LFuqnTUay14+Qv3doXzPZMrS/Ed638Gd8byB4XNUY/0zQYwHTEca5QOxadNFh86zG+DnQQl+DC3ARkmTg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@types/ramda": "~0.30.0", - "ramda": "~0.30.0", - "ramda-adjunct": "^5.0.0" + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swaggerexpert/json-pointer": "^2.10.1" } }, "node_modules/@swagger-api/apidom-ns-api-design-systems": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-1.0.0-beta.30.tgz", - "integrity": "sha512-YsFtttsq39qVU2J9lMD3i+aeuiMD8EjeageszDEePYgb4/k2PZX9YJqb9urwxydBM7BFG7H/r9K/dVUMHFV5hw==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-1.0.0-beta.44.tgz", + "integrity": "sha512-4lz7pIzD0J1X2/pSsYYxu3pH2xDXVpxvjZJDEB81LElxq5IOuGvtKK38Na0tM9pv3oYyJTWbUdT3Ili9O7URyQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6000,15 +5997,15 @@ } }, "node_modules/@swagger-api/apidom-ns-arazzo-1": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-arazzo-1/-/apidom-ns-arazzo-1-1.0.0-beta.30.tgz", - "integrity": "sha512-HpszcpuDlSOXWruHzasR64L8640VHVDuy8xXJrhx1iBu+gDHriOM8gbh8jQgWST91H0smtPeTG9WV1/h6frhRw==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-arazzo-1/-/apidom-ns-arazzo-1-1.0.0-beta.44.tgz", + "integrity": "sha512-Ui0eTJf1mX+MTuxvBWDl7RQ7jtZhPRBT1OfqMYCOneKbajZtka+U9fNUWgRwIuieRn3bHr3T5+s3jRK5AKZ7nQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6016,15 +6013,15 @@ } }, "node_modules/@swagger-api/apidom-ns-asyncapi-2": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-1.0.0-beta.30.tgz", - "integrity": "sha512-/DvnCZY2cVz8E79Nc5mXD8J0++D8QT/c1PKPMMGEGVwGWB6XLh8jZM0HERb6yAiLUC0qzv4Jau/iQH1gs/ZtiQ==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-1.0.0-beta.44.tgz", + "integrity": "sha512-RGA1348E0gfSCpaD7nwYW2377ArDPyhnkzckeLafBYwhCDjOtjZTbJZ5ljnvAQmpv37pmy+ev/uIFUCpyIJ0qQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6032,15 +6029,15 @@ } }, "node_modules/@swagger-api/apidom-ns-json-schema-2019-09": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-2019-09/-/apidom-ns-json-schema-2019-09-1.0.0-beta.30.tgz", - "integrity": "sha512-HZL76SJaUDmL1GuFcev23UX1vVuxSHIED3vvKso+k3KWNfVWZJrr7GX1ELJx84fWW8g3b5S5+nyz5q1ApT084A==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-2019-09/-/apidom-ns-json-schema-2019-09-1.0.0-beta.44.tgz", + "integrity": "sha512-wPRDhIueKa20jZ1zvu//I48p9AzbJdC/KHsicJoRW7y0HgIZpG0AIB6fc1N0p/8NMibOq0JEIdIVYYvhmW9pAg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6048,15 +6045,15 @@ } }, "node_modules/@swagger-api/apidom-ns-json-schema-2020-12": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.30.tgz", - "integrity": "sha512-D2adAcu/ISoBe0zRbcX0HyaDvWoMhmaL8iPR4pvjLY7soB2tCR4uLEzAkqPa2zaOKBRA2ziF74aNKrKbM5sX8w==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.44.tgz", + "integrity": "sha512-QwE0gnDO8GfGEpZdWDjFAkoBH+6atPk/oeX/OFjFOOkwMJXSZiIQbx69xefS1XEMSg40aOHKIjpeSTzErirEfg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-2019-09": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-2019-09": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6064,14 +6061,14 @@ } }, "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.30.tgz", - "integrity": "sha512-u5YMIw/g74Z59wPBFS2A2LaheC+EEqRcbpUQOApTvb6zjW+xWxbCuKV1ypzIaVDDPIry8e3mpwjjXLj1mvad5w==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.44.tgz", + "integrity": "sha512-p392XjvmJ9NTxIzQ5/l33rzOm4hx4XDRWe+SwuciTg4NsuNQupTq3zrvXagCVepzYBlSg/1k0SYCm7yVRR+VJw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-ast": "^1.0.0-beta.30", - "@swagger-api/apidom-core": "^1.0.0-beta.30", + "@swagger-api/apidom-ast": "^1.0.0-beta.44", + "@swagger-api/apidom-core": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6079,15 +6076,15 @@ } }, "node_modules/@swagger-api/apidom-ns-json-schema-draft-6": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.30.tgz", - "integrity": "sha512-/Mp11+tBKTN6XnpOiQo/cKnqmvfJhdCniHCK6Bg8wpCI3dMi+nSSpIYgWEPVQfNsLtf/PaYegrtYY56W4UzNRw==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.44.tgz", + "integrity": "sha512-aodklkUWZ774je8VMDge/OFpDNeefZc+5jKymtMip1uYbVLmO7ByvkqSR665xXqOA8VMEzsG+q59L+4UX3jObA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6095,15 +6092,15 @@ } }, "node_modules/@swagger-api/apidom-ns-json-schema-draft-7": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.30.tgz", - "integrity": "sha512-6sZ0LLYnEz9KXtt9xTRSc0EORBl5Fj3LUbfabUjqLQZGldsJWU+3TTQ4XtzFFHlan7z2WYyALKP7iP+b60XbPg==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.44.tgz", + "integrity": "sha512-CbPRkDusgxBBF4MuBz8T88CYmqnqOUd3VqbTHU948Q2KqvcleI2W+HiTgJfBew0XkEiIwbWwRPdfBuuWZwVtyg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-draft-6": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-draft-6": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6111,16 +6108,16 @@ } }, "node_modules/@swagger-api/apidom-ns-openapi-2": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.0.0-beta.30.tgz", - "integrity": "sha512-nloJUjf6AtKRnBuWmaFkVk7lR7aht9cudXkR/W0ui+feLSJ5rnYy6nyLyGFLZqLnb2cSV8L6bB6tGPJnvc5KzA==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.0.0-beta.44.tgz", + "integrity": "sha512-T5PSrtqFcUpIPKJq7Z0aAQthxm4S5F5Ke5Nuk9UDfvD6FHDsk8nRc+GSleSt/Je4X0aqc/G2ZsztH1b5bWZ06g==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6128,15 +6125,15 @@ } }, "node_modules/@swagger-api/apidom-ns-openapi-3-0": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.0.0-beta.30.tgz", - "integrity": "sha512-7bz6kCgjStTKGGI4wBP2ho574lyfjH5EDPPuXhkwmAG2mOn9MZezlQhsbdo3B+vbi/58mqQb2XCoB4aeP1F+GQ==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.0.0-beta.44.tgz", + "integrity": "sha512-2S3mymU47rWVBxJipA1qseVXE11Q4N3gKu/joxw40V6qB1UhCTzl4IjJaYhGKE+eGkJ3i6CCocHHlNlnTX0Pcg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6144,17 +6141,17 @@ } }, "node_modules/@swagger-api/apidom-ns-openapi-3-1": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.30.tgz", - "integrity": "sha512-pq2jxSp0I6xnGzyAiEXWYMuurp8H7TlOQ6Ijr/XX54gNmaIK+yQ3HXc7S6FZx+B2kQx03Tb8Y8O7L7J7YnmFiA==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.44.tgz", + "integrity": "sha512-pSxoB/xZq1B7vjt2kC8DObpaMZgZ4qdLplVJFZgh23ohnU6D/ubaJhe04kS0vxVMpwrcEjYU44t7paMLEUr1nA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-ast": "^1.0.0-beta.30", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-json-pointer": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.30", + "@swagger-api/apidom-ast": "^1.0.0-beta.44", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-json-pointer": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -6162,278 +6159,300 @@ } }, "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-1.0.0-beta.30.tgz", - "integrity": "sha512-ER5kQtxOXG8W1cQC7xH8EYYUOAMaqVrECIZShoa6yOLoI0/a40xFF5Lansn2P9szR1hT/2neM8KLcjaxCFjXSQ==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-1.0.0-beta.44.tgz", + "integrity": "sha512-mhPA7qn0NXh84Vw2hwtK+9sOplV3bf9HJiZ1M3YyjTnhXGVf3lBNvi2f2OzLPdiYwNTG0p3N43f3v+SQzLVbKg==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-1.0.0-beta.30.tgz", - "integrity": "sha512-Xghcidv1TJVwrb/jFHQZA5YHPm+LxNPpFjOJYrijugXK72D3a5fqc/2PZzkGXeYefE4lGM+YB83c08N6NDCa4w==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-1.0.0-beta.44.tgz", + "integrity": "sha512-So6M95X5G3PQ4AboP6HzA30c7XdpYXDognslELAvZpl8OEG8nVaKyqn7+lRsK2pG6oBSYBaqEialZNZly7Np5Q==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-arazzo-json-1": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-arazzo-json-1/-/apidom-parser-adapter-arazzo-json-1-1.0.0-beta.30.tgz", - "integrity": "sha512-SZajkrTJ7c1I9CI3gnsdHZCQFSIyQ2H/lkWDjA/drZkRcfbR1CTbR2q0BGGlV5Y+nFHBxjRNpPbYbZrqh0WV4w==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-arazzo-json-1/-/apidom-parser-adapter-arazzo-json-1-1.0.0-beta.44.tgz", + "integrity": "sha512-avgDrkdSpbAzEb6++qdEqckO7Yyh538FBnk1A9+OfkYSj7k83yx5azngAmr66tBG3oSSCELTmEpXM+oyBGHH/Q==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-arazzo-yaml-1": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-arazzo-yaml-1/-/apidom-parser-adapter-arazzo-yaml-1-1.0.0-beta.30.tgz", - "integrity": "sha512-T+N1ix+V5IpOWMFcamQRI50830JayD1gifnRm+mVeWJKMzp+xm08bnO8NiR9LQ2SKJZ6FWYM38oG2tAt0Lwxcg==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-arazzo-yaml-1/-/apidom-parser-adapter-arazzo-yaml-1-1.0.0-beta.44.tgz", + "integrity": "sha512-kUxujeqr0Av8IY1T5xxZXaj/0tV/dQGqCDv9cLcnivKEp2gnCFxwkdKMlRCosMpPOb+ApOcUYtgGYM/RnkyGbg==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-1.0.0-beta.30.tgz", - "integrity": "sha512-KjyF966T9HVvSsk+RWaOcNDxXBqOWr/09SAw1OdBBfGHqs+xF3KOV7/2RB88Adw3+ZZ3E5oXDvVVhobq8wVvyA==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-1.0.0-beta.44.tgz", + "integrity": "sha512-Q+qc+tTucc5Ol1ikeEVbt9DasnlXhM8MtY6Ej6ylDS+eSPzZMw09vU2V6Tk/QWSoSDHSqZMHt9G12SHI78LrbA==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-1.0.0-beta.30.tgz", - "integrity": "sha512-+6zlRD0nP7T5Yiu9hHgP3b7d016WYRXqfr9TW/yqPFInM/tI74ROPJnMQ1G3s0HyW6lB0KX7cG0O0TqcMmnSqg==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-1.0.0-beta.44.tgz", + "integrity": "sha512-E4PVQLIWI3QG7UVHL23Qf4GUQCzTyRfiiMHt34gySTtFIE9OV48hIax3J85dF2DMR1CXtzj86zQTacYYe84EUQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-json": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-1.0.0-beta.30.tgz", - "integrity": "sha512-cciT19OOXafwBnXe9KFVwUGEVu4Zrvb4k12TYNlNqzVg1xA9pBc3Ywq5EgHIhiiQOLY3fILr0fr6B36N6irN2Q==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-1.0.0-beta.44.tgz", + "integrity": "sha512-KeMu0tHLXNNJaZkR0blzHl3KvCfkde414h9FMosBH+zh4KLA0wmly1Mn+9tzvAbBa7vqxKmHg1o/7vDU2ZGA3Q==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-ast": "^1.0.0-beta.30", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", + "@swagger-api/apidom-ast": "^1.0.0-beta.44", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", - "tree-sitter": "=0.22.1", + "tree-sitter": "=0.21.1", "tree-sitter-json": "=0.24.8", "web-tree-sitter": "=0.24.5" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-2": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-1.0.0-beta.30.tgz", - "integrity": "sha512-Q5b9XVTId/FiGSmGKSOxyKJZYdvWcZOqogpLkF0Q8PtPVCgp2LFl73XuJOgjxO1nkE+n/ap+93svgaaxQRaVow==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-1.0.0-beta.44.tgz", + "integrity": "sha512-F3Um4/raAob3M/CLOCrl1qrOb/Z+E1elMGpGupyk3uqgj0xQ0otlwzJS+2gz0LAJHofzq2WLZGDRsKlPaepTtQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-1.0.0-beta.30.tgz", - "integrity": "sha512-VsDpKXmRl6sXpgR6o582yyDJqfFfliYVrVWve0DCOTkpvOeOYqPPLA45oMMvunJkqVsBL4Fpy9/ZqAQvdlur7g==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-1.0.0-beta.44.tgz", + "integrity": "sha512-Jw0BxctNKmDifDh5Jo3exhp9VtWz/OZR8qqCL1y65ySvIr4MkCkvEmGTvfdXGZrxwhPVm5QDmJ5/X0MKHi/ZDQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-1.0.0-beta.30.tgz", - "integrity": "sha512-Q2NQ1/IF500mFuZZDC3tTw75UOTgSknqRyBywsA159BRnqnWxwk/2//Ifh8Vwq/mMyW2zSChigCvnqI+/IvQxA==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-1.0.0-beta.44.tgz", + "integrity": "sha512-NWeHqHBr/XpQkDnomMCzENujzYx/zE9Jo7NgzPhDMH3kgS/3snwrgNRFRDMCazwDqdjWfhivHdE5biqsOGEe7A==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-2": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-1.0.0-beta.30.tgz", - "integrity": "sha512-6Zj1UtbQIwnsVJi2xn+Zl9yn9U014XzkX6QKrpAXIUGNCcjwWIbuOKd3u2T481OOP0BuVf3JpWhRqxumtosV3w==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-1.0.0-beta.44.tgz", + "integrity": "sha512-TfgItza/SrM5YsLhEdl75HHGwlnVKVveWh2miF1MTsI9K3k3I4Zv0nUnKNlH+R9TY6DXUr7c52c4Jf672VyLxQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-1.0.0-beta.30.tgz", - "integrity": "sha512-YaGDkZaV9ZRtbIGorsyyqL2x323gLMqqgLrPpAjaBbBFiAJRwF/gwRHMY4iJ85H2YeUxUq0jqtSc3jH3wsQJGg==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-1.0.0-beta.44.tgz", + "integrity": "sha512-g0+N/XpJTMioHfXGh5roTXPZO6AounZCpMfe2GpfdHLRN4hpxzffd+xZLVMmcaFt1x3XYAhBFCxNXIumg5Pw5A==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-1.0.0-beta.30.tgz", - "integrity": "sha512-rBa7daaUrDVAIwJZm+S4lwc5pqNt6avNTGxEB69dNZ3QDJmCC+HUnudUtsG3VqMfP46JITKUPvtzRLGjX8CgRg==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-1.0.0-beta.44.tgz", + "integrity": "sha512-cddGQ9XUafxv6ERO53JOxryZL968TwjNfsc/9NB9KYHTv32sk73cncIh5KfmGd7SH5UExmGv4YD93n7r2i7ePw==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.30", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.44", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-1.0.0-beta.30.tgz", - "integrity": "sha512-NRmQehyw4gbDzeBAl0zjyPqj4e/jNYgqnRLcOsxTKpWODud8RHBqEvju/M6iET6ru0o+A9265efFzqR9hiE0LA==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-1.0.0-beta.44.tgz", + "integrity": "sha512-2UUKNaI9CESIN1f8skTzVHTVujuJyK5T0SE6THQ8wcihPhyoRaCUU4kcSSx22IxPIABFQ21+RYZ0sM3D5dOZ2Q==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-ast": "^1.0.0-beta.30", - "@swagger-api/apidom-core": "^1.0.0-beta.30", - "@swagger-api/apidom-error": "^1.0.0-beta.30", - "@tree-sitter-grammars/tree-sitter-yaml": "=0.7.0", + "@swagger-api/apidom-ast": "^1.0.0-beta.44", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", + "@tree-sitter-grammars/tree-sitter-yaml": "=0.7.1", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", - "tree-sitter": "=0.22.1", + "tree-sitter": "=0.22.4", "web-tree-sitter": "=0.24.5" } }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { + "version": "0.22.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, "node_modules/@swagger-api/apidom-reference": { - "version": "1.0.0-beta.30", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.30.tgz", - "integrity": "sha512-l1MpLMlmaX+y2hra5EadfR37sAMzmEz1wZomVcnw7vJEFlLQo3WwOdFvpQemPCZ9IJHUs+5zhZ++w7z60uKpSw==", + "version": "1.0.0-beta.44", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.44.tgz", + "integrity": "sha512-qXaL0H8ehmMX7NLYKSrIVl1ayE5e2jsvZ3ApcksmHYWUKsbuDTfKB7pKR78VporRv+SRXiq2WGeeYY1bCuCHKA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.26.10", - "@swagger-api/apidom-core": "^1.0.0-beta.30", + "@swagger-api/apidom-core": "^1.0.0-beta.44", + "@swagger-api/apidom-error": "^1.0.0-beta.44", "@types/ramda": "~0.30.0", - "axios": "^1.8.2", + "axios": "^1.9.0", "minimatch": "^7.4.3", "process": "^0.11.10", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" }, "optionalDependencies": { - "@swagger-api/apidom-error": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-json-pointer": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-arazzo-json-1": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-openapi-json-2": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^1.0.0-beta.3 <1.0.0-rc.0", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.3 <1.0.0-rc.0" + "@swagger-api/apidom-json-pointer": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-arazzo-json-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.40 <1.0.0-rc.0" } }, "node_modules/@swagger-api/apidom-reference/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6466,6 +6485,18 @@ "node": ">=12.20.0" } }, + "node_modules/@swaggerexpert/json-pointer": { + "version": "2.10.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swaggerexpert/json-pointer/-/json-pointer-2.10.2.tgz", + "integrity": "sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.13.6", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", @@ -6608,18 +6639,18 @@ } }, "node_modules/@tree-sitter-grammars/tree-sitter-yaml": { - "version": "0.7.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@tree-sitter-grammars/tree-sitter-yaml/-/tree-sitter-yaml-0.7.0.tgz", - "integrity": "sha512-GOMIK3IaDvECD0eZEhAsLl03RMtM1E8StxuGMn6PpMKFg7jyQ+jSzxJZ4Jmc/tYitah9/AECt8o4tlRQ5yEZQg==", + "version": "0.7.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@tree-sitter-grammars/tree-sitter-yaml/-/tree-sitter-yaml-0.7.1.tgz", + "integrity": "sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "node-addon-api": "^8.3.0", + "node-addon-api": "^8.3.1", "node-gyp-build": "^4.8.4" }, "peerDependencies": { - "tree-sitter": "^0.22.1" + "tree-sitter": "^0.22.4" }, "peerDependenciesMeta": { "tree-sitter": { @@ -6628,9 +6659,9 @@ } }, "node_modules/@tree-sitter-grammars/tree-sitter-yaml/node_modules/node-addon-api": { - "version": "8.3.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-addon-api/-/node-addon-api-8.3.1.tgz", - "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "version": "8.5.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", "optional": true, "engines": { @@ -7918,9 +7949,9 @@ } }, "node_modules/apg-lite": { - "version": "1.0.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/apg-lite/-/apg-lite-1.0.4.tgz", - "integrity": "sha512-B32zCN3IdHIc99Vy7V9BaYTUzLeRA8YXYY1aQD1/5I2aqIrO0coi4t6hJPqMisidlBxhyME8UexkHt31SlR6Og==", + "version": "1.0.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/apg-lite/-/apg-lite-1.0.5.tgz", + "integrity": "sha512-SlI+nLMQDzCZfS39ihzjGp3JNBQfJXyMi6cg9tkLOCPVErgFsUIAEdO9IezR7kbP5Xd0ozcPNQBkf9TO5cHgWw==", "license": "BSD-2-Clause" }, "node_modules/arch": { @@ -10191,9 +10222,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/core-js-pure/-/core-js-pure-3.41.0.tgz", - "integrity": "sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==", + "version": "3.44.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/core-js-pure/-/core-js-pure-3.44.0.tgz", + "integrity": "sha512-gvMQAGB4dfVUxpYD0k3Fq8J+n5bB6Ytl15lqlZrOIXFzxOhtPaObfkQGHtMRdyjIf7z2IeNULwi1jEwyS+ltKQ==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -20529,6 +20560,7 @@ "version": "1.0.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -27828,9 +27860,9 @@ } }, "node_modules/short-unique-id": { - "version": "5.2.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/short-unique-id/-/short-unique-id-5.2.2.tgz", - "integrity": "sha512-MlRVyT5RYfDO2kUzBgOPlZriRzG+NIAuwSy1HBN8tahXyFi3+804GGi/mzjUsi6VxgiQuDgMnhoI2FqmSHX8Tg==", + "version": "5.3.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/short-unique-id/-/short-unique-id-5.3.2.tgz", + "integrity": "sha512-KRT/hufMSxXKEDSQujfVE0Faa/kZ51ihUcZQAcmP04t00DvPj7Ox5anHke1sJYUtzSuiT/Y5uyzg/W7bBEGhCg==", "license": "Apache-2.0", "bin": { "short-unique-id": "bin/short-unique-id", @@ -29509,18 +29541,18 @@ } }, "node_modules/swagger-client": { - "version": "3.34.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-client/-/swagger-client-3.34.4.tgz", - "integrity": "sha512-Qvtu8DtARAx5GwefA0eV1WRLa4Q9bhczrtNAsiBMOx3HkxAOczy1APQhrcblJdLys0xEGQ4xYizYFXfIL9BhpA==", + "version": "3.35.6", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-client/-/swagger-client-3.35.6.tgz", + "integrity": "sha512-OgwNneIdC45KXwOfwrlkwgWPeAKiV4K75mOnZioTddo1mpp9dTboCDVJas7185Ww1ziBwzShBqXpNGmyha9ZQg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.22.15", "@scarf/scarf": "=1.4.0", - "@swagger-api/apidom-core": ">=1.0.0-beta.13 <1.0.0-rc.0", - "@swagger-api/apidom-error": ">=1.0.0-beta.13 <1.0.0-rc.0", - "@swagger-api/apidom-json-pointer": ">=1.0.0-beta.13 <1.0.0-rc.0", - "@swagger-api/apidom-ns-openapi-3-1": ">=1.0.0-beta.13 <1.0.0-rc.0", - "@swagger-api/apidom-reference": ">=1.0.0-beta.13 <1.0.0-rc.0", + "@swagger-api/apidom-core": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-error": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-json-pointer": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-reference": ">=1.0.0-beta.41 <1.0.0-rc.0", "@swaggerexpert/cookie": "^2.0.2", "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", @@ -29535,12 +29567,12 @@ } }, "node_modules/swagger-ui-react": { - "version": "5.21.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.21.0.tgz", - "integrity": "sha512-lS5paITM1kkcBb/BTTSMHKelh8elHfcuUP4T3R3mO80tDR0AYJL2HR5UdQD6nV1LwdvekzRM8gKjJA6hVayi0A==", + "version": "5.22.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.22.0.tgz", + "integrity": "sha512-Y0TEWg2qD4u/dgZ9q9G16yM/Edvyz0ovkIZlpACN8X/2gzSoIzS/fhSpLSJfCOxRt2UqrKmajMB11VK6cGZk2g==", "license": "Apache-2.0", "dependencies": { - "@babel/runtime-corejs3": "^7.26.10", + "@babel/runtime-corejs3": "^7.27.1", "@scarf/scarf": "=1.4.0", "base64-js": "^1.5.1", "classnames": "^2.5.1", @@ -29568,7 +29600,7 @@ "reselect": "^5.1.1", "serialize-error": "^8.1.0", "sha.js": "^2.4.11", - "swagger-client": "^3.34.4", + "swagger-client": "^3.35.3", "url-parse": "^1.5.10", "xml": "=1.0.1", "xml-but-prettier": "^1.0.1", @@ -30157,15 +30189,15 @@ } }, "node_modules/tree-sitter": { - "version": "0.22.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/tree-sitter/-/tree-sitter-0.22.1.tgz", - "integrity": "sha512-gRO+jk2ljxZlIn20QRskIvpLCMtzuLl5T0BY6L9uvPYD17uUrxlxWkvYCiVqED2q2q7CVtY52Uex4WcYo2FEXw==", + "version": "0.21.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "node-addon-api": "^8.2.1", - "node-gyp-build": "^4.8.2" + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" } }, "node_modules/tree-sitter-json": { @@ -30189,9 +30221,9 @@ } }, "node_modules/tree-sitter-json/node_modules/node-addon-api": { - "version": "8.3.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-addon-api/-/node-addon-api-8.3.1.tgz", - "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "version": "8.5.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", "optional": true, "engines": { @@ -30199,9 +30231,9 @@ } }, "node_modules/tree-sitter/node_modules/node-addon-api": { - "version": "8.3.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-addon-api/-/node-addon-api-8.3.1.tgz", - "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "version": "8.5.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", "optional": true, "engines": { diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 0d50216971..ae9ad7c593 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -51,7 +51,7 @@ "rxjs": "7.8.2", "sass": "1.89.0", "stream": "0.0.3", - "swagger-ui-react": "5.21.0", + "swagger-ui-react": "5.22.0", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -164,8 +164,8 @@ } }, "engines": { - "npm": "=10.9.2", - "node": "=20.19.2" + "npm": "=10.9.3", + "node": "=20.19.3" }, "browserslist": [ ">0.2%", From e084bfe05740939b49302164284007ab6dc4fff4 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:06:24 +0200 Subject: [PATCH 017/152] chore: Update all non-major dependencies (v3.x.x) (#4213) Signed-off-by: Renovate Bot Co-authored-by: Renovate Bot Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 671 ++++++++++-------- api-catalog-ui/frontend/package.json | 40 +- gradle/versions.gradle | 4 +- .../package-lock.json | 119 ++-- zowe-cli-id-federation-plugin/package.json | 6 +- 5 files changed, 462 insertions(+), 378 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index 9f300eee6b..bc1316ba2c 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -11,8 +11,8 @@ "@emotion/core": "11.0.0", "@emotion/is-prop-valid": "1.3.1", "@emotion/react": "11.14.0", - "@emotion/styled": "11.14.0", - "@eslint/migrate-config": "1.5.0", + "@emotion/styled": "11.14.1", + "@eslint/migrate-config": "1.5.2", "@jest/globals": "29.7.0", "@material-ui/core": "4.12.4", "@material-ui/icons": "4.11.3", @@ -21,7 +21,7 @@ "@react-loadable/revised": "1.5.0", "@types/enzyme": "3.10.19", "@types/jest": "29.5.14", - "@types/react": "18.3.22", + "@types/react": "18.3.23", "agentkeepalive": "4.6.0", "buffer": "6.0.3", "emotion-theming": "11.0.0", @@ -41,9 +41,9 @@ "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", "react-error-boundary": "5.0.0", - "react-hook-form": "7.56.4", + "react-hook-form": "7.60.0", "react-redux": "9.2.0", - "react-router": "7.6.0", + "react-router": "7.6.3", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -53,22 +53,22 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.89.0", + "sass": "1.89.2", "stream": "0.0.3", - "swagger-ui-react": "5.22.0", + "swagger-ui-react": "5.26.2", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" }, "devDependencies": { - "@babel/core": "7.27.1", - "@babel/eslint-parser": "7.27.1", + "@babel/core": "7.28.0", + "@babel/eslint-parser": "7.28.0", "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "@babel/preset-env": "7.27.2", + "@babel/preset-env": "7.28.0", "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", - "@eslint/compat": "1.2.9", - "@eslint/js": "9.27.0", + "@eslint/compat": "1.3.1", + "@eslint/js": "9.31.0", "@reduxjs/toolkit": "2.8.2", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", @@ -77,22 +77,22 @@ "ajv": "8.17.1", "ansi-regex": "6.1.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001718", - "concurrently": "9.1.2", + "caniuse-lite": "1.0.30001727", + "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.27.0", + "eslint": "9.31.0", "eslint-config-airbnb": "19.0.4", "eslint-config-prettier": "9.1.0", "eslint-plugin-cypress": "4.3.0", "eslint-plugin-flowtype": "8.0.3", "eslint-plugin-header": "3.1.1", - "eslint-plugin-import": "2.31.0", + "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-prettier": "5.4.0", + "eslint-plugin-prettier": "5.5.1", "eslint-plugin-react": "7.37.5", "express": "4.21.2", "globals": "15.15.0", @@ -109,7 +109,7 @@ "mini-css-extract-plugin": "2.9.2", "nodemon": "3.1.10", "nth-check": "2.1.1", - "prettier": "3.5.3", + "prettier": "3.6.2", "prop-types": "15.8.1", "querystring-es3": "0.2.1", "react-app-rewired": "2.2.1", @@ -194,30 +194,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -233,9 +233,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/eslint-parser/-/eslint-parser-7.27.1.tgz", - "integrity": "sha512-q8rjOuadH0V6Zo4XLMkJ3RMQ9MSBqwaDByyYB0izsYdaIWGNLmEblbCOf1vyFHICcg16CD7Fsi51vcQnYxmt6Q==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", + "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", "dev": true, "license": "MIT", "dependencies": { @@ -252,29 +252,39 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", - "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "version": "7.27.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -337,22 +347,31 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", - "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "version": "0.6.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -381,14 +400,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.27.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -512,25 +531,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.27.6", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1056,15 +1075,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", - "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1108,9 +1127,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", - "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", + "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1158,18 +1177,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", + "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.27.1", - "globals": "^11.1.0" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1178,16 +1197,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.27.1", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", @@ -1206,13 +1215,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", - "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1287,6 +1297,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", @@ -1571,16 +1598,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.27.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", - "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", + "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.1" + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1640,9 +1668,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "version": "7.27.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1792,9 +1820,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", - "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", + "version": "7.28.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", + "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -2030,13 +2058,13 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.27.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/preset-env/-/preset-env-7.27.2.tgz", - "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/preset-env/-/preset-env-7.28.0.tgz", + "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", @@ -2050,19 +2078,20 @@ "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-classes": "^7.28.0", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", @@ -2079,15 +2108,15 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.0", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -2100,10 +2129,10 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "engines": { @@ -2126,6 +2155,20 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -2207,13 +2250,13 @@ } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -2221,36 +2264,27 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.28.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2802,9 +2836,9 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.14.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@emotion/styled/-/styled-11.14.0.tgz", - "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "version": "11.14.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -2894,15 +2928,15 @@ } }, "node_modules/@eslint/compat": { - "version": "1.2.9", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/compat/-/compat-1.2.9.tgz", - "integrity": "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==", + "version": "1.3.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/compat/-/compat-1.3.1.tgz", + "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^9.10.0" + "eslint": "^8.40 || 9" }, "peerDependenciesMeta": { "eslint": { @@ -2911,9 +2945,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2926,9 +2960,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3005,10 +3039,11 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.31.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3017,12 +3052,12 @@ } }, "node_modules/@eslint/migrate-config": { - "version": "1.5.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/migrate-config/-/migrate-config-1.5.0.tgz", - "integrity": "sha512-XFLv8jczLFeAclYDLaidv9Eo4ykO/F/TMMGbUgdDtrgAA3iGnEI1xvDTaWZ6jRZi0YFtOpQBujZUYKBmksSnjQ==", + "version": "1.5.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/migrate-config/-/migrate-config-1.5.2.tgz", + "integrity": "sha512-OOh3O5ncnW/ZNZEVGNQNKI0ElDW29HmEcAAsgl+skEMw1b+VN0YVsUJjbPNzJDBnarq3jFeVQq6nPI+9jHFnZw==", "license": "Apache-2.0", "dependencies": { - "@eslint/compat": "^1.2.9", + "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.1.0", "camelcase": "^8.0.0", "espree": "^10.3.0", @@ -3847,9 +3882,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -4774,16 +4809,16 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@pkgr/core/-/core-0.2.1.tgz", - "integrity": "sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg==", + "version": "0.2.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { @@ -7075,9 +7110,10 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.22", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/react/-/react-18.3.22.tgz", - "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", + "version": "18.3.23", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -7683,9 +7719,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -8065,18 +8101,20 @@ "license": "MIT" }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8470,9 +8508,9 @@ } }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.10.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -8681,14 +8719,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.13", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", - "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "version": "0.4.14", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.4", + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { @@ -8710,13 +8748,13 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", - "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "version": "0.6.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.4" + "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -9038,9 +9076,9 @@ "license": "BSD-2-Clause" }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "funding": [ { "type": "opencollective", @@ -9057,10 +9095,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -9348,9 +9386,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001727", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "funding": [ { "type": "opencollective", @@ -10073,9 +10111,9 @@ "license": "MIT" }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "version": "9.2.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10208,13 +10246,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/core-js-compat/-/core-js-compat-3.41.0.tgz", - "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", + "version": "3.44.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.1" }, "funding": { "type": "opencollective", @@ -11673,9 +11711,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.134", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/electron-to-chromium/-/electron-to-chromium-1.5.134.tgz", - "integrity": "sha512-zSwzrLg3jNP3bwsLqWHmS5z2nIOQ5ngMnfMZOWWtXnqqQkPVyOipxK98w+1beLw1TB+EImPNcG8wVP/cLVs2Og==", + "version": "1.5.183", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/electron-to-chromium/-/electron-to-chromium-1.5.183.tgz", + "integrity": "sha512-vCrDBYjQCAEefWGjlK3EpoSKfKbT10pR4XXPdn65q7snuNOZnthoVpBfZPykmDapOKfoD+MMIPG8ZjKyyc9oHA==", "license": "ISC" }, "node_modules/emittery": { @@ -11908,9 +11946,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -11918,18 +11956,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -11941,21 +11979,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -11964,7 +12005,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -12153,18 +12194,19 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.31.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -12176,9 +12218,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -12319,9 +12361,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -12389,30 +12431,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -12498,14 +12540,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.4.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", - "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", + "version": "5.5.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -12699,6 +12741,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/ajv": { "version": "6.12.6", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ajv/-/ajv-6.12.6.tgz", @@ -12717,9 +12772,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -12734,9 +12789,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -12754,14 +12809,14 @@ "license": "MIT" }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12771,9 +12826,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -16131,6 +16186,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/is-number/-/is-number-7.0.0.tgz", @@ -23251,9 +23319,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -23847,9 +23915,10 @@ "license": "MIT" }, "node_modules/react-hook-form": { - "version": "7.56.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-hook-form/-/react-hook-form-7.56.4.tgz", - "integrity": "sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw==", + "version": "7.60.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-hook-form/-/react-hook-form-7.60.0.tgz", + "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", + "license": "MIT", "engines": { "node": ">=18.0.0" }, @@ -23980,9 +24049,9 @@ } }, "node_modules/react-router": { - "version": "7.6.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.6.0.tgz", - "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "version": "7.6.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.6.3.tgz", + "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -27367,9 +27436,10 @@ "license": "CC0-1.0" }, "node_modules/sass": { - "version": "1.89.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.89.0.tgz", - "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", + "version": "1.89.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.89.2.tgz", + "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -27440,9 +27510,9 @@ } }, "node_modules/sass/node_modules/immutable": { - "version": "5.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/immutable/-/immutable-5.1.2.tgz", - "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", + "version": "5.1.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", "license": "MIT" }, "node_modules/sass/node_modules/readdirp": { @@ -28771,6 +28841,20 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream": { "version": "0.0.3", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/stream/-/stream-0.0.3.tgz", @@ -29567,9 +29651,9 @@ } }, "node_modules/swagger-ui-react": { - "version": "5.22.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.22.0.tgz", - "integrity": "sha512-Y0TEWg2qD4u/dgZ9q9G16yM/Edvyz0ovkIZlpACN8X/2gzSoIzS/fhSpLSJfCOxRt2UqrKmajMB11VK6cGZk2g==", + "version": "5.26.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.26.2.tgz", + "integrity": "sha512-2QHyN/vl3HaihXpBhJm7du7gfSibM3T3cSdk7Od98u2oFtraNTAe1r1k9cw/XBKBpl5XTye6rvACe8VP/NO2og==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.27.1", @@ -29600,7 +29684,7 @@ "reselect": "^5.1.1", "serialize-error": "^8.1.0", "sha.js": "^2.4.11", - "swagger-client": "^3.35.3", + "swagger-client": "^3.35.5", "url-parse": "^1.5.10", "xml": "=1.0.1", "xml-but-prettier": "^1.0.1", @@ -29650,14 +29734,13 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.11.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/synckit/-/synckit-0.11.3.tgz", - "integrity": "sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==", + "version": "0.11.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.1", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index ae9ad7c593..191dc6b12d 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -7,8 +7,8 @@ "@emotion/core": "11.0.0", "@emotion/is-prop-valid": "1.3.1", "@emotion/react": "11.14.0", - "@emotion/styled": "11.14.0", - "@eslint/migrate-config": "1.5.0", + "@emotion/styled": "11.14.1", + "@eslint/migrate-config": "1.5.2", "@jest/globals": "29.7.0", "@material-ui/core": "4.12.4", "@material-ui/icons": "4.11.3", @@ -17,7 +17,7 @@ "@react-loadable/revised": "1.5.0", "@types/enzyme": "3.10.19", "@types/jest": "29.5.14", - "@types/react": "18.3.22", + "@types/react": "18.3.23", "agentkeepalive": "4.6.0", "buffer": "6.0.3", "emotion-theming": "11.0.0", @@ -37,9 +37,9 @@ "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", "react-error-boundary": "5.0.0", - "react-hook-form": "7.56.4", + "react-hook-form": "7.60.0", "react-redux": "9.2.0", - "react-router": "7.6.0", + "react-router": "7.6.3", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -49,9 +49,9 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.89.0", + "sass": "1.89.2", "stream": "0.0.3", - "swagger-ui-react": "5.22.0", + "swagger-ui-react": "5.26.2", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -82,14 +82,14 @@ "includeFailureMsg": true }, "devDependencies": { - "@babel/core": "7.27.1", - "@babel/eslint-parser": "7.27.1", + "@babel/core": "7.28.0", + "@babel/eslint-parser": "7.28.0", "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "@babel/preset-env": "7.27.2", + "@babel/preset-env": "7.28.0", "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", - "@eslint/compat": "1.2.9", - "@eslint/js": "9.27.0", + "@eslint/compat": "1.3.1", + "@eslint/js": "9.31.0", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", @@ -98,22 +98,22 @@ "ajv": "8.17.1", "ansi-regex": "6.1.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001718", - "concurrently": "9.1.2", + "caniuse-lite": "1.0.30001727", + "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.27.0", + "eslint": "9.31.0", "eslint-config-airbnb": "19.0.4", "eslint-config-prettier": "9.1.0", "eslint-plugin-cypress": "4.3.0", "eslint-plugin-flowtype": "8.0.3", "eslint-plugin-header": "3.1.1", - "eslint-plugin-import": "2.31.0", + "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-prettier": "5.4.0", + "eslint-plugin-prettier": "5.5.1", "eslint-plugin-react": "7.37.5", "express": "4.21.2", "globals": "15.15.0", @@ -130,7 +130,7 @@ "mini-css-extract-plugin": "2.9.2", "nodemon": "3.1.10", "nth-check": "2.1.1", - "prettier": "3.5.3", + "prettier": "3.6.2", "prop-types": "15.8.1", "querystring-es3": "0.2.1", "react-app-rewired": "2.2.1", @@ -154,8 +154,8 @@ "resolve-url-loader": "5.0.0", "lodash": "4.17.21", "semver": "7.7.2", - "@babel/traverse": "7.27.1", - "axios": "1.9.0", + "@babel/traverse": "7.28.0", + "axios": "1.10.0", "swagger-ui-react": { "react-syntax-highlighter": { "refractor": "5.0.0" diff --git a/gradle/versions.gradle b/gradle/versions.gradle index aaac2dafb3..9884cd25a7 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -73,14 +73,14 @@ dependencyResolutionManagement { // force version in build.gradle file - compatibility with Slf4j version('log4j', '2.25.1') version('lombok', '1.18.38') - version('netty', '4.2.2.Final') + version('netty', '4.2.3.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 version('nettyReactor', '1.2.7') version('nimbusJoseJwt', '9.48') version('openApiDiff', '2.1.2') version('picocli', '4.7.7') - version('reactor', '3.7.7') + version('reactor', '3.7.8') version('restAssured', '5.5.5') version('rhino', '1.8.0') version('springDoc', '2.8.9') diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index a6710d54d3..39e2dfdc47 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -14,9 +14,9 @@ "devDependencies": { "@eslint/js": "9.31.0", "@types/jest": "29.5.14", - "@types/node": "20.19.7", - "@typescript-eslint/eslint-plugin": "8.36.0", - "@typescript-eslint/parser": "8.36.0", + "@types/node": "20.19.8", + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", "@zowe/cli": "8.24.4", "@zowe/cli-test-utils": "8.24.2", "@zowe/imperative": "8.24.2", @@ -2060,9 +2060,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.19.7", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.7.tgz", - "integrity": "sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==", + "version": "20.19.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.8.tgz", + "integrity": "sha512-HzbgCY53T6bfu4tT7Aq3TvViJyHjLjPNaAS3HOuMc9pw97KHsUtXNX4L+wu59g1WnjsZSko35MbEqnO58rihhw==", "dev": true, "license": "MIT", "dependencies": { @@ -2105,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", - "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", + "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/type-utils": "8.36.0", - "@typescript-eslint/utils": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/type-utils": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2129,7 +2129,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.36.0", + "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2158,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.36.0.tgz", - "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.37.0.tgz", + "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/typescript-estree": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "engines": { @@ -2183,14 +2183,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", - "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", + "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.36.0", - "@typescript-eslint/types": "^8.36.0", + "@typescript-eslint/tsconfig-utils": "^8.37.0", + "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "engines": { @@ -2205,14 +2205,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", - "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", + "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0" + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,9 +2223,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", - "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", + "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", "dev": true, "license": "MIT", "engines": { @@ -2240,14 +2240,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", - "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", + "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.36.0", - "@typescript-eslint/utils": "8.36.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2277,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.36.0.tgz", - "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.37.0.tgz", + "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", "dev": true, "license": "MIT", "engines": { @@ -2291,16 +2292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", - "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", + "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.36.0", - "@typescript-eslint/tsconfig-utils": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/project-service": "8.37.0", + "@typescript-eslint/tsconfig-utils": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2359,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.36.0.tgz", - "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.37.0.tgz", + "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/typescript-estree": "8.36.0" + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2383,13 +2384,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.36.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", - "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", + "version": "8.37.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", + "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 3031893bb8..2eeabd7060 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -51,9 +51,9 @@ "devDependencies": { "@eslint/js": "9.31.0", "@types/jest": "29.5.14", - "@types/node": "20.19.7", - "@typescript-eslint/eslint-plugin": "8.36.0", - "@typescript-eslint/parser": "8.36.0", + "@types/node": "20.19.8", + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", "@zowe/cli": "8.24.4", "@zowe/cli-test-utils": "8.24.2", "@zowe/imperative": "8.24.2", From b8036682bf1785dee0d35caee2206b98f2245bd8 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 15 Jul 2025 16:33:32 +0200 Subject: [PATCH 018/152] fix: API ML startup message in modulith mode (#4216) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../product/logging/ApimlLogInjector.java | 12 +++- .../product/logging/ApimlLogInjectorTest.java | 10 +++- .../zowe/apiml/GatewayHealthIndicator.java | 51 ++++++++++++---- .../AcceptanceTestWithMockServices.java | 2 +- .../StartupMessageAcceptanceTest.java | 60 +++++++++++++++++++ .../config/GatewayHealthIndicator.java | 32 ++++++---- .../config/GatewayHealthIndicatorTest.java | 2 +- 7 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java diff --git a/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlLogInjector.java b/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlLogInjector.java index 88bbec48f6..c254ae6a7f 100644 --- a/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlLogInjector.java +++ b/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlLogInjector.java @@ -14,7 +14,9 @@ import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; @@ -29,6 +31,8 @@ @RequiredArgsConstructor public class ApimlLogInjector implements BeanPostProcessor { + private final ApplicationContext applicationContext; + @Override public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull String beanName) { return bean; @@ -41,7 +45,13 @@ public Object postProcessBeforeInitialization(final Object bean, @Nonnull String // make the field accessible if defined private ReflectionUtils.makeAccessible(field); Class clazz = getClass(bean); - ApimlLogger log = ApimlLogger.of(clazz, YamlMessageServiceInstance.getInstance()); + + ApimlLogger log; + try { + log = applicationContext.getBean(ApimlLogger.class); + } catch (BeansException e) { + log = ApimlLogger.of(clazz, YamlMessageServiceInstance.getInstance()); + } field.set(bean, log); } }); diff --git a/apiml-utility/src/test/java/org/zowe/apiml/product/logging/ApimlLogInjectorTest.java b/apiml-utility/src/test/java/org/zowe/apiml/product/logging/ApimlLogInjectorTest.java index bfae292792..303e72634d 100644 --- a/apiml-utility/src/test/java/org/zowe/apiml/product/logging/ApimlLogInjectorTest.java +++ b/apiml-utility/src/test/java/org/zowe/apiml/product/logging/ApimlLogInjectorTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -47,13 +48,16 @@ private static class TestComponent { @TestConfiguration static class TestConfig { + @Autowired + private ApplicationContext applicationContext; + @Bean - public ApimlLogInjector apimlLogInjector() { - return new ApimlLogInjector(); + ApimlLogInjector apimlLogInjector() { + return new ApimlLogInjector(applicationContext); } @Bean - public TestComponent testComponent() { + TestComponent testComponent() { return new ApimlLogInjectorTest.TestComponent(); } diff --git a/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java index 463477a9e4..e065d1a2eb 100644 --- a/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java +++ b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java @@ -11,17 +11,20 @@ package org.zowe.apiml; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.Status; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; +import org.zowe.apiml.product.compatibility.ApimlHealthCheckHandler; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.zaas.ZaasServiceAvailableEvent; import java.util.concurrent.atomic.AtomicBoolean; @@ -39,25 +42,27 @@ @RequiredArgsConstructor public class GatewayHealthIndicator extends AbstractHealthIndicator { - private static final ApimlLogger apimlLog = ApimlLogger.of(GatewayHealthIndicator.class, YamlMessageServiceInstance.getInstance()); private final DiscoveryClient discoveryClient; + @InjectApimlLogger + private final ApimlLogger apimlLog = ApimlLogger.empty(); + @Value("${apiml.catalog.serviceId:}") private String apiCatalogServiceId; private AtomicBoolean discoveryAvailable = new AtomicBoolean(false); private AtomicBoolean zaasAvailable = new AtomicBoolean(false); - + private AtomicBoolean catalogAvailable = new AtomicBoolean(false); private AtomicBoolean startedInformationPublished = new AtomicBoolean(false); @Override protected void doHealthCheck(Builder builder) throws Exception { - var anyCatalogIsAvailable = apiCatalogServiceId != null && !apiCatalogServiceId.isEmpty(); - var apiCatalogUp = !this.discoveryClient.getInstances(apiCatalogServiceId).isEmpty(); + var anyCatalogIsAvailable = StringUtils.isNotBlank(apiCatalogServiceId); + catalogAvailable.set(anyCatalogIsAvailable && !this.discoveryClient.getInstances(apiCatalogServiceId).isEmpty()); // Keeping for backwards compatibility, in modulith the amount of gateways is the amount of authentication services available - int gatewayCount = this.discoveryClient.getInstances(CoreService.GATEWAY.getServiceId()).size(); - int zaasCount = gatewayCount; + var gatewayCount = this.discoveryClient.getInstances(CoreService.GATEWAY.getServiceId()).size(); + var zaasCount = gatewayCount; builder.status(toStatus(discoveryAvailable.get() && zaasAvailable.get())) .withDetail(CoreService.DISCOVERY.getServiceId(), toStatus(discoveryAvailable.get()).getCode()) @@ -66,23 +71,49 @@ protected void doHealthCheck(Builder builder) throws Exception { .withDetail("zaasCount", zaasCount); if (anyCatalogIsAvailable) { - builder.withDetail(CoreService.API_CATALOG.getServiceId(), toStatus(apiCatalogUp).getCode()); + builder.withDetail(CoreService.API_CATALOG.getServiceId(), toStatus(catalogAvailable.get()).getCode()); + } + + if (isFullyUp()) { + onFullyUp(); } + } + + private boolean isFullyUp() { + return !startedInformationPublished.get() && discoveryAvailable.get() && catalogAvailable.get() && zaasAvailable.get(); + } - if (!startedInformationPublished.get() && discoveryAvailable.get() && apiCatalogUp && zaasAvailable.get()) { + private void onFullyUp() { + if (startedInformationPublished.compareAndSet(false, true)) { apimlLog.log("org.zowe.apiml.common.mediationLayerStarted"); - startedInformationPublished.set(true); } } @EventListener public void onApplicationEvent(ZaasServiceAvailableEvent event) { zaasAvailable.set(true); + if (isFullyUp()) { + onFullyUp(); + } } @EventListener public void onApplicationEvent(EurekaRegistryAvailableEvent event) { discoveryAvailable.set(true); + if (isFullyUp()) { + onFullyUp(); + } + } + + @EventListener + public void onApplicationEvent(EurekaInstanceRegisteredEvent event) { + var instanceInfo = event.getInstanceInfo(); + if (String.valueOf(instanceInfo.getAppName()).equalsIgnoreCase(apiCatalogServiceId)) { + catalogAvailable.set(true); + } + if (isFullyUp()) { + onFullyUp(); + } } boolean isStartedInformationPublished() { diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTestWithMockServices.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTestWithMockServices.java index 830d0a44fd..dd28788dd1 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTestWithMockServices.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTestWithMockServices.java @@ -28,7 +28,7 @@ public class AcceptanceTestWithMockServices extends AcceptanceTestWithBasePath { @Autowired - private ApplicationEventPublisher applicationEventPublisher; + protected ApplicationEventPublisher applicationEventPublisher; @Autowired @Qualifier("applicationRegistry") diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java new file mode 100644 index 0000000000..5efa37caf2 --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java @@ -0,0 +1,60 @@ +/* + * 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.acceptance; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.eureka.EurekaServerConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; +import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.zaas.ZaasServiceAvailableEvent; + +import java.util.concurrent.TimeUnit; + +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +@AcceptanceTest +@ExtendWith(MockitoExtension.class) +public class StartupMessageAcceptanceTest extends AcceptanceTestWithMockServices { + + @MockitoBean private ApimlLogger logger; + + @Mock private InstanceInfo instanceInfo; + + @BeforeEach + void setUp() { + lenient().when(instanceInfo.getInstanceId()).thenReturn("apicatalog:localhost:1000"); + lenient().when(instanceInfo.getAppName()).thenReturn("APICATALOG"); + + applicationEventPublisher.publishEvent(new ZaasServiceAvailableEvent("dummy")); + applicationEventPublisher.publishEvent(new EurekaRegistryAvailableEvent(mock(EurekaServerConfig.class))); + applicationEventPublisher.publishEvent(new EurekaInstanceRegisteredEvent(new Object(), instanceInfo, DISCOVERY_PORT, false)); + } + + @Test + @Timeout(unit = TimeUnit.SECONDS, value = 30) + void whenFullyStartedUp_thenEmitMessage() { + + verify(logger, timeout(30_000)).log("org.zowe.apiml.common.mediationLayerStarted"); + + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java index 7be21cd967..ab4028f2a4 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/GatewayHealthIndicator.java @@ -10,6 +10,7 @@ package org.zowe.apiml.gateway.config; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; @@ -18,9 +19,11 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Component; import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.compatibility.ApimlHealthCheckHandler; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; + +import java.util.concurrent.atomic.AtomicBoolean; import static org.springframework.boot.actuate.health.Status.DOWN; import static org.springframework.boot.actuate.health.Status.UP; @@ -35,11 +38,11 @@ public class GatewayHealthIndicator extends AbstractHealthIndicator { protected final DiscoveryClient discoveryClient; - private String apiCatalogServiceId; + private final String apiCatalogServiceId; + @InjectApimlLogger + private final ApimlLogger apimlLog = ApimlLogger.empty(); - private final ApimlLogger apimlLog = ApimlLogger.of(GatewayHealthIndicator.class, - YamlMessageServiceInstance.getInstance()); - boolean startedInformationPublished = false; + private AtomicBoolean startedInformationPublished = new AtomicBoolean(false); public GatewayHealthIndicator(DiscoveryClient discoveryClient, @Value("${apiml.catalog.serviceId:}") String apiCatalogServiceId) { @@ -49,16 +52,16 @@ public GatewayHealthIndicator(DiscoveryClient discoveryClient, @Override protected void doHealthCheck(Health.Builder builder) { - boolean anyCatalogIsAvailable = apiCatalogServiceId != null && !apiCatalogServiceId.isEmpty(); - boolean apiCatalogUp = !this.discoveryClient.getInstances(apiCatalogServiceId).isEmpty(); + var anyCatalogIsAvailable = StringUtils.isNotBlank(apiCatalogServiceId); + var apiCatalogUp = anyCatalogIsAvailable && !this.discoveryClient.getInstances(apiCatalogServiceId).isEmpty(); // When DS goes 'down' after it was already 'up', the new status is not shown. This is probably feature of // Eureka client which caches the status of services. When DS is down the cache is not refreshed. - boolean discoveryUp = !this.discoveryClient.getInstances(CoreService.DISCOVERY.getServiceId()).isEmpty(); - boolean zaasUp = !this.discoveryClient.getInstances(CoreService.ZAAS.getServiceId()).isEmpty(); + var discoveryUp = !this.discoveryClient.getInstances(CoreService.DISCOVERY.getServiceId()).isEmpty(); + var zaasUp = !this.discoveryClient.getInstances(CoreService.ZAAS.getServiceId()).isEmpty(); - int gatewayCount = this.discoveryClient.getInstances(CoreService.GATEWAY.getServiceId()).size(); - int zaasCount = this.discoveryClient.getInstances(CoreService.ZAAS.getServiceId()).size(); + var gatewayCount = this.discoveryClient.getInstances(CoreService.GATEWAY.getServiceId()).size(); + var zaasCount = this.discoveryClient.getInstances(CoreService.ZAAS.getServiceId()).size(); builder.status(toStatus(discoveryUp)) .withDetail(CoreService.DISCOVERY.getServiceId(), toStatus(discoveryUp).getCode()) @@ -76,12 +79,15 @@ protected void doHealthCheck(Health.Builder builder) { } private void onFullyUp() { - if (!startedInformationPublished) { + if (startedInformationPublished.compareAndSet(false, true)) { apimlLog.log("org.zowe.apiml.common.mediationLayerStarted"); - startedInformationPublished = true; } } + boolean isStartedInformationPublished() { + return startedInformationPublished.get(); + } + private Status toStatus(boolean up) { return up ? UP : DOWN; } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/GatewayHealthIndicatorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/GatewayHealthIndicatorTest.java index 5b6e4ec3ad..0c54c699be 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/GatewayHealthIndicatorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/GatewayHealthIndicatorTest.java @@ -106,7 +106,7 @@ void whenHealthRequested_onceLogMessageAboutStartup() { Health.Builder builder = new Health.Builder(); healthIndicator.doHealthCheck(builder); - assertThat(healthIndicator.startedInformationPublished, is(true)); + assertThat(healthIndicator.isStartedInformationPublished(), is(true)); } } } From 54a57d13464711853beab7d347069646bfafccbc Mon Sep 17 00:00:00 2001 From: Andrea Tabone <39694626+taban03@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:31:17 +0200 Subject: [PATCH 019/152] fix: Fix SAF auth check in non-modulith (#4212) Signed-off-by: Andrea Tabone Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- integration-tests/build.gradle | 3 +- .../config/NewSecurityConfiguration.java | 28 +++++---- .../org/zowe/apiml/acceptance/ZaasTest.java | 62 +++++++++++++++++-- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index cba14a6362..e7c7465930 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -368,8 +368,7 @@ task runContainerTests(type: Test) { 'GatewayProxyTest', 'GatewayServiceRouting', 'GatewayCentralRegistry', - 'ZaasTest', - 'SAFAuthResourceCheckTest' + 'ZaasTest' ) } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java index 25671c4ff4..ead04f2f1f 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java @@ -158,7 +158,7 @@ public SecurityFilterChain authenticationFunctionalityFilterChain(HttpSecurity h private class CustomSecurityFilters extends AbstractHttpConfigurer { @Override - public void configure(HttpSecurity http) throws Exception { + public void configure(HttpSecurity http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); //drive filter order this way http.addFilterBefore(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) @@ -193,7 +193,7 @@ private LogoutHandler logoutHandler() { /** * Secures endpoints: * - /auth/access-token/generate - * + *

* Requires authentication by a client certificate forwarded form Gateway or basic authentication, supports credentials in header and body. * The request is fulfilled by the filter chain only, there is no controller to handle it. * Order of custom filters: @@ -228,7 +228,7 @@ public SecurityFilterChain accessTokenFilterChain(HttpSecurity http) throws Exce private class CustomSecurityFilters extends AbstractHttpConfigurer { @Override - public void configure(HttpSecurity http) throws Exception { + public void configure(HttpSecurity http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); //drive filter order this way http.addFilterBefore(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) @@ -361,7 +361,7 @@ public SecurityFilterChain queryFilterChain(HttpSecurity http) throws Exception private class CustomSecurityFilters extends AbstractHttpConfigurer { @Override - public void configure(HttpSecurity http) throws Exception { + public void configure(HttpSecurity http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); http.addFilterBefore(queryFilter("/**", authenticationManager), UsernamePasswordAuthenticationFilter.class); } @@ -406,7 +406,7 @@ public SecurityFilterChain ticketFilterChain(HttpSecurity http) throws Exception private class CustomSecurityFilters extends AbstractHttpConfigurer { @Override - public void configure(HttpSecurity http) throws Exception { + public void configure(HttpSecurity http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); http.addFilterBefore(ticketFilter("/**", authenticationManager), UsernamePasswordAuthenticationFilter.class); } @@ -444,7 +444,7 @@ public SecurityFilterChain refreshFilterChain(HttpSecurity http) throws Exceptio ))).authorizeHttpRequests(requests -> requests .anyRequest().authenticated()) .authenticationProvider(tokenAuthenticationProvider) - .logout(logout -> logout.disable()) // logout filter in this chain not needed + .logout(AbstractHttpConfigurer::disable) // logout filter in this chain not needed .x509(x509 -> x509 //default x509 filter, authenticates trusted cert, refreshFilter(..) depends on this .userDetailsService(new SimpleUserDetailService())) .with(new CustomSecurityFilters(), Customizer.withDefaults()); @@ -454,7 +454,7 @@ public SecurityFilterChain refreshFilterChain(HttpSecurity http) throws Exceptio private class CustomSecurityFilters extends AbstractHttpConfigurer { @Override - public void configure(HttpSecurity http) throws Exception { + public void configure(HttpSecurity http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); http.addFilterBefore(refreshFilter("/**", authenticationManager), UsernamePasswordAuthenticationFilter.class); } @@ -519,11 +519,9 @@ public SecurityFilterChain certificateOrAuthEndpointsFilterChain(HttpSecurity ht .logout(AbstractHttpConfigurer::disable); // logout filter in this chain not needed if (isAttlsEnabled) { - http.x509(withDefaults()) + http // filter out API ML certificate .addFilterBefore(reversedCategorizeCertFilter(), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class); - } else { - http.x509(x509 -> x509.userDetailsService(x509UserDetailsService())); // default x509 filter, authenticates trusted cert } return http.authenticationProvider(compoundAuthProvider) // for authenticating credentials @@ -535,10 +533,12 @@ public SecurityFilterChain certificateOrAuthEndpointsFilterChain(HttpSecurity ht private class CustomSecurityFilters extends AbstractHttpConfigurer { @Override - public void configure(HttpSecurity http) throws Exception { + public void configure(HttpSecurity http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); // place the following filters before the x509 filter http + .addFilterAfter(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) + .addFilterAfter(x509ForwardingAwareAuthenticationFilter(), CategorizeCertsFilter.class) // this filter consumes certificates from custom attribute and maps them to credentials and authenticates them .addFilterBefore(basicFilter(authenticationManager), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) .addFilterBefore(cookieFilter(authenticationManager), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) .addFilterBefore(bearerContentFilter(authenticationManager), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class); @@ -577,6 +577,12 @@ private BearerContentFilter bearerContentFilter(AuthenticationManager authentica handlerInitializer.getResourceAccessExceptionHandler(), new String[] {"/"}); } + + private X509ForwardingAwareAuthenticationFilter x509ForwardingAwareAuthenticationFilter() { + return new X509AuthAwareFilter("/**", + handlerInitializer.getAuthenticationFailureHandler(), + x509AuthenticationProvider); + } } private CategorizeCertsFilter reversedCategorizeCertFilter() { diff --git a/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java b/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java index 5df166a7ea..88a4945737 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java @@ -12,21 +12,24 @@ import io.restassured.config.SSLConfig; import org.apache.http.conn.ssl.SSLSocketFactory; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.zaas.ZaasApplication; -import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; import org.zowe.apiml.zaas.utils.JWTUtils; import static io.restassured.RestAssured.config; import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.apache.hc.core5.http.HttpStatus.SC_SERVICE_UNAVAILABLE; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; import static org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; @SpringBootTest(classes = ZaasApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -37,9 +40,6 @@ class ZaasTest { private static final String COOKIE = "apimlAuthenticationToken"; - @MockitoBean(name = "x509Mapper") - private AuthenticationMapper x509Mapper; - @Autowired private HttpConfig httpConfig; @@ -49,6 +49,29 @@ class ZaasTest { @Value("${apiml.service.hostname:localhost}") private String hostname; + private static final String BODY = """ + { + "resourceClass": "ZOWE", + "resourceName": "APIML.SERVICES", + "accessLevel": "READ" + } + """; + + @Value("${server.ssl.keyPassword}") + private char[] password; + + @Value("${server.ssl.keyStore}") + private String clientCertKeystore; + + @Value("${server.ssl.keyStore}") + private String keystore; + + @BeforeEach + void setUp() throws Exception { + SslContextConfigurer configurer = new SslContextConfigurer(password, clientCertKeystore, keystore); + SslContext.prepareSslAuthentication(configurer); + } + @Test void givenZosmfCookieAndDummyAuthProvider_whenZoweJwtRequest_thenUnavailable() { String zosmfJwt = JWTUtils.createZosmfJwtToken("user", "z/OS", "Ltpa", httpConfig.getHttpsConfig()); @@ -65,4 +88,33 @@ void givenZosmfCookieAndDummyAuthProvider_whenZoweJwtRequest_thenUnavailable() { //@formatter:on } + @Test + void givenNoCert_whenCallingSafCheck_then401() { + + //@formatter:off + given() + .config(SslContext.tlsWithoutCert) + .contentType(APPLICATION_JSON) + .body(BODY) + .when() + .post(String.format("https://%s:%d/zaas/auth/check", hostname, port)) + .then() + .statusCode(SC_UNAUTHORIZED); + //@formatter:on + } + + @Test + void givenInvalidCert_whenCallingSafCheck_then401() { + //@formatter:off + given() + .config(SslContext.clientCertUnknownUser) + .contentType(APPLICATION_JSON) + .body(BODY) + .when() + .post(String.format("https://%s:%d/zaas/auth/check", hostname, port)) + .then() + .statusCode(SC_UNAUTHORIZED); + //@formatter:on + } + } From 5a7a011d60e78ba2e8533a664ea08e1088875fa5 Mon Sep 17 00:00:00 2001 From: Andrea Tabone <39694626+taban03@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:37:28 +0200 Subject: [PATCH 020/152] refactor: Include Caching service in the Modulith (#4190) Signed-off-by: ac892247 Signed-off-by: Andrea Tabone Co-authored-by: ac892247 Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 115 ++++-- .../apicatalog/config/CachingConfig.java | 2 + .../detail-page/service-version-compare.cy.js | 3 - .../eureka/client/ApimlPeerEurekaNode.java | 23 -- .../zowe/apiml/product/web/HttpConfig.java | 23 +- apiml-package/src/main/resources/bin/start.sh | 17 + .../security-common-log-messages.yml | 7 + .../zowe/apiml/filter/AttlsHttpHandler.java | 9 +- .../apiml/filter/AttlsHttpHandlerTest.java | 14 +- apiml/build.gradle | 3 + .../java/org/zowe/apiml/ApimlApplication.java | 9 +- ...achingServiceEurekaInstanceConfigBean.java | 26 ++ .../org/zowe/apiml/EurekaConfiguration.java | 210 ++-------- .../GatewayEurekaInstanceConfigBean.java | 2 + .../java/org/zowe/apiml/ModulithConfig.java | 52 +-- .../org/zowe/apiml/WebSecurityConfig.java | 71 ++-- .../zowe/apiml/ZaasSchemeTransformApi.java | 30 +- .../controller/ApimlExceptionHandler.java | 7 + .../controller/ReactivePATController.java | 107 ++++- .../src/main/resources/apiml-log-messages.yml | 0 apiml/src/main/resources/application.yml | 78 +++- .../apiml/ZaasSchemeTransformApiTest.java | 58 ++- ...ReactiveAuthenticationControllerTests.java | 19 +- .../controller/ReactivePATControllerTest.java | 11 + apiml/src/test/resources/application.yml | 9 + build.gradle | 4 + caching-service/build.gradle | 6 +- .../apiml/caching/api/CachingController.java | 162 ++++---- .../apiml/caching/config/GeneralConfig.java | 3 +- .../caching/config/MessageConfiguration.java | 2 + .../caching/config/SpringSecurityConfig.java | 91 +++-- .../apiml/caching/config/SwaggerConfig.java | 2 + .../apiml/caching/service/RejectStrategy.java | 1 + .../infinispan/config/InfinispanConfig.java | 13 +- .../infinispan/storage/InfinispanStorage.java | 4 +- .../service/inmemory/InMemoryStorage.java | 2 + .../config/InMemoryConfiguration.java | 2 +- .../caching/service/redis/RedisStorage.java | 4 +- .../redis/config/RedisConfiguration.java | 2 +- .../caching/service/vsam/VsamRecord.java | 2 +- .../caching/service/vsam/VsamStorage.java | 4 +- .../vsam/config/VsamConfiguration.java | 2 +- .../src/main/resources/application.yml | 16 +- .../src/main/resources/infinispan.xml | 2 + .../caching/api/CachingControllerTest.java | 380 +++++++++++------- .../apiml/caching/config/AttlsConfigTest.java | 44 +- .../functional/InMemoryFunctionalTest.java | 4 +- .../storage/InfinispanStorageTest.java | 2 +- .../service/inmemory/InMemoryStorageTest.java | 2 +- .../service/inmemory/RejectStrategyTest.java | 2 +- .../service/redis/RedisStorageTest.java | 2 +- .../caching/service/vsam/VsamRecordTest.java | 2 +- .../caching/service/vsam/VsamStorageTest.java | 2 +- .../src/test/resources/application.yml | 4 +- .../java/org/zowe/apiml/cache}/Storage.java | 2 +- .../zowe/apiml/cache}/StorageException.java | 2 +- .../zowe/apiml/caching/model/KeyValue.java | 4 + .../apiml/product/constants/CoreService.java | 3 +- .../org/zowe/apiml/security/HttpsConfig.java | 3 + config/local/apiml-service-2.yml | 79 ++++ config/local/apiml-service.yml | 10 +- .../staticdef/ServiceDefinitionProcessor.java | 2 +- .../src/main/resources/application.yml | 3 +- .../gateway/caching/CachingServiceClient.java | 108 +---- .../caching/CachingServiceClientApi.java | 96 +++++ .../caching/CachingServiceClientRest.java | 128 ++++++ .../gateway/caching/LoadBalancerCache.java | 48 +-- .../caching/CachingServiceClientApiTest.java | 113 ++++++ ...java => CachingServiceClientRestTest.java} | 29 +- .../caching/LoadBalancerCacheTest.java | 18 +- integration-tests/build.gradle | 21 +- .../caching/CachingAuthenticationTest.java | 4 + .../pat/AccessTokenServiceTest.java | 6 +- .../schemes/PassticketSchemeTest.java | 2 +- .../schemes/ZoweJwtSchemeTest.java | 5 +- .../external/CachingStorageTest.java | 4 +- .../org/zowe/apiml/util/SecurityUtils.java | 3 +- .../util/categories/NonModulithTest.java | 30 ++ .../util/service/FullApiMediationLayer.java | 15 +- .../apiml/util/service/RunningService.java | 4 +- .../config/EnableApiDiscoveryConfig.java | 1 + .../enable/register/RegisterToApiLayer.java | 28 +- package.json | 2 +- .../zowe/apiml/zaas/ZaasStartupListener.java | 2 +- .../zowe/apiml/zaas/cache/CachingClient.java | 32 ++ .../zaas/cache/CachingServiceClient.java | 10 +- .../apiml/zaas/cache/LocalCachingClient.java | 83 ++++ .../zowe/apiml/zaas/config/CacheConfig.java | 18 +- .../token/ApimlAccessTokenProvider.java | 8 +- .../security/service/zosmf/ZosmfService.java | 19 +- .../src/main/resources/application.yml | 4 +- .../zaas/cache/CachingServiceClientTest.java | 3 + .../zaas/cache/InMemoryCachingClientTest.java | 129 ++++++ 93 files changed, 1826 insertions(+), 928 deletions(-) create mode 100644 apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java create mode 100644 apiml/src/main/resources/apiml-log-messages.yml rename {caching-service/src/main/java/org/zowe/apiml/caching/service => common-service-core/src/main/java/org/zowe/apiml/cache}/Storage.java (99%) rename {caching-service/src/main/java/org/zowe/apiml/caching/service => common-service-core/src/main/java/org/zowe/apiml/cache}/StorageException.java (96%) rename {caching-service => common-service-core}/src/main/java/org/zowe/apiml/caching/model/KeyValue.java (92%) create mode 100644 config/local/apiml-service-2.yml create mode 100644 gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientApi.java create mode 100644 gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientApiTest.java rename gateway-service/src/test/java/org/zowe/apiml/gateway/caching/{CachingServiceClientTest.java => CachingServiceClientRestTest.java} (87%) create mode 100644 integration-tests/src/test/java/org/zowe/apiml/util/categories/NonModulithTest.java create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingClient.java create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/cache/LocalCachingClient.java create mode 100644 zaas-service/src/test/java/org/zowe/apiml/zaas/cache/InMemoryCachingClientTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1e6c737f1f..78bdc2d0b6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -58,10 +58,6 @@ jobs: APIML_SERVICE_HOSTNAME: api-catalog-services APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -78,7 +74,7 @@ jobs: - uses: ./.github/actions/setup - name: Run Modulith CI Tests - timeout-minutes: 4 + timeout-minutes: 6 run: > ENV_CONFIG=docker-modulith ./gradlew runStartUpCheck :integration-tests:runContainerModulithTests --info -Ddiscoverableclient.instances=1 -Denvironment.config=-docker-modulith -Denvironment.modulith=true @@ -151,10 +147,6 @@ jobs: APIML_SERVICE_HOSTNAME: api-catalog-services-2 APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml-2:10011/eureka,https://apiml:10011/eureka APIML_SERVICE_GATEWAYHOSTNAME: https://apiml-2:10010 - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -168,7 +160,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=1 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true @@ -254,10 +246,6 @@ jobs: APIML_HEALTH_PROTECTED: false APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -280,7 +268,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true @@ -400,7 +388,7 @@ jobs: - uses: ./.github/actions/setup - name: Run CI Tests - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck :integration-tests:runContainerTests --info -Denvironment.config=-docker -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} @@ -482,7 +470,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-ha -Ddiscoverableclient.instances=1 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD @@ -740,7 +728,7 @@ jobs: - uses: ./.github/actions/setup - name: Run CI Tests - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck :integration-tests:runGatewayCentralRegistryTest --info -Denvironment.config=-docker -Dgateway.instances=2 -Ddiscoverableclient.instances=0 @@ -1193,10 +1181,6 @@ jobs: env: APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -1246,7 +1230,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true @@ -1389,8 +1373,6 @@ jobs: image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} volumes: - /api-defs:/api-defs - caching-service: - image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -1530,7 +1512,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD @@ -1678,6 +1660,79 @@ jobs: - uses: ./.github/actions/teardown + CITestsModulithWithInfinispan: + needs: PublishJibContainers + runs-on: ubuntu-latest + container: ubuntu:latest + timeout-minutes: 15 + + services: + apiml: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_X509_ENABLED: true + APIML_SECURITY_SSL_NONSTRICTVERIFYSSLCERTIFICATESOFSERVICES: true + ZWE_CACHING_SERVICE_PERSISTENT: 'infinispan' + JGROUPS_BIND_PORT: "7600" + SERVER_SSL_KEYSTORETYPE: "PKCS12" + CACHING_STORAGE_INFINISPAN_PERSISTENCE_DATALOCATION: "data_replica" + CACHING_STORAGE_INFINISPAN_INITIALHOSTS: "apiml-2[7600]" + CACHING_STORAGE_MODE: "infinispan" + JGROUPS_BIND_ADDRESS: "apiml" + + apiml-2: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_X509_ENABLED: true + APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient + APIML_SECURITY_SSL_NONSTRICTVERIFYSSLCERTIFICATESOFSERVICES: true + ZWE_CACHING_SERVICE_PERSISTENT: 'infinispan' + JGROUPS_BIND_PORT: "7600" + SERVER_SSL_KEYSTORETYPE: "PKCS12" + CACHING_STORAGE_INFINISPAN_PERSISTENCE_DATALOCATION: "data" + CACHING_STORAGE_INFINISPAN_INITIALHOSTS: "apiml[7600]" + CACHING_STORAGE_MODE: "infinispan" + JGROUPS_BIND_ADDRESS: "apiml-2" + APIML_SERVICE_HOSTNAME: "apiml-2" + + api-catalog-services: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka/ + + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - uses: ./.github/actions/setup + + - name: Build with Gradle + run: > + ENV_CONFIG=docker-modulith ./gradlew :integration-tests:runModulithInfinispanServiceTests --info + -Ddiscoverableclient.instances=1 -Denvironment.config=-docker-modulith -Denvironment.modulith=true + -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} + + - uses: ./.github/actions/dump-jacoco + if: always() + + - name: Store results + uses: actions/upload-artifact@v4 + if: always() + with: + name: CITestsModulithWithInfinispan-${{ env.JOB_ID }} + path: | + integration-tests/build/reports/** + results/** + + - uses: ./.github/actions/teardown + E2EUITests: needs: PublishJibContainers runs-on: ubuntu-latest @@ -1761,7 +1816,7 @@ jobs: api-catalog-ui/frontend/node_modules key: my-cache-${{ runner.os }}-${{ hashFiles('api-catalog-ui/frontend/*.json') }} - name: Run startup check - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck --info --scan -Denvironment.config=-ha -Ddiscoverableclient.instances=1 - name: Show status when APIML is not ready yet @@ -1875,7 +1930,7 @@ jobs: api-catalog-ui/frontend/node_modules key: my-cache-${{ runner.os }}-${{ hashFiles('api-catalog-ui/frontend/*.json') }} - name: Run startup check - timeout-minutes: 4 + timeout-minutes: 6 run: > ./gradlew runStartUpCheck --info --scan -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=1 -Denvironment.modulith=true -Denvironment.gwCount=1 -Dgateway.host=gateway-service,gateway-service-2 -Ddiscovery.host=gateway-service -Ddiscovery.additionalHost=gateway-service-2 @@ -1885,8 +1940,8 @@ jobs: run: | apt update apt install -y apt-transport-https ca-certificates curl software-properties-common - curl -k -s https://apiml:10010/application/health - curl -k -s https://apiml-2:10010/application/health + curl -k -s https://gateway-service:10010/application/health + curl -k -s https://gateway-service-2:10010/application/health - name: Cypress run API Catalog run: | cd api-catalog-ui/frontend diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java index 5089201dfd..3dbc473d8c 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java @@ -16,6 +16,7 @@ import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import java.util.Arrays; @@ -24,6 +25,7 @@ public class CachingConfig { @Bean + @Primary public CacheManager cacheManager() { final SimpleCacheManager cacheManager = new SimpleCacheManager(); cacheManager.setCaches(Arrays.asList( diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index 4df1ce952e..76df837b19 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -42,9 +42,6 @@ describe('>>> Service version compare Test', () => { // FIXME modulith mode does not support multi tenancy yet let expectedServicesCount = 16; - if (isModulith) { - expectedServicesCount = 15; - } cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div .find('a.MuiTab-root') // Find all the anchor elements within the div diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java index 8d2b2ad8cf..259194b042 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/eureka/client/ApimlPeerEurekaNode.java @@ -46,8 +46,6 @@ import javax.net.ssl.SSLException; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; @@ -300,16 +298,6 @@ public EurekaHttpResponse execute() { expiryTime); } - /** - * Get the service Url of the peer eureka node. - * - * @return the service Url of the peer eureka node. - */ - @Override - public String getServiceUrl() { - return serviceUrl; - } - /** * Shuts down all resources used for peer replication. */ @@ -339,17 +327,6 @@ private void syncInstancesWhenTimestampDiffers(String appName, String id, Instan } } - @Override - public String getBatcherName() { - String batcherName; - try { - batcherName = new URL(serviceUrl).getHost(); - } catch (MalformedURLException e1) { - batcherName = serviceUrl; - } - return "target_" + batcherName; - } - private static String taskId(String requestType, String appName, String id) { return requestType + '#' + appName + '/' + id; } diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java index d911af5a64..aa9a397f5e 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java @@ -26,15 +26,12 @@ import org.springframework.context.annotation.Primary; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; -import org.zowe.apiml.security.ApimlPoolingHttpClientConnectionManager; -import org.zowe.apiml.security.HttpsConfig; -import org.zowe.apiml.security.HttpsConfigError; -import org.zowe.apiml.security.HttpsFactory; -import org.zowe.apiml.security.SecurityUtils; +import org.zowe.apiml.security.*; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; - +import java.security.KeyStore; +import java.security.cert.X509Certificate; import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -128,6 +125,11 @@ public void init() { updateStorePaths(); try { + X509Certificate certificate = null; + if (keyStore != null) { + KeyStore ks = SecurityUtils.loadKeyStore(keyStoreType, keyStore, keyStorePassword); + certificate = (X509Certificate) ks.getCertificate(keyAlias); + } Supplier httpsConfigSupplier = () -> HttpsConfig.builder() .protocol(protocol).enabledProtocols(supportedProtocols).cipherSuite(ciphers) @@ -141,12 +143,12 @@ public void init() { httpsConfig = httpsConfigSupplier.get() .keyAlias(keyAlias).keyStore(keyStore).keyPassword(keyPassword) - .keyStorePassword(keyStorePassword).keyStoreType(keyStoreType) + .keyStorePassword(keyStorePassword).keyStoreType(keyStoreType).certificate(certificate) .build(); HttpsConfig httpsConfigWithoutKeystore = httpsConfigSupplier.get().build(); - log.info("Using HTTPS configuration: {}", httpsConfig.toString()); + log.debug("Using HTTPS configuration: {}", httpsConfig.toString()); HttpsFactory factory = new HttpsFactory(httpsConfig); ApimlPoolingHttpClientConnectionManager secureConnectionManager = getConnectionManager(factory); @@ -199,6 +201,11 @@ public Set publicKeyCertificatesBase64() { return publicKeyCertificatesBase64; } + @Bean + public HttpsConfig httpsConfig() { + return httpsConfig; + } + /** * Returns RestTemplate with keystore. This RestTemplate makes calls to other systems with a certificate to sign to * other systems by certificate. It is necessary to call systems like DiscoverySystem etc. diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 91655f9ced..3ee2882ec5 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -100,6 +100,10 @@ # - ZWE_zowe_logDirectory # - ZWE_zowe_network_server_tls_attls # - ZWE_zowe_verifyCertificates - if we accept only verified certificates +# - ZWE_configs_storage_evictionStrategy +# - ZWE_configs_storage_mode +# - ZWE_configs_storage_size +# - ZWE_configs_storage_vsam_name # Optional variables: if [ -n "${LAUNCH_COMPONENT}" ]; then @@ -207,6 +211,10 @@ else externalProtocol="http" fi +if [ -n "${ZWE_configs_storage_vsam_name}" ]; then + VSAM_FILE_NAME=//\'${ZWE_configs_storage_vsam_name}\' +fi + LIBPATH="$LIBPATH":"/lib" LIBPATH="$LIBPATH":"/usr/lib" LIBPATH="$LIBPATH":"${JAVA_HOME}/bin" @@ -387,6 +395,15 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dserver.webSocket.maxIdleTimeout=${ZWE_configs_server_webSocket_maxIdleTimeout:-${ZWE_components_gateway_server_webSocket_maxIdleTimeout:-3600000}} \ -Dserver.webSocket.requestBufferSize=${ZWE_configs_server_webSocket_requestBufferSize:-${ZWE_components_gateway_server_webSocket_requestBufferSize:-8192}} \ -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-https} \ + -Dcaching.storage.evictionStrategy=${ZWE_configs_storage_evictionStrategy:-reject} \ + -Dcaching.storage.size=${ZWE_configs_storage_size:-10000} \ + -Dcaching.storage.mode=${ZWE_configs_storage_mode:-infinispan} \ + -Dcaching.storage.vsam.name=${VSAM_FILE_NAME} \ + -Djgroups.bind.address=${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}} \ + -Djgroups.bind.port=${ZWE_configs_storage_infinispan_jgroups_port:-7600} \ + -Djgroups.keyExchange.port=${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-7601} \ + -Djgroups.tcp.diag.enabled=${ZWE_configs_storage_infinispan_jgroups_tcp_diag_enabled:-false} \ + -Dcaching.storage.infinispan.initialHosts=${ZWE_configs_storage_infinispan_initialHosts:-localhost[7600]} \ -jar "${JAR_FILE}" & pid=$! diff --git a/apiml-security-common/src/main/resources/security-common-log-messages.yml b/apiml-security-common/src/main/resources/security-common-log-messages.yml index 7696c6ed30..fcfdbbf4c3 100644 --- a/apiml-security-common/src/main/resources/security-common-log-messages.yml +++ b/apiml-security-common/src/main/resources/security-common-log-messages.yml @@ -123,6 +123,13 @@ messages: reason: "The string sent by the Gateway was not recognized as valid DER-encoded certificates in the Base64 printable form." action: "Check that the URL configured in apiml.security.x509.certificatesUrls responds with valid DER-encoded certificates in the Base64 printable form." + - key: org.zowe.apiml.security.common.attls.notSecure + number: ZWEAT505 + type: ERROR + text: "The connection is not secure." + reason: "AT-TLS is not properly configured." + action: "Review AT-TLS documentation and make sure your configuration is correct for this service." + # Various messages # 600-699 diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java index 1b90534500..39f5c53969 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java @@ -16,6 +16,7 @@ import lombok.Builder; import lombok.RequiredArgsConstructor; import lombok.Value; +import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.RequestFacade; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.BeansException; @@ -48,6 +49,7 @@ @Component @RequiredArgsConstructor @ConditionalOnProperty(name = "server.attls.enabled", havingValue = "true") +@Slf4j public class AttlsHttpHandler implements BeanPostProcessor { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -76,11 +78,11 @@ private String getMessage(String key) { } Mono internalError(ServerHttpRequest request, ServerHttpResponse response) { - return writeError(request, response, getMessage("org.zowe.apiml.gateway.internalServerError")); + return writeError(request, response, getMessage("org.zowe.apiml.common.internalServerError")); } Mono unsecureError(ServerHttpRequest request, ServerHttpResponse response) { - return writeError(request, response, getMessage("org.zowe.apiml.gateway.security.attls.notSecure")); + return writeError(request, response, getMessage("org.zowe.apiml.security.common.attls.notSecure")); } ServerHttpRequest updateCertificate(ServerHttpRequest request, HttpServletRequest nativeRequest, byte[] rawCertificate) throws CertificateException { @@ -121,7 +123,8 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw requestFacade.setAttribute("attls", attlsContext); request = updateCertificate(request, requestFacade, attlsContext.getCertificate()); } catch (IoctlCallException | UnknownEnumValueException | ContextIsNotInitializedException | - CertificateException e) { + CertificateException | UnsatisfiedLinkError e) { + log.error("Cannot verify AT-TLS status", e); return internalError(request, response); } diff --git a/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsHttpHandlerTest.java b/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsHttpHandlerTest.java index 7a9457b1f4..9722c1cce6 100644 --- a/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsHttpHandlerTest.java +++ b/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsHttpHandlerTest.java @@ -153,7 +153,7 @@ void cleanContext() { } void mockAttlsContext(AttlsContext attlsContext) { - ThreadLocal contexts = (ThreadLocal) ReflectionTestUtils.getField(InboundAttls.class,"contexts"); + ThreadLocal contexts = (ThreadLocal) ReflectionTestUtils.getField(InboundAttls.class, "contexts"); if (attlsContext == null) { contexts.remove(); } else { @@ -162,7 +162,7 @@ void mockAttlsContext(AttlsContext attlsContext) { } AttlsContext createAttlsContext(StatConn statConn) { - return new AttlsContext(0, false) { + return new AttlsContext(0, false) { @Override public StatConn getStatConn() { return statConn; @@ -210,15 +210,15 @@ class Messages { private Message createMessage(String key, String message) { MessageTemplate messageTemplate = new MessageTemplate(key, "0", MessageType.ERROR, message); - return Message.of("org.zowe.apiml.gateway.internalServerError", messageTemplate, new Object[0]); + return Message.of("org.zowe.apiml.common.internalServerError", messageTemplate, new Object[0]); } @BeforeEach void init() { - lenient().doReturn(createMessage("org.zowe.apiml.gateway.internalServerError", "InternalError")) - .when(messageService).createMessage("org.zowe.apiml.gateway.internalServerError"); - lenient().doReturn(createMessage("org.zowe.apiml.gateway.security.attls.notSecure", "Unsecured")) - .when(messageService).createMessage("org.zowe.apiml.gateway.security.attls.notSecure"); + lenient().doReturn(createMessage("org.zowe.apiml.common.internalServerError", "InternalError")) + .when(messageService).createMessage("org.zowe.apiml.common.internalServerError"); + lenient().doReturn(createMessage("org.zowe.apiml.security.common.attls.notSecure", "Unsecured")) + .when(messageService).createMessage("org.zowe.apiml.security.common.attls.notSecure"); } @Test diff --git a/apiml/build.gradle b/apiml/build.gradle index 612b98a5e7..0700251866 100644 --- a/apiml/build.gradle +++ b/apiml/build.gradle @@ -58,9 +58,12 @@ dependencies { api project(":gateway-service") api project(":discovery-service") api project(":zaas-service") + api project(":caching-service") implementation libs.bundles.modulith implementation libs.spring.boot.starter.web + implementation libs.swagger2.parser + implementation libs.swagger3.parser implementation libs.bundles.jaxb implementation libs.spring.cloud.starter.eureka.client implementation libs.spring.cloud.starter.eureka.server diff --git a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java index 82942f7e02..d31c181636 100644 --- a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java +++ b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java @@ -17,6 +17,9 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.zowe.apiml.gateway.config.GatewayHealthIndicator; +import org.zowe.apiml.enable.EnableApiDiscovery; +import org.zowe.apiml.enable.config.EnableApiDiscoveryConfig; +import org.zowe.apiml.enable.register.RegisterToApiLayer; @SpringBootApplication( exclude = { ReactiveOAuth2ClientAutoConfiguration.class }, @@ -39,11 +42,11 @@ ), @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, - value = GatewayHealthIndicator.class + classes = { EnableApiDiscoveryConfig.class, EurekaController.class, RegisterToApiLayer.class, GatewayHealthIndicator.class } ), @ComponentScan.Filter( - type = FilterType.ASSIGNABLE_TYPE, - classes = EurekaController.class + type = FilterType.ANNOTATION, + classes = EnableApiDiscovery.class ) } ) diff --git a/apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java b/apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java new file mode 100644 index 0000000000..d766fe80cc --- /dev/null +++ b/apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java @@ -0,0 +1,26 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties("apiml.caching.eureka.instance") +public class CachingServiceEurekaInstanceConfigBean extends EurekaInstanceConfigBean { + + public CachingServiceEurekaInstanceConfigBean(InetUtils inetUtils) { + super(inetUtils); + } + +} diff --git a/apiml/src/main/java/org/zowe/apiml/EurekaConfiguration.java b/apiml/src/main/java/org/zowe/apiml/EurekaConfiguration.java index 2e1b25bb53..cda82fc122 100644 --- a/apiml/src/main/java/org/zowe/apiml/EurekaConfiguration.java +++ b/apiml/src/main/java/org/zowe/apiml/EurekaConfiguration.java @@ -33,30 +33,15 @@ import com.netflix.discovery.converters.wrappers.CodecWrapper; import com.netflix.discovery.converters.wrappers.CodecWrappers; import com.netflix.discovery.shared.transport.jersey.TransportClientFactories; -import com.netflix.discovery.shared.transport.jersey3.EurekaIdentityHeaderFilter; -import com.netflix.discovery.shared.transport.jersey3.EurekaJersey3Client; -import com.netflix.discovery.shared.transport.jersey3.EurekaJersey3ClientImpl; import com.netflix.discovery.shared.transport.jersey3.Jersey3TransportClientFactories; import com.netflix.eureka.DefaultEurekaServerContext; import com.netflix.eureka.EurekaServerConfig; import com.netflix.eureka.EurekaServerContext; -import com.netflix.eureka.EurekaServerIdentity; -import com.netflix.eureka.cluster.PeerEurekaNode; import com.netflix.eureka.cluster.PeerEurekaNodes; import com.netflix.eureka.registry.PeerAwareInstanceRegistry; -import com.netflix.eureka.resources.ASGResource; -import com.netflix.eureka.resources.ApplicationsResource; -import com.netflix.eureka.resources.DefaultServerCodecs; -import com.netflix.eureka.resources.InstancesResource; -import com.netflix.eureka.resources.PeerReplicationResource; -import com.netflix.eureka.resources.SecureVIPResource; -import com.netflix.eureka.resources.ServerCodecs; -import com.netflix.eureka.resources.ServerInfoResource; -import com.netflix.eureka.resources.VIPResource; +import com.netflix.eureka.resources.*; import com.netflix.eureka.transport.EurekaServerHttpClientFactory; -import com.netflix.eureka.transport.Jersey3DynamicGZIPContentEncodingFilter; import com.netflix.eureka.transport.Jersey3EurekaServerHttpClientFactory; -import com.netflix.eureka.transport.Jersey3ReplicationClient; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -64,8 +49,6 @@ import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.Path; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientRequestFilter; import jakarta.ws.rs.core.Application; import jakarta.ws.rs.ext.Provider; import org.apache.commons.logging.Log; @@ -88,11 +71,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.cloud.client.actuator.HasFeatures; -import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.netflix.eureka.EurekaConstants; import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; import org.springframework.cloud.netflix.eureka.server.*; -import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.*; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; @@ -104,11 +85,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.io.IOException; -import java.net.InetAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.UnknownHostException; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -126,8 +102,8 @@ */ @Configuration(proxyBeanMethods = false) @Import(EurekaServerInitializerConfiguration.class) -@EnableConfigurationProperties({ EurekaDashboardProperties.class, InstanceRegistryProperties.class, - EurekaProperties.class }) +@EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class, + EurekaProperties.class}) @PropertySource("classpath:/eureka/server.properties") public class EurekaConfiguration implements WebMvcConfigurer { @@ -136,7 +112,7 @@ public class EurekaConfiguration implements WebMvcConfigurer { /** * List of packages containing Jersey resources required by the Eureka server. */ - private static final String[] EUREKA_PACKAGES = new String[] { "com.netflix.discovery", "com.netflix.eureka" }; + private static final String[] EUREKA_PACKAGES = new String[]{"com.netflix.discovery", "com.netflix.eureka"}; /** * Static content pattern for dashboard elements (images, css, etc...). @@ -178,15 +154,6 @@ ServerCodecs serverCodecs() { return new CloudServerCodecs(this.eurekaServerConfig); } - private static CodecWrapper getFullJson(EurekaServerConfig serverConfig) { - CodecWrapper codec = CodecWrappers.getCodec(serverConfig.getJsonCodecName()); - return codec == null ? CodecWrappers.getCodec(JACKSON_JSON.codecName()) : codec; - } - - private static CodecWrapper getFullXml(EurekaServerConfig serverConfig) { - CodecWrapper codec = CodecWrappers.getCodec(serverConfig.getXmlCodecName()); - return codec == null ? CodecWrappers.getCodec(CodecWrappers.XStreamXml.class) : codec; - } @Bean @ConditionalOnMissingBean @@ -243,8 +210,8 @@ PeerReplicationResource peerReplicationResource() { @Bean PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs, - EurekaServerHttpClientFactory eurekaServerHttpClientFactory, - EurekaInstanceConfigBean eurekaInstanceConfigBean) { + EurekaServerHttpClientFactory eurekaServerHttpClientFactory, + EurekaInstanceConfigBean eurekaInstanceConfigBean) { if (eurekaInstanceConfigBean.isAsyncClientInitialization()) { if (log.isDebugEnabled()) { log.debug("Initializing client asynchronously..."); @@ -257,8 +224,7 @@ PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs, log.debug("Asynchronous client initialization done."); } }); - } - else { + } else { this.eurekaClient.getApplications(); // force initialization } @@ -268,31 +234,24 @@ PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs, this.instanceRegistryProperties.getDefaultOpenForTrafficCount()); } - @Bean - @ConditionalOnMissingBean - PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry, ServerCodecs serverCodecs, - ReplicationClientAdditionalFilters replicationClientAdditionalFilters) { - return new RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, - this.applicationInfoManager, replicationClientAdditionalFilters); - } - @Bean @ConditionalOnMissingBean EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs, PeerAwareInstanceRegistry registry, - PeerEurekaNodes peerEurekaNodes) { + PeerEurekaNodes peerEurekaNodes) { return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs, registry, peerEurekaNodes, this.applicationInfoManager); } @Bean EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, - EurekaServerContext serverContext) { + EurekaServerContext serverContext) { return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext); } /** * Register the Jersey filter. + * * @param eurekaJerseyApp an {@link Application} for the filter to be registered * @return a jersey {@link FilterRegistrationBean} */ @@ -309,7 +268,7 @@ FilterRegistrationBean jerseyFilterRegistration(ResourceConfig eurekaJerseyAp @Bean FilterRegistrationBean eurekaVersionFilterRegistration(ServerProperties serverProperties, - Environment env) { + Environment env) { final String contextPath = serverProperties.getServlet().getContextPath(); String regex = EurekaConstants.DEFAULT_PREFIX + STATIC_CONTENT_PATTERN; if (StringUtils.hasText(contextPath)) { @@ -368,13 +327,14 @@ public String getServletPath() { /** * Construct a Jersey {@link jakarta.ws.rs.core.Application} with all the resources * required by the Eureka server. - * @param environment an {@link Environment} instance to retrieve classpath resources + * + * @param environment an {@link Environment} instance to retrieve classpath resources * @param resourceLoader a {@link ResourceLoader} instance to get classloader from * @return created {@link Application} object */ @Bean ResourceConfig jerseyApplication(Environment environment, ResourceLoader resourceLoader, - BeanFactory beanFactory) { + BeanFactory beanFactory) { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false, environment); @@ -451,137 +411,6 @@ EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) { } - /** - * {@link PeerEurekaNodes} which updates peers when /refresh is invoked. Peers are - * updated only if eureka.client.use-dns-for-fetching-service-urls is - * false and one of following properties have changed. - *

- *

- *
    - *
  • eureka.client.availability-zones
  • - *
  • eureka.client.region
  • - *
  • eureka.client.service-url.<zone>
  • - *
- */ - static class RefreshablePeerEurekaNodes extends PeerEurekaNodes - implements ApplicationListener { - - /* for testing */ ReplicationClientAdditionalFilters replicationClientAdditionalFilters; - - RefreshablePeerEurekaNodes(final PeerAwareInstanceRegistry registry, final EurekaServerConfig serverConfig, - final EurekaClientConfig clientConfig, final ServerCodecs serverCodecs, - final ApplicationInfoManager applicationInfoManager, - final ReplicationClientAdditionalFilters replicationClientAdditionalFilters) { - super(registry, serverConfig, clientConfig, serverCodecs, applicationInfoManager); - this.replicationClientAdditionalFilters = replicationClientAdditionalFilters; - } - - @Override - protected PeerEurekaNode createPeerEurekaNode(String peerEurekaNodeUrl) { - Jersey3ReplicationClient replicationClient = createReplicationClient(serverConfig, serverCodecs, - peerEurekaNodeUrl, this.replicationClientAdditionalFilters.getFilters()); - - String targetHost = hostFromUrl(peerEurekaNodeUrl); - if (targetHost == null) { - targetHost = "host"; - } - return new PeerEurekaNode(registry, targetHost, peerEurekaNodeUrl, replicationClient, serverConfig); - } - - // FIXME: 4.0 update Jersey3ReplicationClient.createReplicationClient to handle - // additional filters - private static Jersey3ReplicationClient createReplicationClient(EurekaServerConfig config, - ServerCodecs serverCodecs, String serviceUrl, Collection additionalFilters) { - String name = Jersey3ReplicationClient.class.getSimpleName() + ": " + serviceUrl + "apps/: "; - - EurekaJersey3Client jerseyClient; - try { - String hostname; - try { - hostname = new URL(serviceUrl).getHost(); - } - catch (MalformedURLException e) { - hostname = serviceUrl; - } - - String jerseyClientName = "Discovery-PeerNodeClient-" + hostname; - EurekaJersey3ClientImpl.EurekaJersey3ClientBuilder clientBuilder = new EurekaJersey3ClientImpl.EurekaJersey3ClientBuilder() - .withClientName(jerseyClientName) - .withUserAgent("Java-EurekaClient-Replication") - .withEncoderWrapper(serverCodecs.getFullJsonCodec()) - .withDecoderWrapper(serverCodecs.getFullJsonCodec()) - .withConnectionTimeout(config.getPeerNodeConnectTimeoutMs()) - .withReadTimeout(config.getPeerNodeReadTimeoutMs()) - .withMaxConnectionsPerHost(config.getPeerNodeTotalConnectionsPerHost()) - .withMaxTotalConnections(config.getPeerNodeTotalConnections()) - .withConnectionIdleTimeout(config.getPeerNodeConnectionIdleTimeoutSeconds()); - - if (serviceUrl.startsWith("https://") && "true" - .equals(System.getProperty("com.netflix.eureka.shouldSSLConnectionsUseSystemSocketFactory"))) { - clientBuilder.withSystemSSLConfiguration(); - } - jerseyClient = clientBuilder.build(); - } - catch (Throwable e) { - throw new RuntimeException("Cannot Create new Replica Node :" + name, e); - } - - String ip = null; - try { - ip = InetAddress.getLocalHost().getHostAddress(); - } - catch (UnknownHostException e) { - log.warn("Cannot find localhost ip", e); - } - - Client jerseyApacheClient = jerseyClient.getClient(); - jerseyApacheClient.register(new Jersey3DynamicGZIPContentEncodingFilter(config)); - - for (ClientRequestFilter filter : additionalFilters) { - jerseyApacheClient.register(filter); - } - - EurekaServerIdentity identity = new EurekaServerIdentity(ip); - jerseyApacheClient.register(new EurekaIdentityHeaderFilter(identity)); - - return new Jersey3ReplicationClient(jerseyClient, serviceUrl); - } - - @Override - public void onApplicationEvent(final EnvironmentChangeEvent event) { - if (shouldUpdate(event.getKeys())) { - updatePeerEurekaNodes(resolvePeerUrls()); - } - } - - /* - * Check whether specific properties have changed. - */ - protected boolean shouldUpdate(final Set changedKeys) { - assert changedKeys != null; - - // if eureka.client.use-dns-for-fetching-service-urls is true, then - // service-url will not be fetched from environment. - if (this.clientConfig.shouldUseDnsForFetchingServiceUrls()) { - return false; - } - - if (changedKeys.contains("eureka.client.region")) { - return true; - } - - for (final String key : changedKeys) { - // property keys are not expected to be null. - if (key.startsWith("eureka.client.service-url.") - || key.startsWith("eureka.client.availability-zones.")) { - return true; - } - } - return false; - } - - } - class CloudServerCodecs extends DefaultServerCodecs { CloudServerCodecs(EurekaServerConfig serverConfig) { @@ -589,6 +418,15 @@ class CloudServerCodecs extends DefaultServerCodecs { getFullXml(serverConfig), CodecWrappers.getCodec(CodecWrappers.JacksonXmlMini.class)); } - } + private static CodecWrapper getFullJson(EurekaServerConfig serverConfig) { + CodecWrapper codec = CodecWrappers.getCodec(serverConfig.getJsonCodecName()); + return codec == null ? CodecWrappers.getCodec(JACKSON_JSON.codecName()) : codec; + } + private static CodecWrapper getFullXml(EurekaServerConfig serverConfig) { + CodecWrapper codec = CodecWrappers.getCodec(serverConfig.getXmlCodecName()); + return codec == null ? CodecWrappers.getCodec(CodecWrappers.XStreamXml.class) : codec; + } + + } } diff --git a/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java b/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java index 0502ca910f..c03cb7f680 100644 --- a/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java +++ b/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java @@ -12,10 +12,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.commons.util.InetUtils; import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @Component @ConfigurationProperties("apiml.gateway.eureka.instance") +@Primary public class GatewayEurekaInstanceConfigBean extends EurekaInstanceConfigBean { public GatewayEurekaInstanceConfigBean(InetUtils inetUtils) { diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index 90e81d5a90..c682ebcd9d 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -19,13 +19,7 @@ import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; import jakarta.annotation.PostConstruct; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; +import jakarta.servlet.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.Context; @@ -33,6 +27,7 @@ import org.apache.catalina.connector.Connector; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -40,7 +35,6 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; -import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; @@ -50,6 +44,7 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.TomcatHttpHandlerAdapter; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.web.context.ServletContextAware; import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.discovery.ApimlInstanceRegistry; @@ -57,20 +52,14 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.zaas.ZaasStartupListener; import org.zowe.apiml.zaas.security.service.JwtSecurity; import reactor.core.publisher.Flux; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Timer; -import java.util.TimerTask; +import java.util.*; @EnableScheduling @Configuration @@ -83,6 +72,7 @@ public class ModulithConfig { private final Map instances = new HashMap<>(); private final GatewayEurekaInstanceConfigBean eurekaInstanceGw; private final EurekaClientConfig eurekaConfig; + private final CachingServiceEurekaInstanceConfigBean eurekaInstanceCaching; private final Timer timer = new Timer("PeerReplicated-StaticServices"); @@ -101,8 +91,8 @@ public class ModulithConfig { @Bean ApplicationInfo applicationInfo() { return ApplicationInfo.builder() - .isModulith(true) - .authServiceId(CoreService.GATEWAY.getServiceId()).build(); + .isModulith(true) + .authServiceId(CoreService.GATEWAY.getServiceId()).build(); } private InstanceInfo getInstanceInfo(String serviceId) { @@ -122,6 +112,9 @@ private InstanceInfo getInstanceInfo(String serviceId) { metadata = eurekaInstanceGw.getMetadataMap(); metadata.put("management.port", "10010"); break; + case "cachingservice": + metadata = eurekaInstanceCaching.getMetadataMap(); + break; default: } @@ -157,19 +150,15 @@ private ApimlInstanceRegistry getRegistry() { void createLocalInstances() { instances.put(CoreService.GATEWAY.getServiceId(), getInstanceInfo(CoreService.GATEWAY.getServiceId())); instances.put(CoreService.DISCOVERY.getServiceId(), getInstanceInfo(CoreService.DISCOVERY.getServiceId())); + instances.put(CoreService.CACHING.getServiceId(), getInstanceInfo(CoreService.CACHING.getServiceId())); EurekaServerContextHolder.initialize(applicationContext.getBean(EurekaServerContext.class)); } - @EventListener - public void onApplicationEvent(EurekaRegistryAvailableEvent event) { + @EventListener(ApplicationReadyEvent.class) + public void onApplicationStart() { ApimlInstanceRegistry registry = getRegistry(); instances.forEach((key, value) -> registry.registerStatically(instances.get(key), false, CoreService.GATEWAY.getServiceId().equalsIgnoreCase(key))); - var jwtSec = applicationContext.getBean(JwtSecurity.class); - if (!jwtSec.getZosmfListener().isZosmfReady()) { - jwtSec.getZosmfListener().getZosmfRegisteredListener().onEvent(new CacheRefreshedEvent()); - } - log.info("Initialize timer for static services peer-replicated heartbeats"); // This timer calls Eureka registry's peerReplicate method to accumulate all heartbeats of statically-onboarded services once @@ -184,6 +173,17 @@ public void run() { } + @Scheduled(initialDelay = 3000, fixedRate = 20_000) // TODO find better solution but DON'T JUST REMOVE! + public void periodicJwtInit() { + var jwtSec = applicationContext.getBean(JwtSecurity.class); + if (!jwtSec.getZosmfListener().isZosmfReady()) { + jwtSec.getZosmfListener().getZosmfRegisteredListener().onEvent(new CacheRefreshedEvent()); + ZaasStartupListener listener = applicationContext.getBean(ZaasStartupListener.class); + listener.notifyStartup(); + } + } + + @Bean ReactiveDiscoveryClient registryReactiveDiscoveryClient(DiscoveryClient registryDiscoveryClient) { return new ReactiveDiscoveryClient() { @@ -257,6 +257,8 @@ MessageService messageService() { messageService.loadMessages("/gateway-log-messages.yml"); messageService.loadMessages("/zaas-log-messages.yml"); + + messageService.loadMessages("/caching-log-messages.yml"); return messageService; } diff --git a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java index 7103239f22..47e0099583 100644 --- a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java @@ -32,18 +32,14 @@ import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; -import org.zowe.apiml.filter.BasicLoginFilter; -import org.zowe.apiml.filter.CategorizeCertsWebFilter; -import org.zowe.apiml.filter.LogoutHandler; -import org.zowe.apiml.filter.QueryWebFilter; -import org.zowe.apiml.filter.X509AuthFilter; +import org.zowe.apiml.filter.*; import org.zowe.apiml.gateway.filters.security.AuthExceptionHandlerReactive; import org.zowe.apiml.gateway.filters.security.TokenAuthFilter; -import org.zowe.apiml.security.common.util.X509Util; import org.zowe.apiml.handler.FailedAuthenticationWebHandler; import org.zowe.apiml.handler.LocalTokenProvider; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.util.X509Util; import org.zowe.apiml.security.common.verify.CertificateValidator; import org.zowe.apiml.util.HttpUtils; import org.zowe.apiml.zaas.security.config.CompoundAuthProvider; @@ -53,9 +49,7 @@ import java.util.List; import java.util.Set; -import static org.springframework.http.HttpMethod.DELETE; -import static org.springframework.http.HttpMethod.GET; -import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.*; import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; import static org.zowe.apiml.gateway.services.ServicesInfoController.SERVICES_FULL_URL; import static org.zowe.apiml.gateway.services.ServicesInfoController.SERVICES_SHORT_URL; @@ -97,14 +91,14 @@ public class WebSecurityConfig { private int internalDiscoveryPort; private static final List UNAUTHENTICATED_PATTERNS = List.of( - "/application/", - "/application/version", - "/eureka/css/**", - "/eureka/js/**", - "/eureka/fonts/**", - "/eureka/images/**", - APPLICATION_INFO, - "/favicon.ico"); + "/application/", + "/application/version", + "/eureka/css/**", + "/eureka/js/**", + "/eureka/fonts/**", + "/eureka/images/**", + APPLICATION_INFO, + "/favicon.ico"); private final ServerWebExchangeMatcher discoveryPortMatcher = exchange -> exchange.getRequest().getURI().getPort() == internalDiscoveryPort ? MatchResult.match() : MatchResult.notMatch(); private final ServerWebExchangeMatcher isInUnauthenticatedPaths = pathMatchers(UNAUTHENTICATED_PATTERNS.toArray(new String[]{})); @@ -165,7 +159,7 @@ SecurityWebFilterChain discoveryServiceClientCertificateFilterChain(ServerHttpSe @Bean @Order(1) SecurityWebFilterChain discoveryServiceBasicAuthOrTokenOrCertFilterChain(ServerHttpSecurity http, - AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { + AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { http .securityMatcher(new AndServerWebExchangeMatcher( discoveryPortMatcher, @@ -243,15 +237,15 @@ SecurityWebFilterChain discoveryAllowedEndpoints(ServerHttpSecurity http) { * This one applies independently of connector, it covers /application/health in both GW and DS * * @param http - * @param authConfigurationProperties Obtain auth configuration such as auth cookie name + * @param authConfigurationProperties Obtain auth configuration such as auth cookie name * @param authExceptionHandlerReactive Exception handler * @return The configured {@link SecurityWebFilterChain} to optionally protect /application/health path */ @Bean @Order(2) SecurityWebFilterChain healthEndpointFilterChain(ServerHttpSecurity http, - AuthConfigurationProperties authConfigurationProperties, - AuthExceptionHandlerReactive authExceptionHandlerReactive) { + AuthConfigurationProperties authConfigurationProperties, + AuthExceptionHandlerReactive authExceptionHandlerReactive) { http .securityMatcher(pathMatchers(APPLICATION_HEALTH)) .csrf(ServerHttpSecurity.CsrfSpec::disable) @@ -307,8 +301,8 @@ SecurityWebFilterChain applicationEndpointsProtected(ServerHttpSecurity http, } /** - * Filter chain for protecting endpoints with MF credentials (basic or token) - */ + * Filter chain for protecting endpoints with MF credentials (basic or token) + */ @Bean @Order(10) SecurityWebFilterChain discoveryBasicAuthOrToken(ServerHttpSecurity http, @@ -381,15 +375,14 @@ SecurityWebFilterChain queryFilter(ServerHttpSecurity http) { /** * Secures endpoints: - * - /auth/access-token/generate + * - /auth/access-token/generate *

* Requires authentication by a client certificate or basic authentication, supports credentials in header and body. * The request is fulfilled by the filter chain only, there is no controller to handle it. * Order of custom filters: - * - CategorizeCertsWebFilter - checks for forwarded client certificate and put it into a custom request attribute - * - X509AuthFilter - attempts to log in a user using forwarded client certificate, generates access token and stops the chain on success, reply with the token - * - ShouldBeAlreadyAuthenticatedFilter - stops filter chain if none of the authentications was successful - * + * - CategorizeCertsWebFilter - checks for forwarded client certificate and put it into a custom request attribute + * - X509AuthFilter - attempts to log in a user using forwarded client certificate, generates access token and stops the chain on success, reply with the token + * - ShouldBeAlreadyAuthenticatedFilter - stops filter chain if none of the authentications was successful */ @Bean SecurityWebFilterChain accessTokenFilter(ServerHttpSecurity http) { @@ -408,24 +401,23 @@ SecurityWebFilterChain accessTokenFilter(ServerHttpSecurity http) { /** * Secures endpoints: - * - /auth/access-token/revoke/tokens/** - * - /auth/access-token/evict + * - /auth/access-token/revoke/tokens/** + * - /auth/access-token/evict *

* Requires authentication by a client certificate forwarded form Gateway or basic authentication, supports only credentials in header. * Order of custom filters: - * - CategorizeCertsWebFilter - checks for forwarded client certificate and put it into a custom request attribute - * - X509AuthFilter - attempts to log in using a user using forwarded client certificate, replaces pre-authentication in security context by the authentication result - * - BasicLoginFilter - attempts to log in a user using credentials from basic authentication header + * - CategorizeCertsWebFilter - checks for forwarded client certificate and put it into a custom request attribute + * - X509AuthFilter - attempts to log in using a user using forwarded client certificate, replaces pre-authentication in security context by the authentication result + * - BasicLoginFilter - attempts to log in a user using credentials from basic authentication header */ @Bean - SecurityWebFilterChain revokeTokenFilterChain(ServerHttpSecurity http, - AuthConfigurationProperties authConfigurationProperties, - AuthExceptionHandlerReactive authExceptionHandlerReactive) { + SecurityWebFilterChain revokeTokenFilterChain(ServerHttpSecurity http) { var man = new ProviderManager(x509AuthenticationProvider); var reactiveX509provider = new ReactiveAuthenticationManagerAdapter(man); - - return x509SecurityConfig(http) - .securityMatcher(pathMatchers("/gateway/api/v1/auth/access-token/revoke/tokens/**")) + return http + .headers(customizer -> customizer.frameOptions(ServerHttpSecurity.HeaderSpec.FrameOptionsSpec::disable)) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .securityMatcher(pathMatchers("/gateway/api/v1/auth/access-token/revoke/tokens/**", "/gateway/api/v1/auth/access-token/evict")) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .addFilterAfter(new CategorizeCertsWebFilter(publicKeyCertificatesBase64, certificateValidator), SecurityWebFiltersOrder.FIRST) @@ -434,6 +426,7 @@ SecurityWebFilterChain revokeTokenFilterChain(ServerHttpSecurity http, .build(); } + /** * This security filter chain secures the /refresh access token (PAT) endpoint * diff --git a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java index 14acd6d03e..3bb4b50905 100644 --- a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java +++ b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java @@ -10,7 +10,6 @@ package org.zowe.apiml; -import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpUpgradeHandler; @@ -23,11 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.ClientResponse; import org.zowe.apiml.constants.ApimlConstants; -import org.zowe.apiml.gateway.filters.AbstractAuthSchemeFactory; -import org.zowe.apiml.gateway.filters.ErrorHeaders; -import org.zowe.apiml.gateway.filters.RequestCredentials; -import org.zowe.apiml.gateway.filters.ZaasInternalErrorException; -import org.zowe.apiml.gateway.filters.ZaasSchemeTransform; +import org.zowe.apiml.gateway.filters.*; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; @@ -41,14 +36,9 @@ import reactor.core.publisher.Mono; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Optional; +import java.util.*; import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; import static org.zowe.apiml.security.common.filter.CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE; @@ -129,7 +119,7 @@ public Mono> pas } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createInvalidAuthenticationErrorMessage(); + return createMissingAuthenticationErrorMessage(); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -141,7 +131,7 @@ public Mono> pas return Mono.error(new ZaasInternalErrorException(currentApimlId, e.getMessage())); } catch (Exception e) { log.debug("Token has expired", e); - return createErrorMessage(e.getMessage()); + return createInvalidAuthenticationErrorMessage(); } } @@ -149,7 +139,7 @@ private void updateServiceId(Optional authSource, RequestCredentials authSource .filter(PATAuthSource.class::isInstance) .map(PATAuthSource.class::cast) - .filter(as -> as.getDefaultServiceId() == null) + .filter(as -> StringUtils.isBlank(as.getDefaultServiceId())) .ifPresent(as -> as.setDefaultServiceId(request.getServiceId())); } @@ -168,7 +158,7 @@ public Mono> } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createInvalidAuthenticationErrorMessage(); + return createMissingAuthenticationErrorMessage(); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -191,7 +181,7 @@ public Mono> } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createInvalidAuthenticationErrorMessage(); + return createMissingAuthenticationErrorMessage(); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -213,7 +203,7 @@ public Mono> } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createInvalidAuthenticationErrorMessage(); + return createMissingAuthenticationErrorMessage(); } var token = authSourceService.getJWT(authSource.get()); var response = ZaasTokenResponse.builder().cookieName(COOKIE_AUTH_NAME).token(token).build(); @@ -293,8 +283,8 @@ public String getRequestURI() { } @Override - public T upgrade(Class handlerClass) throws IOException, ServletException { - return request.upgrade(handlerClass); + public T upgrade(Class handlerClass) { + throw new UnsupportedOperationException(); } interface Exclude { 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 effe13e562..388938edbc 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.i18n.LocaleContextResolver; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.gateway.controllers.GatewayExceptionHandler; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.log.ApimlLogger; @@ -102,6 +103,12 @@ public Mono handleBadCredentialsException(ServerWebExchange exchange, BadC return setBodyResponse(exchange, SC_UNAUTHORIZED, "org.zowe.apiml.security.login.invalidCredentials"); } + @ExceptionHandler(StorageException.class) + public Mono handleStorageException(ServerWebExchange exchange, StorageException ex) { + log.debug("Incompatible storage option: {}", ex.getMessage()); + return setBodyResponse(exchange, ex.getStatus().value(), ex.getKey(), (Object[]) ex.getParameters()); + } + @ExceptionHandler(ZosAuthenticationException.class) public Mono handleZosAuthenticationException(ServerWebExchange exchange, ZosAuthenticationException ex) { log.debug("Zos Authentication Exception: {}", ex.getMessage()); diff --git a/apiml/src/main/java/org/zowe/apiml/controller/ReactivePATController.java b/apiml/src/main/java/org/zowe/apiml/controller/ReactivePATController.java index f1c0f108a4..0b99f55120 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ReactivePATController.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ReactivePATController.java @@ -30,11 +30,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.security.common.audit.RauditxService; @@ -50,9 +46,7 @@ import java.util.Set; import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; -import static org.zowe.apiml.zaas.controllers.AuthController.ACCESS_TOKEN_REVOKE; -import static org.zowe.apiml.zaas.controllers.AuthController.ACCESS_TOKEN_REVOKE_MULTIPLE; -import static org.zowe.apiml.zaas.controllers.AuthController.ACCESS_TOKEN_VALIDATE; +import static org.zowe.apiml.zaas.controllers.AuthController.*; @RestController @RequestMapping("/gateway/api/v1/auth") @@ -139,6 +133,43 @@ public Mono> generatePat(@RequestBody AccessTokenRequest .switchIfEmpty(Mono.just(ResponseEntity.status(HttpStatusCode.valueOf(401)).build())); } + @DeleteMapping(value = ACCESS_TOKEN_EVICT) + @Operation(summary = "Remove invalidated tokens and rules which are not relevant anymore.", + tags = {"Access token"}, + description = """ + Will evict all the invalidated tokens which are not relevant anymore + + **Request:** + + The evict requires the user credentials in one of the following formats: + + * Basic authentication + * Client certificate + + **Response:** + + The response is no content. + """, + + operationId = "accessTokensInvalidateAdminScopeDELETE", + security = { + @SecurityRequirement(name = "Bearer"), + @SecurityRequirement(name = "CookieAuth"), + @SecurityRequirement(name = "LoginBasicAuth"), + @SecurityRequirement(name = "ClientCert") + } + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Successfully evicted") + }) + @PreAuthorize("@safMethodSecurityExpressionRoot.hasSafServiceResourceAccess('SERVICES', 'UPDATE',#root)") + public Mono> evictNonRelevantTokensAndRules() { + return Mono.fromCallable(() -> { + tokenProvider.evictNonRelevantTokensAndRules(); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + }); + } + /** * Validates whether a personal access token is currently valid and authorized for the specified service ID. * The request must contain a valid token and the associated service ID. If the token is valid and has not been @@ -161,7 +192,13 @@ public Mono> generatePat(@RequestBody AccessTokenRequest @Operation(summary = "Validate personal access token.", tags = {"Access token"}, operationId = "accessTokenValidatePOST", - description = "Use the `/access-token/validate` API to verify that personal access token is valid. \n\n**Response:**\n\nThe response is a plain text body.", + description = """ + Use the `/access-token/validate` API to verify that personal access token is valid. + + **Response:** + + The response is a plain text body. + """, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( content = @Content( schema = @Schema(implementation = ValidateRequestModel.class) @@ -203,7 +240,21 @@ public Mono> validateAccessToken(@RequestBody ValidateReq @Operation(summary = "Invalidate multiple personal access tokens.", tags = {"Access token"}, operationId = "accessTokensInvalidateDELETE", - description = "Use the `/access-token/revoke/token` API to invalidate multiple personal access tokens issued for your user ID. \n\n**Request:**\n\nThe revoke request requires the user credentials in one of the following formats:\n * Cookie named `apimlAuthenticationToken`.\n * Bearer authentication \n*Header example:* Authorization: Bearer *token* \n* Client certificate \n\n**Response:**\n\nThe response is no content.", + description = """ + Use the `/access-token/revoke/token` API to invalidate multiple personal access tokens issued for your user ID. + + **Request:** + + The revoke request requires the user credentials in one of the following formats: + * Cookie named `apimlAuthenticationToken`. + * Bearer authentication + *Header example:* Authorization: Bearer *token* + * Client certificate + + **Response:** + + The response is no content. + """, security = { @SecurityRequirement(name = "Bearer"), @SecurityRequirement(name = "CookieAuth"), @@ -260,7 +311,13 @@ public Mono> revokeAllUserAccessTokens(@RequestBody(requi summary = "Invalidate personal access token.", tags = {"Access token"}, operationId = "accessTokenInvalidateDELETE", - description = "Use the `/access-token/revoke` API to invalidate a specific personal access token. \n\n**Response:**\n\nThe response is no content.", + description = """ + Use the `/access-token/revoke` API to invalidate a specific personal access token. + + **Response:** + + The response is no content. + """, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( content = @Content( schemaProperties = { @@ -322,7 +379,19 @@ public Mono> revokeAccessToken(@RequestBody Mono> revokeAccessTokensForUser(@RequestBody Rules @Operation(summary = "Invalidate multiple personal access tokens by service ID.", tags = {"Access token"}, operationId = "accessTokensInvalidateAdminScopeDELETE", - description = "Use the `/access-token/revoke/token/scope` API to invalidate multiple personal access tokens issued for service ID.\n\n**Request:**\n\nThe revoke scope request requires the user credentials in one of the following formats:\n\n* Basic authentication\n* Client certificate \n\n**Response:**\n\nThe response is no content.", + description = """ + Use the `/access-token/revoke/token/scope` API to invalidate multiple personal access tokens issued for service ID. + + **Request:** + + The revoke scope request requires the user credentials in one of the following formats: + * Basic authentication + * Client certificate + + **Response:** + + The response is no content. + """, security = { @SecurityRequirement(name = "Bearer"), @SecurityRequirement(name = "CookieAuth"), diff --git a/apiml/src/main/resources/apiml-log-messages.yml b/apiml/src/main/resources/apiml-log-messages.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 327720f3c9..2ced71cae0 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -13,7 +13,7 @@ eureka: server: max-threads-for-peer-replication: 2 useReadOnlyResponseCache: false - peer-node-read-timeout-ms: 5000 + peer-node-read-timeout-ms: 10000 spring: cloud: gateway: @@ -44,9 +44,20 @@ spring: springdoc: api-docs: - path: /gateway/api-docs + path: /v3/api-docs + groups: + enabled: true writer-with-order-by-keys: true - pathsToMatch: /gateway/api/v1/** + group-configs: + - group: gateway + pathsToMatch: /gateway/api/v1/** + - group: cachingservice + paths-to-match: /cachingservice/api/v1/** + - group: discovery + paths-to-match: /discovery/api/v1/** + - group: zaas + paths-to-match: /zaas/api/v1/** + apiml: catalog: serviceId: apicatalog @@ -56,7 +67,7 @@ apiml: # The `apiml.discovery` node contains discovery-service only configuration userid: eureka # Userid that Eureka server will use to check authentication of its clients (other services) password: password # Password that Eureka server will use to check authentication of its clients (other services) - allPeersUrls: http://${apiml.discovery.userid}:${apiml.discovery.password}@${apiml.service.hostname}:${apiml.service.port}/eureka/ + allPeersUrls: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/eureka/ gateway: eureka: @@ -91,7 +102,7 @@ apiml: - apiId: zowe.apiml.gateway version: 1.0.0 gatewayUrl: api/v1 - swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/gateway/api-docs + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/v3/api-docs/gateway documentationUrl: https://zowe.github.io/docs-site/ service: title: API Gateway @@ -104,6 +115,35 @@ apiml: registry: enabled: false metadata-key-allow-list: zos.sysname,zos.system,zos.sysplex,zos.cpcName,zos.zosName,zos.lpar + caching: + eureka: + instance: + metadata-map: + apiml: + registrationType: primary + apiBasePath: /cachingservice/api/v1 + catalog: + tile: + id: apimediationlayer + title: API Mediation Layer API + description: The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation. + version: 1.0.0 + routes: + api_v1: + gatewayUrl: / + serviceUrl: /cachingservice + apiInfo: + - apiId: zowe.apiml.cachingservice + version: 1.0.0 + gatewayUrl: api/v1 + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/v3/api-docs/cachingservice + documentationUrl: https://zowe.github.io/docs-site/ + service: + title: Caching service for internal usage. + description: Service that provides caching API. + authentication: + sso: true + connection: timeout: 60000 idleConnectionTimeoutSeconds: 5 @@ -179,7 +219,15 @@ server: trustStorePassword: password trustStoreType: PKCS12 - +caching: + storage: + mode: infinispan + infinispan: + initialHosts: localhost[7099] +jgroups: + bind: + port: 7600 + address: localhost logbackServiceName: ZWEAGW1 logging: @@ -191,12 +239,13 @@ logging: com.netflix.discovery: ERROR com.netflix.discovery.DiscoveryClient: OFF com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient: OFF + com.netflix.eureka: ERROR com.sun.jersey.server.impl.application.WebApplicationImpl: WARN io.netty.resolver.dns: WARN io.netty.util.internal.MacAddressUtil: ERROR # Can print WARN not finding usable MAC Address javax.net.ssl: ERROR org.apache: WARN #org.apache.catalina, org.apache.coyote, org.apache.tomcat - org.apache.http.conn.ssl.DefaultHostnameVerifier: DEBUG #logs only SSLException + org.apache.http.conn.ssl.DefaultHostnameVerifier: WARN #logs only SSLException org.apache.tomcat.util.net.SSLUtilBase: ERROR org.eclipse.jetty: WARN org.ehcache: WARN @@ -211,13 +260,14 @@ logging: org.springframework.context.support.[PostProcessorRegistrationDelegate$BeanPostProcessorChecker]: ERROR org.springframework.http.server.reactive: INFO org.springframework.security.config.annotation.web.builders.WebSecurity: ERROR - org.springframework.security.web: INFO - org.springframework.web.reactive: INFO + org.springframework.security.web: WARN + org.springframework.web.reactive: WARN org.zowe.apiml: INFO - reactor.netty: DEBUG + reactor.netty: WARN reactor.netty.http.client: INFO reactor.netty.http.client.HttpClientConnect: OFF - org.springframework.security: TRACE + org.infinispan: WARN + org.jgroups: WARN management: endpoint: @@ -241,6 +291,7 @@ logging: com.netflix: INFO # Update to DEBUG com.netflix.eureka: DEBUG com.netflix.discovery.shared.transport.decorator: DEBUG + com.netflix.eureka.cluster: DEBUG com.sun.jersey.server.impl.application.WebApplicationImpl: INFO javax.net.ssl: ERROR org.apache: INFO @@ -248,7 +299,7 @@ logging: org.apache.tomcat.util.net: DEBUG org.apache.tomcat.util.net.jsse.JSSESupport: INFO org.ehcache: INFO - org.springframework: INFO + org.springframework: DEBUG # org.springframework.cloud.gateway: TRACE # org.springframework.cloud.gateway.filter: TRACE # org.springframework.cloud.gateway.route: TRACE @@ -258,9 +309,12 @@ logging: # org.springframework.web.reactive: DEBUG # org.springframework.web.reactive.socket: DEBUG org.zowe.apiml: DEBUG + reactor.netty: DEBUG reactor.netty.http.client: DEBUG reactor.netty.http.client.HttpClient: DEBUG reactor.netty.http.client.HttpClientConnect: DEBUG + org.jgroups: DEBUG + org.infinispan: DEBUG --- spring.config.activate.on-profile: attls diff --git a/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java b/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java index c05c72bb6f..b4f6f47d89 100644 --- a/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java +++ b/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java @@ -28,17 +28,11 @@ import reactor.test.StepVerifier; import javax.management.ServiceNotFoundException; - import java.util.Map; import java.util.Optional; -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.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.zowe.apiml.constants.ApimlConstants.AUTH_FAIL_HEADER; @@ -50,7 +44,7 @@ class ZaasSchemeTransformApiTest { private ZaasSchemeTransformApi transformApi; TokenCreationService tokenCreationService; ZosmfService zosmfService; - RequestCredentials requestCredentials; + RequestCredentials credentials; AuthSource authSource; private static MessageService messageService; @@ -60,8 +54,8 @@ static void messageService() { messageService.loadMessages("/zaas-log-messages.yml"); } - private static final String INVALID_AUTH_MSG = "ZWEAO402E The request has not been applied because it lacks valid authentication credentials."; - private static final String MISSING_AUTH_MSG = "ZWEAG160E No authentication provided in the request"; + private final String MISSING_AUTH_MSG = "ZWEAG160E No authentication provided in the request"; + private final String INVALID_AUTH_MSG = "ZWEAO402E The request has not been applied because it lacks valid authentication credentials."; @BeforeEach void setUp() { @@ -70,7 +64,7 @@ void setUp() { tokenCreationService = mock(TokenCreationService.class); zosmfService = mock(ZosmfService.class); passTicketService = mock(PassTicketService.class); - requestCredentials = mockCredentials(); + credentials = mockCredentials(); authSource = mock(AuthSource.class); transformApi = new ZaasSchemeTransformApi( authSourceService, @@ -102,7 +96,7 @@ void thenReturnsExpectedTicket() throws PassTicketException { when(passTicketService.generate("USER1", "app1")).thenReturn("ticket123"); - StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { assertNotNull(result); TicketResponse response = result.getBody(); assertNotNull(response); @@ -117,8 +111,8 @@ void whenTicketGenerationFails_writeErrorHeader() throws PassTicketException { when(passTicketService.generate("USER1", "app1")).thenThrow(new RuntimeException("boom")); - StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { - assertEquals("boom", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); }).verifyComplete(); } } @@ -146,16 +140,16 @@ void whenAuthSourceInvalid_writeErrorHeader() throws PassTicketException { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); when(authSourceService.isValid(authSource)).thenReturn(false); - StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { - assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); } @Test void whenApplicationNameIsMissing_inPassticket_thenReturnsError() { - when(requestCredentials.getApplId()).thenReturn(null); - StepVerifier.create(transformApi.passticket(requestCredentials)).assertNext(result -> { + when(credentials.getApplId()).thenReturn(null); + StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -171,9 +165,9 @@ class GivenSafIdtScheme { @Test void whenMissingAppId_returnsError() { - when(requestCredentials.getApplId()).thenReturn(null); + when(credentials.getApplId()).thenReturn(null); - StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -198,7 +192,7 @@ void whenValidUser_returnsToken() throws PassTicketException { when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) .thenReturn("saf-idt"); - StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { assertNotNull(result); assertEquals("saf-idt", result.getBody().getToken()); }).verifyComplete(); @@ -210,7 +204,7 @@ void whenSafIdTokenCreationFails_returnsError() { when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) .thenThrow(new RuntimeException("Simulated SAF IDT failure")); - StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { assertEquals("Simulated SAF IDT failure", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -226,8 +220,8 @@ void whenAuthSourceInvalid_returnsError() { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); when(authSourceService.isValid(authSource)).thenReturn(false); - StepVerifier.create(transformApi.safIdt(requestCredentials)).assertNext(result -> { - assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); + StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); } @@ -268,7 +262,7 @@ void thenReturnsJwt() { when(authSourceService.getJWT(authSource)).thenReturn("jwt-token"); - StepVerifier.create(transformApi.zoweJwt(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> { assertNotNull(result); ZaasTokenResponse response = result.getBody(); assertNotNull(response); @@ -280,7 +274,7 @@ void thenReturnsJwt() { void whenJwtRetrievalFails_returnsErrorResponse() { when(authSourceService.getJWT(authSource)).thenThrow(new RuntimeException("boom")); - StepVerifier.create(transformApi.zoweJwt(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> { assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -292,7 +286,7 @@ void whenMissingAuthSource_returnsError() { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); - StepVerifier.create(transformApi.zoweJwt(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> { assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -312,7 +306,7 @@ class GivenValidAuth { ZosmfService zosmfService; @BeforeEach - void setup() { + public void setup() { parsed = mock(AuthSource.Parsed.class); zosmfService = mock(ZosmfService.class); @@ -338,7 +332,7 @@ void whenValidAuthSource_returnsTokenResponse() throws ServiceNotFoundException when(zosmfService.exchangeAuthenticationForZosmfToken(anyString(), eq(parsed))) .thenReturn(mockResponse); - StepVerifier.create(transformApi.zosmf(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> { assertNotNull(result); assertEquals("zosmf-token", result.getBody().getToken()); @@ -350,7 +344,7 @@ void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundExcep when(zosmfService.exchangeAuthenticationForZosmfToken(any(), any())) .thenThrow(new RuntimeException("Error returned from zosmf")); - StepVerifier.create(transformApi.zosmf(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> { assertEquals("Error returned from zosmf", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -361,7 +355,7 @@ void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundExcep void whenAuthSourceMissing_returnsMissingAuthError() { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); - StepVerifier.create(transformApi.zosmf(requestCredentials)).assertNext(result -> { + StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> { assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java b/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java index c8a555d7c0..414bca3768 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/ReactiveAuthenticationControllerTests.java @@ -10,9 +10,14 @@ package org.zowe.apiml.acceptance; +import io.restassured.http.ContentType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.zowe.apiml.enable.register.RegisterToApiLayer; import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.config.SslContextConfigurer; @@ -22,11 +27,14 @@ import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; @AcceptanceTest +@Import(ReactiveAuthenticationControllerTests.MockRegisterToApiLayerConfig.class) class ReactiveAuthenticationControllerTests extends AcceptanceTestWithMockServices { private static final String REFRESH_ENDPOINT = "/gateway/api/v1/auth/refresh"; + private static final String LOGIN_ENDPOINT = "/gateway/api/v1/auth/login"; private static final String AUTH_COOKIE = "apimlAuthenticationToken"; private static final String DISTRIBUTE_INVALIDATE_ENDPOINT = "/gateway/api/v1/auth/distribute"; private static final String INVALIDATE_JWT_ENDPOINT = "/gateway/api/v1/auth/invalidate"; @@ -47,6 +55,7 @@ void setUp() throws Exception { private String login() { return given() + .contentType(ContentType.JSON) .body(""" { "username": "USER", @@ -55,7 +64,7 @@ private String login() { """) .log().all() .when() - .post(URI.create(basePath + "/gateway/api/v1/auth/login")) + .post(URI.create(basePath + LOGIN_ENDPOINT)) .then() .statusCode(204) .cookie(AUTH_COOKIE) @@ -161,5 +170,11 @@ void whenInvalidateJwt_withCert_thenSuccess() { .then() .statusCode(200); } - + @TestConfiguration + public static class MockRegisterToApiLayerConfig { + @Bean + public RegisterToApiLayer registerToApiLayer() { + return mock(RegisterToApiLayer.class); + } + } } diff --git a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePATControllerTest.java b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePATControllerTest.java index 1dd54ff232..871b179f4c 100644 --- a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePATControllerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePATControllerTest.java @@ -362,4 +362,15 @@ void revokeAccessTokensForScope_nullServiceId() throws JsonProcessingException { verify(messageService).createMessage("org.zowe.apiml.security.query.invalidRevokeRequestBody"); } + @Test + void evictNonRelevantTokensAndRules_success() { + Mono> result = controller.evictNonRelevantTokensAndRules(); + + StepVerifier.create(result) + .expectNextMatches(response -> HttpStatus.NO_CONTENT.equals(response.getStatusCode())) + .verifyComplete(); + + verify(tokenProvider, times(1)).evictNonRelevantTokensAndRules(); + } + } diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml index 4afbd8913b..699114858b 100644 --- a/apiml/src/test/resources/application.yml +++ b/apiml/src/test/resources/application.yml @@ -101,6 +101,15 @@ apiml: urls: authenticate: https://mock-services:10013/zss/saf/authenticate verify: https://localhost:10013/zss/saf/verify +caching: + storage: + mode: infinispan + infinispan: + initialHosts: localhost[7099] +jgroups: + bind: + port: 7600 + address: localhost server: port: ${apiml.service.port} diff --git a/build.gradle b/build.gradle index 5134886fe1..d9daf46254 100644 --- a/build.gradle +++ b/build.gradle @@ -261,6 +261,10 @@ task runInfinispanServiceTests(dependsOn: ":integration-tests:runInfinispanServi description "Run tests for caching service with infinispan storage option" group "Integration tests" } +task runModulithInfinispanServiceTests(dependsOn: ":integration-tests:runModulithInfinispanServiceTests") { + description "Run tests for APIML with infinispan storage option" + group "Integration tests" +} task runHATests(dependsOn: ":integration-tests:runHATests") { description "Run HA tests only" group "Integration tests" diff --git a/caching-service/build.gradle b/caching-service/build.gradle index 9050e9b349..02c3ad345e 100644 --- a/caching-service/build.gradle +++ b/caching-service/build.gradle @@ -58,11 +58,13 @@ configurations.all { dependencies { api project(':apiml-tomcat-common') api project(':onboarding-enabler-spring') + api project(':apiml-security-common') implementation libs.spring.boot.starter.aop + implementation libs.spring.boot.starter.webflux implementation libs.spring.boot.starter.security implementation libs.spring.boot.starter.actuator - implementation libs.spring.doc + implementation libs.spring.doc.webflux.ui implementation libs.spring.retry implementation libs.bundles.infinispan @@ -76,7 +78,7 @@ dependencies { testImplementation libs.spring.boot.starter.test testImplementation libs.spring.boot.starter testImplementation libs.rest.assured - + testImplementation libs.reactor.test compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java b/caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java index 6cc803aa7b..a377538c61 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java @@ -14,61 +14,66 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.SslInfo; import org.springframework.web.bind.annotation.*; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.Messages; -import org.zowe.apiml.caching.service.Storage; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.message.core.Message; import org.zowe.apiml.message.core.MessageService; - -import jakarta.servlet.http.HttpServletRequest; +import reactor.core.publisher.Mono; import java.util.Optional; @Slf4j @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1") +@RequestMapping("/cachingservice/api/v1") public class CachingController { private final Storage storage; private final MessageService messageService; + @Autowired(required = false) + ApplicationInfo applicationInfo; + - @GetMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = {"/cache", "/cache/"}, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Retrieves all values in the cache", description = "Values returned for the calling service") @ResponseBody - public ResponseEntity getAllValues(HttpServletRequest request) { - return getServiceId(request).>map( + public Mono> getAllValues(ServerHttpRequest request) { + return Mono.fromCallable(() -> getServiceId(request).>map( s -> { try { return new ResponseEntity<>(storage.readForService(s), HttpStatus.OK); } catch (Exception exception) { - return handleInternalError(exception, request.getRequestURL()); + return handleInternalError(exception, request); } } - ).orElseGet(this::getUnauthorizedResponse); + ).orElseGet(this::getUnauthorizedResponse)); } - @DeleteMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_VALUE) + @DeleteMapping(value = {"/cache", "/cache/"}, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Delete all values for service from the cache", description = "Will delete all key-value pairs for specific service") - @ResponseBody - public ResponseEntity deleteAllValues(HttpServletRequest request) { - return getServiceId(request).map( + public Mono> deleteAllValues(ServerHttpRequest request) { + return Mono.fromCallable(() -> getServiceId(request).map( s -> { try { storage.deleteForService(s); return new ResponseEntity<>(HttpStatus.OK); } catch (Exception exception) { - return handleInternalError(exception, request.getRequestURL()); + return handleInternalError(exception, request); } } - ).orElseGet(this::getUnauthorizedResponse); + ).orElseGet(this::getUnauthorizedResponse)); } private ResponseEntity getUnauthorizedResponse() { @@ -81,115 +86,109 @@ private ResponseEntity getUnauthorizedResponse() { @Operation(summary = "Retrieves a specific value in the cache", description = "Value returned is for the provided {key}") @ResponseBody - public ResponseEntity getValue(@PathVariable String key, HttpServletRequest request) { - return keyRequest(storage::read, - key, request, HttpStatus.OK); + public Mono> getValue(@PathVariable String key, ServerHttpRequest request) { + return Mono.fromCallable(() -> keyRequest(storage::read, + key, request, HttpStatus.OK)); } @DeleteMapping(value = "/cache/{key}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Delete key from the cache", description = "Will delete key-value pair for the provided {key}") - @ResponseBody - public ResponseEntity delete(@PathVariable String key, HttpServletRequest request) { - return keyRequest(storage::delete, - key, request, HttpStatus.NO_CONTENT); + public Mono> delete(@PathVariable String key, ServerHttpRequest request) { + return Mono.fromCallable(() -> keyRequest(storage::delete, + key, request, HttpStatus.NO_CONTENT)); } - @PostMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = {"/cache", "/cache/"}, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Create a new key in the cache", description = "A new key-value pair will be added to the cache") - @ResponseBody - public ResponseEntity createKey(@RequestBody KeyValue keyValue, HttpServletRequest request) { - return keyValueRequest(storage::create, - keyValue, request, HttpStatus.CREATED); + public Mono> createKey(@RequestBody KeyValue keyValue, ServerHttpRequest request) { + return Mono.fromCallable(() -> keyValueRequest(storage::create, + keyValue, request, HttpStatus.CREATED)); } @PostMapping(value = "/cache-list/{mapKey}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Add a new item in the cache map", description = "A new key-value pair will be added to the specific cache map with given map key.") - @ResponseBody - public ResponseEntity storeMapItem(@PathVariable String mapKey, @RequestBody KeyValue keyValue, HttpServletRequest request) { - return mapKeyValueRequest(storage::storeMapItem, - mapKey, keyValue, request, HttpStatus.CREATED); + public Mono> storeMapItem(@PathVariable String mapKey, @RequestBody KeyValue keyValue, ServerHttpRequest request) { + return Mono.fromCallable(() -> mapKeyValueRequest(storage::storeMapItem, + mapKey, keyValue, request, HttpStatus.CREATED)); } @GetMapping(value = "/cache-list/{mapKey}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Retrieves all the items in the cache map", description = "Values returned for the calling service and specific cache map.") @ResponseBody - public ResponseEntity getAllMapItems(@PathVariable String mapKey, HttpServletRequest request) { - return getServiceId(request).>map( + public Mono> getAllMapItems(@PathVariable String mapKey, ServerHttpRequest request) { + return Mono.fromCallable(() -> getServiceId(request).>map( s -> { log.debug("Storing for serviceId: {}", s); try { return new ResponseEntity<>(storage.getAllMapItems(s, mapKey), HttpStatus.OK); } catch (Exception exception) { - return handleIncompatibleStorageMethod(exception, request.getRequestURL()); + return handleIncompatibleStorageMethod(exception, request); } } - ).orElseGet(this::getUnauthorizedResponse); + ).orElseGet(this::getUnauthorizedResponse)); } - @GetMapping(value = "/cache-list", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = {"/cache-list", "/cache-list/"}, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Retrieves all the maps in the cache", description = "Values returned for the calling service") @ResponseBody - public ResponseEntity getAllMaps(HttpServletRequest request) { - return getServiceId(request).>map( + public Mono> getAllMaps(ServerHttpRequest request) { + return Mono.fromCallable(() -> getServiceId(request).>map( s -> { log.debug("Get all for serviceId: {}", s); try { return new ResponseEntity<>(storage.getAllMaps(s), HttpStatus.OK); } catch (Exception exception) { - return handleIncompatibleStorageMethod(exception, request.getRequestURL()); + return handleIncompatibleStorageMethod(exception, request); } } - ).orElseGet(this::getUnauthorizedResponse); + ).orElseGet(this::getUnauthorizedResponse)); } @DeleteMapping(value = "/cache-list/evict/rules/{mapKey}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Delete a record from a rules map in the cache", description = "Will delete a key-value pair from a specific rules map") - @ResponseBody - public ResponseEntity evictRules(@PathVariable String mapKey, HttpServletRequest request) { - return getServiceId(request).map( + public Mono> evictRules(@PathVariable String mapKey, ServerHttpRequest request) { + return Mono.fromCallable(() -> getServiceId(request).map( s -> { log.debug("Delete record for serviceId: {}", s); try { storage.removeNonRelevantRules(s, mapKey); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception exception) { - return handleInternalError(exception, request.getRequestURL()); + return handleInternalError(exception, request); } } - ).orElseGet(this::getUnauthorizedResponse); + ).orElseGet(this::getUnauthorizedResponse)); } @DeleteMapping(value = "/cache-list/evict/tokens/{mapKey}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Delete a record from an invalid tokens map in the cache", description = "Will delete a key-value pair from a specific tokens map") - @ResponseBody - public ResponseEntity evictTokens(@PathVariable String mapKey, HttpServletRequest request) { - return getServiceId(request).map( + public Mono> evictTokens(@PathVariable String mapKey, ServerHttpRequest request) { + return Mono.fromCallable(() -> getServiceId(request).map( s -> { log.debug("Evict tokens for serviceId: {}", s); try { storage.removeNonRelevantTokens(s, mapKey); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception exception) { - return handleInternalError(exception, request.getRequestURL()); + return handleInternalError(exception, request); } } - ).orElseGet(this::getUnauthorizedResponse); + ).orElseGet(this::getUnauthorizedResponse)); } - @PutMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_VALUE) + @PutMapping(value = {"/cache", "/cache/"}, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Update key in the cache", description = "Value at the key in the provided key-value pair will be updated to the provided value") - @ResponseBody - public ResponseEntity update(@RequestBody KeyValue keyValue, HttpServletRequest request) { - return keyValueRequest(storage::update, - keyValue, request, HttpStatus.NO_CONTENT); + public Mono> update(@RequestBody KeyValue keyValue, ServerHttpRequest request) { + return Mono.fromCallable(() -> keyValueRequest(storage::update, + keyValue, request, HttpStatus.NO_CONTENT)); } @@ -204,9 +203,9 @@ private ResponseEntity exceptionToResponse(StorageException exception) { * Do the storage operation passed in as Lambda * Properly handle and package Exceptions. */ - private ResponseEntity keyRequest(KeyOperation keyOperation, String key, HttpServletRequest request, HttpStatus successStatus) { + private ResponseEntity keyRequest(KeyOperation keyOperation, String key, ServerHttpRequest request, HttpStatus successStatus) { Optional serviceId = getServiceId(request); - if (!serviceId.isPresent()) { + if (serviceId.isEmpty()) { return getUnauthorizedResponse(); } try { @@ -220,7 +219,7 @@ private ResponseEntity keyRequest(KeyOperation keyOperation, String key, } catch (StorageException exception) { return exceptionToResponse(exception); } catch (Exception exception) { - return handleInternalError(exception, request.getRequestURL()); + return handleInternalError(exception, request); } } @@ -231,9 +230,9 @@ private ResponseEntity keyRequest(KeyOperation keyOperation, String key, * Properly handle and package Exceptions. */ private ResponseEntity keyValueRequest(KeyValueOperation keyValueOperation, KeyValue keyValue, - HttpServletRequest request, HttpStatus successStatus) { + ServerHttpRequest request, HttpStatus successStatus) { Optional serviceId = getServiceId(request); - if (!serviceId.isPresent()) { + if (serviceId.isEmpty()) { return getUnauthorizedResponse(); } @@ -246,12 +245,12 @@ private ResponseEntity keyValueRequest(KeyValueOperation keyValueOperati } catch (StorageException exception) { return exceptionToResponse(exception); } catch (Exception exception) { - return handleInternalError(exception, request.getRequestURL()); + return handleInternalError(exception, request); } } private ResponseEntity mapKeyValueRequest(MapKeyValueOperation operation, String mapKey, KeyValue keyValue, - HttpServletRequest request, HttpStatus successStatus) { + ServerHttpRequest request, HttpStatus successStatus) { Optional serviceId = getServiceId(request); if (serviceId.isEmpty()) { return getUnauthorizedResponse(); @@ -267,25 +266,38 @@ private ResponseEntity mapKeyValueRequest(MapKeyValueOperation operation } catch (StorageException exception) { return exceptionToResponse(exception); } catch (Exception exception) { - return handleInternalError(exception, request.getRequestURL()); + return handleInternalError(exception, request); } } - private Optional getServiceId(HttpServletRequest request) { - Optional certificateServiceId = getHeader(request, "X-Certificate-DistinguishedName"); + private Optional getServiceId(ServerHttpRequest request) { + Optional certificateServiceId = getCertificateServiceId(request); Optional specificServiceId = getHeader(request, "X-CS-Service-ID"); if (certificateServiceId.isPresent() && specificServiceId.isPresent()) { return Optional.of(certificateServiceId.get() + ", SERVICE=" + specificServiceId.get()); - } else if (!specificServiceId.isPresent()) { - return certificateServiceId; + } + + return specificServiceId.or(() -> certificateServiceId); + } + + private Optional getCertificateServiceId(ServerHttpRequest request) { + if (applicationInfo != null && applicationInfo.isModulith()) { + return extractFromSslInfo(request); } else { - return specificServiceId; + return getHeader(request, "X-Certificate-DistinguishedName"); } } - private Optional getHeader(HttpServletRequest request, String headerName) { - String serviceId = request.getHeader(headerName); + private Optional extractFromSslInfo(ServerHttpRequest request) { + return Optional.ofNullable(request.getSslInfo()) + .map(SslInfo::getPeerCertificates) + .filter(certs -> certs.length > 0) + .map(certs -> certs[0].getSubjectX500Principal().getName()); + } + + private Optional getHeader(ServerHttpRequest request, String headerName) { + String serviceId = request.getHeaders().getFirst(headerName); if (StringUtils.isEmpty(serviceId)) { return Optional.empty(); } else { @@ -293,15 +305,15 @@ private Optional getHeader(HttpServletRequest request, String headerName } } - private ResponseEntity handleInternalError(Exception exception, StringBuffer requestURL) { + private ResponseEntity handleInternalError(Exception exception, ServerHttpRequest request) { Messages internalServerError = Messages.INTERNAL_SERVER_ERROR; - Message message = messageService.createMessage(internalServerError.getKey(), requestURL, exception.getMessage(), exception.toString()); + Message message = messageService.createMessage(internalServerError.getKey(), request.getURI().toString(), exception.getMessage(), exception.toString()); return new ResponseEntity<>(message.mapToView(), internalServerError.getStatus()); } - private ResponseEntity handleIncompatibleStorageMethod(Exception exception, StringBuffer requestURL) { + private ResponseEntity handleIncompatibleStorageMethod(Exception exception, ServerHttpRequest request) { Messages internalServerError = Messages.INCOMPATIBLE_STORAGE_METHOD; - Message message = messageService.createMessage(internalServerError.getKey(), requestURL, exception.getMessage(), exception.toString()); + Message message = messageService.createMessage(internalServerError.getKey(), request.getURI().toString(), exception.getMessage(), exception.toString()); return new ResponseEntity<>(message.mapToView(), internalServerError.getStatus()); } diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java index 7faa0ea30e..4d9c6fc464 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java @@ -17,12 +17,13 @@ import org.springframework.context.annotation.Import; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.zowe.apiml.filter.AttlsHttpHandler; import org.zowe.apiml.product.web.ApimlTomcatCustomizer; import org.zowe.apiml.product.web.TomcatAcceptFixConfig; import org.zowe.apiml.product.web.TomcatKeyringFix; @Configuration -@Import({TomcatKeyringFix.class, TomcatAcceptFixConfig.class, ApimlTomcatCustomizer.class}) +@Import({TomcatKeyringFix.class, TomcatAcceptFixConfig.class, ApimlTomcatCustomizer.class, AttlsHttpHandler.class,}) @Data @ToString public class GeneralConfig implements WebMvcConfigurer { diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java index 52b62a11c6..fc49f7400b 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java @@ -10,6 +10,7 @@ package org.zowe.apiml.caching.config; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.zowe.apiml.message.core.MessageService; @@ -18,6 +19,7 @@ @Configuration public class MessageConfiguration { @Bean + @ConditionalOnMissingBean(name = "modulithConfig") public MessageService messageService() { MessageService messageService = YamlMessageServiceInstance.getInstance(); messageService.loadMessages("/utility-log-messages.yml"); diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java index 42dbcba52c..16cc3cb04c 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java @@ -13,23 +13,26 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; -import org.zowe.apiml.filter.AttlsFilter; -import org.zowe.apiml.filter.SecureConnectionFilter; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; +import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.zowe.apiml.security.common.util.X509Util; +import reactor.core.publisher.Mono; -import java.util.Collections; +import java.util.ArrayList; +import java.util.List; @Configuration -@EnableWebSecurity +@EnableWebFluxSecurity public class SpringSecurityConfig { @Value("${apiml.service.ssl.verifySslCertificatesOfServices:true}") @@ -38,49 +41,51 @@ public class SpringSecurityConfig { @Value("${apiml.service.ssl.nonStrictVerifySslCertificatesOfServices:false}") private boolean nonStrictVerifyCerts; - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; - @Value("${apiml.health.protected:true}") private boolean isHealthEndpointProtected; @Bean - public WebSecurityCustomizer webSecurityCustomizer() { - String[] noSecurityAntMatchers = { - "/application/info", - "/v3/api-docs" - }; + @Order(1) + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - return web -> { - if (!isHealthEndpointProtected) { - web.ignoring().requestMatchers("/application/health"); - } - web.ignoring().requestMatchers(noSecurityAntMatchers); - }; - } + var antMatchersToIgnore = new ArrayList(); + antMatchersToIgnore.add("/cachingservice/application/info"); + antMatchersToIgnore.add("/cachingservice/v3/api-docs"); + if (!isHealthEndpointProtected) { + antMatchersToIgnore.add("/cachingservice/application/health"); + } - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.csrf(AbstractHttpConfigurer::disable) // NOSONAR - .headers(httpSecurityHeadersConfigurer -> - httpSecurityHeadersConfigurer.httpStrictTransportSecurity(HeadersConfigurer.HstsConfig::disable)) - .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .headers(headers -> headers.hsts(ServerHttpSecurity.HeaderSpec.HstsSpec::disable)) + .securityMatcher(new AndServerWebExchangeMatcher( + ServerWebExchangeMatchers.pathMatchers("/cachingservice/**") + )) + .authorizeExchange(exchange -> exchange + .pathMatchers(antMatchersToIgnore.toArray(new String[0])).permitAll() + .anyExchange().authenticated() + ).exceptionHandling(exceptionHandlingSpec -> + exceptionHandlingSpec.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.FORBIDDEN)) + ); if (verifyCertificates || !nonStrictVerifyCerts) { - http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated()) - .x509(x509 -> x509.userDetailsService(x509UserDetailsService())); - if (isAttlsEnabled) { - http.addFilterBefore(new AttlsFilter(), X509AuthenticationFilter.class); - http.addFilterBefore(new SecureConnectionFilter(), AttlsFilter.class); - } + http.x509(x509spec -> x509spec.principalExtractor(X509Util.x509PrincipalExtractor()) + .authenticationManager(X509Util.x509ReactiveAuthenticationManager())); } else { - http.authorizeHttpRequests(requests -> requests.anyRequest().permitAll()); + http.authorizeExchange(exchange -> exchange.anyExchange().permitAll()); } return http.build(); } - private UserDetailsService x509UserDetailsService() { - return username -> new User("cachingUser", "", Collections.emptyList()); + + @Bean + ReactiveUserDetailsService userDetailsService() { + + return username -> { + List authorities = new ArrayList<>(); + UserDetails userDetails = User.withUsername(username).authorities(authorities).password("").build(); + return Mono.just(userDetails); + }; } } diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java index 4d3a366a10..6b47c66f0a 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java @@ -13,10 +13,12 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration +@ConditionalOnMissingBean(name = "modulithConfig") public class SwaggerConfig { @Value("${apiml.service.title}") diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/RejectStrategy.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/RejectStrategy.java index 511aab4923..a4afea29d5 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/RejectStrategy.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/RejectStrategy.java @@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.message.log.ApimlLogger; @RequiredArgsConstructor diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java index 5b44ce9575..bbdb18cf15 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java @@ -28,13 +28,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ResourceLoader; -import org.zowe.apiml.caching.service.Storage; +import org.zowe.apiml.cache.Storage; import org.zowe.apiml.caching.service.infinispan.exception.InfinispanConfigException; import org.zowe.apiml.caching.service.infinispan.storage.InfinispanStorage; import java.io.IOException; import java.io.InputStream; import java.nio.file.Paths; +import java.util.Arrays; import static org.zowe.apiml.security.SecurityUtils.formatKeyringUrl; import static org.zowe.apiml.security.SecurityUtils.isKeyring; @@ -117,12 +118,12 @@ DefaultCacheManager cacheManager(ResourceLoader resourceLoader) { builder.persistence().passivation(true) .addSoftIndexFileStore() .shared(false); - cacheManager.administration() - .withFlags(CacheContainerAdmin.AdminFlag.VOLATILE) - .getOrCreateCache("zoweCache", builder.build()); - cacheManager.administration() + + var caches = Arrays.asList("zoweCache", "zoweInvalidatedTokenCache", "zosmfAuthenticationEndpoint", "invalidatedJwtTokens", "validationJwtToken", "zosmfInfo", "zosmfJwtEndpoint", "trustedCertificates", "parseOIDCToken", "validationOIDCToken"); + caches.forEach(cacheName -> cacheManager.administration() .withFlags(CacheContainerAdmin.AdminFlag.VOLATILE) - .getOrCreateCache("zoweInvalidatedTokenCache", builder.build()); + .getOrCreateCache(cacheName, builder.build())); + return cacheManager; } diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorage.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorage.java index 28dfe31414..508bbd28bd 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorage.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorage.java @@ -17,8 +17,8 @@ import org.infinispan.lock.api.ClusteredLock; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.Messages; -import org.zowe.apiml.caching.service.Storage; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.models.AccessTokenContainer; import java.time.LocalDateTime; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java index 77b92ff6c4..2b0424e205 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java @@ -11,6 +11,8 @@ package org.zowe.apiml.caching.service.inmemory; import lombok.extern.slf4j.Slf4j; +import org.zowe.apiml.cache.StorageException; +import org.zowe.apiml.cache.Storage; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.*; import org.zowe.apiml.caching.service.inmemory.config.InMemoryConfig; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/config/InMemoryConfiguration.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/config/InMemoryConfiguration.java index 0d739ca126..6b60878f24 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/config/InMemoryConfiguration.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/config/InMemoryConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.zowe.apiml.caching.service.Storage; +import org.zowe.apiml.cache.Storage; import org.zowe.apiml.caching.service.inmemory.InMemoryStorage; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.log.ApimlLogger; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/RedisStorage.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/RedisStorage.java index 900c1e5bd2..7f83521418 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/RedisStorage.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/RedisStorage.java @@ -14,8 +14,8 @@ import org.springframework.retry.annotation.Retryable; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.Messages; -import org.zowe.apiml.caching.service.Storage; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.service.redis.exceptions.RedisOutOfMemoryException; import org.zowe.apiml.caching.service.redis.exceptions.RetryableRedisException; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/config/RedisConfiguration.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/config/RedisConfiguration.java index 2e3568657b..0d6eedf6d2 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/config/RedisConfiguration.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/redis/config/RedisConfiguration.java @@ -19,7 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.zowe.apiml.caching.service.Storage; +import org.zowe.apiml.cache.Storage; import org.zowe.apiml.caching.service.redis.RedisOperator; import org.zowe.apiml.caching.service.redis.RedisStorage; import org.zowe.apiml.message.core.MessageService; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamRecord.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamRecord.java index daa07029b3..0e93bbeca1 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamRecord.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamRecord.java @@ -15,7 +15,7 @@ import org.apache.commons.lang3.StringUtils; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.Messages; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.service.vsam.config.VsamConfig; import java.io.UnsupportedEncodingException; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamStorage.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamStorage.java index a7b32f88fc..db38eea62d 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamStorage.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/VsamStorage.java @@ -15,8 +15,8 @@ import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.EvictionStrategy; import org.zowe.apiml.caching.service.Messages; -import org.zowe.apiml.caching.service.Storage; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.service.vsam.config.VsamConfig; import org.zowe.apiml.message.log.ApimlLogger; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/config/VsamConfiguration.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/config/VsamConfiguration.java index 9fbe4e23b7..a150465789 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/config/VsamConfiguration.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/vsam/config/VsamConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.zowe.apiml.caching.service.Storage; +import org.zowe.apiml.cache.Storage; import org.zowe.apiml.caching.service.vsam.EvictionStrategyProducer; import org.zowe.apiml.caching.service.vsam.VsamInitializer; import org.zowe.apiml.caching.service.vsam.VsamStorage; diff --git a/caching-service/src/main/resources/application.yml b/caching-service/src/main/resources/application.yml index 0004b00603..f158658fc0 100644 --- a/caching-service/src/main/resources/application.yml +++ b/caching-service/src/main/resources/application.yml @@ -115,24 +115,20 @@ spring: enabled: false application: name: Caching service - mvc: - pathmatch: - matchingStrategy: ant-path-matcher # used to resolve spring fox path matching issue output.ansi.enabled: always main: allow-circular-references: true banner-mode: ${apiml.banner:"off"} - web-application-type: servlet - + web-application-type: reactive + allow-bean-definition-overriding: true springdoc: pathsToMatch: /api/v1/** + api-docs: + path: /cachingservice/v3/api-docs server: port: ${apiml.service.port} - servlet: - contextPath: /${apiml.service.serviceId} - ssl: enabled: true clientAuth: want @@ -153,7 +149,7 @@ management: endpoints: migrate-legacy-ids: true web: - base-path: /application + base-path: /cachingservice/application exposure: include: health,info health: @@ -179,7 +175,7 @@ logging: org.springframework.web.servlet.PageNotFound: WARN org.ehcache: INFO org.apache.tomcat.util.net: DEBUG - org.springframework.security.web: INFO + org.springframework.security.web: DEBUG javax.net.ssl: ERROR spring.config.activate.on-profile: dev diff --git a/caching-service/src/main/resources/infinispan.xml b/caching-service/src/main/resources/infinispan.xml index f4ef4f16a8..2fc1b34026 100644 --- a/caching-service/src/main/resources/infinispan.xml +++ b/caching-service/src/main/resources/infinispan.xml @@ -86,6 +86,8 @@ org.zowe.apiml.caching.model.KeyValue + org.zowe.apiml.security.common.token.TokenAuthentication + org.zowe.apiml.security.common.token.TokenAuthentication$Type java.util.HashMap java.util.Arrays$ArrayList diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java index a3f2535590..d4fa844c73 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java @@ -16,21 +16,25 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.Messages; -import org.zowe.apiml.caching.service.Storage; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; +import reactor.test.StepVerifier; -import jakarta.servlet.http.HttpServletRequest; -import java.util.*; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; import static org.mockito.ArgumentMatchers.any; @@ -44,16 +48,19 @@ class CachingControllerTest { private static final KeyValue KEY_VALUE = new KeyValue(KEY, VALUE); - private HttpServletRequest mockRequest; + private ServerHttpRequest mockRequest; private Storage mockStorage; private final MessageService messageService = new YamlMessageService("/caching-log-messages.yml"); private CachingController underTest; @BeforeEach void setUp() { - mockRequest = mock(HttpServletRequest.class); - when(mockRequest.getHeader("X-Certificate-DistinguishedName")).thenReturn(SERVICE_ID); - when(mockRequest.getHeader("X-CS-Service-ID")).thenReturn(null); + mockRequest = mock(ServerHttpRequest.class); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Certificate-DistinguishedName", SERVICE_ID); + headers.add("X-CS-Service-ID", null); + when(mockRequest.getHeaders()).thenReturn(headers); + when(mockRequest.getURI()).thenReturn(URI.create("http://localhost")); mockStorage = mock(Storage.class); underTest = new CachingController(mockStorage, messageService); } @@ -66,19 +73,22 @@ void givenStorageReturnsValidValues_thenReturnProperValues() { values.put(KEY, new KeyValue("key2", VALUE)); when(mockStorage.readForService(SERVICE_ID)).thenReturn(values); - ResponseEntity response = underTest.getAllValues(mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.OK)); - - Map result = (Map) response.getBody(); - assertThat(result, is(values)); + StepVerifier.create(underTest.getAllValues(mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + Map result = (Map) response.getBody(); + assertThat(result, is(values)); + }) + .verifyComplete(); } @Test void givenStorageThrowsInternalException_thenProperlyReturnError() { when(mockStorage.readForService(SERVICE_ID)).thenThrow(new RuntimeException()); - ResponseEntity response = underTest.getAllValues(mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); + StepVerifier.create(underTest.getAllValues(mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR))) + .verifyComplete(); } } @@ -86,18 +96,21 @@ void givenStorageThrowsInternalException_thenProperlyReturnError() { class WhenDeletingAllKeysForService { @Test void givenStorageRaisesNoException_thenReturnOk() { - ResponseEntity response = underTest.deleteAllValues(mockRequest); - - verify(mockStorage).deleteForService(SERVICE_ID); - assertThat(response.getStatusCode(), is(HttpStatus.OK)); + StepVerifier.create(underTest.deleteAllValues(mockRequest)) + .assertNext(response -> { + verify(mockStorage).deleteForService(SERVICE_ID); + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + }) + .verifyComplete(); } @Test void givenStorageThrowsInternalException_thenProperlyReturnError() { when(mockStorage.readForService(SERVICE_ID)).thenThrow(new RuntimeException()); - ResponseEntity response = underTest.getAllValues(mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); + StepVerifier.create(underTest.getAllValues(mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR))) + .verifyComplete(); } } @@ -107,21 +120,26 @@ class WhenGetKey { void givenStorageReturnsValidValue_thenReturnProperValue() { when(mockStorage.read(SERVICE_ID, KEY)).thenReturn(KEY_VALUE); - ResponseEntity response = underTest.getValue(KEY, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.OK)); - - KeyValue body = (KeyValue) response.getBody(); - assertThat(body.getValue(), is(VALUE)); + StepVerifier.create(underTest.getValue(KEY, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + KeyValue body = (KeyValue) response.getBody(); + assertThat(body, notNullValue()); + assertThat(body.getValue(), is(VALUE)); + }) + .verifyComplete(); } - @Test void givenNoKey_thenResponseBadRequest() { ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotProvided", SERVICE_ID).mapToView(); - ResponseEntity response = underTest.getValue(null, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.getValue(null, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } @Test @@ -129,17 +147,21 @@ void givenStoreWithNoKey_thenResponseNotFound() { ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotInCache", KEY, SERVICE_ID).mapToView(); when(mockStorage.read(any(), any())).thenThrow(new StorageException(Messages.KEY_NOT_IN_CACHE.getKey(), Messages.KEY_NOT_IN_CACHE.getStatus(), new Exception("the cause"), KEY, SERVICE_ID)); - ResponseEntity response = underTest.getValue(KEY, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.getValue(KEY, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } @Test void givenErrorReadingStorage_thenResponseInternalError() { when(mockStorage.read(any(), any())).thenThrow(new RuntimeException("error")); - ResponseEntity response = underTest.getValue(KEY, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); + StepVerifier.create(underTest.getValue(KEY, mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR))) + .verifyComplete(); } } @@ -149,9 +171,12 @@ class WhenCreateKey { void givenStorage_thenResponseCreated() { when(mockStorage.create(SERVICE_ID, KEY_VALUE)).thenReturn(KEY_VALUE); - ResponseEntity response = underTest.createKey(KEY_VALUE, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.CREATED)); - assertThat(response.getBody(), is(nullValue())); + StepVerifier.create(underTest.createKey(KEY_VALUE, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.CREATED)); + assertThat(response.getBody(), is(nullValue())); + }) + .verifyComplete(); } @Test @@ -159,19 +184,22 @@ void givenStorageWithExistingKey_thenResponseConflict() { when(mockStorage.create(SERVICE_ID, KEY_VALUE)).thenThrow(new StorageException(Messages.DUPLICATE_KEY.getKey(), Messages.DUPLICATE_KEY.getStatus(), KEY)); ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyCollision", KEY).mapToView(); - ResponseEntity response = underTest.createKey(KEY_VALUE, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.CONFLICT)); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.createKey(KEY_VALUE, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.CONFLICT)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } @Test void givenStorageWithError_thenResponseInternalError() { when(mockStorage.create(SERVICE_ID, KEY_VALUE)).thenThrow(new RuntimeException("error")); - ResponseEntity response = underTest.createKey(KEY_VALUE, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); + StepVerifier.create(underTest.createKey(KEY_VALUE, mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR))) + .verifyComplete(); } - } @Nested @@ -180,9 +208,12 @@ class WhenUpdateKey { void givenStorageWithKey_thenResponseNoContent() { when(mockStorage.update(SERVICE_ID, KEY_VALUE)).thenReturn(KEY_VALUE); - ResponseEntity response = underTest.update(KEY_VALUE, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); - assertThat(response.getBody(), is(nullValue())); + StepVerifier.create(underTest.update(KEY_VALUE, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); + assertThat(response.getBody(), is(nullValue())); + }) + .verifyComplete(); } @Test @@ -190,9 +221,12 @@ void givenStorageWithNoKey_thenResponseNotFound() { when(mockStorage.update(SERVICE_ID, KEY_VALUE)).thenThrow(new StorageException(Messages.KEY_NOT_IN_CACHE.getKey(), Messages.KEY_NOT_IN_CACHE.getStatus(), KEY, SERVICE_ID)); ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotInCache", KEY, SERVICE_ID).mapToView(); - ResponseEntity response = underTest.update(KEY_VALUE, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.update(KEY_VALUE, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } } @@ -202,18 +236,24 @@ class WhenDeleteKey { void givenStorageWithKey_thenResponseNoContent() { when(mockStorage.delete(any(), any())).thenReturn(KEY_VALUE); - ResponseEntity response = underTest.delete(KEY, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); - assertThat(response.getBody(), is(KEY_VALUE)); + StepVerifier.create(underTest.delete(KEY, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); + assertThat(response.getBody(), is(KEY_VALUE)); + }) + .verifyComplete(); } @Test void givenNoKey_thenResponseBadRequest() { ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotProvided").mapToView(); - ResponseEntity response = underTest.delete(null, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.delete(null, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } @Test @@ -221,20 +261,25 @@ void givenStorageWithNoKey_thenResponseNotFound() { ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotInCache", KEY, SERVICE_ID).mapToView(); when(mockStorage.delete(any(), any())).thenThrow(new StorageException(Messages.KEY_NOT_IN_CACHE.getKey(), Messages.KEY_NOT_IN_CACHE.getStatus(), KEY, SERVICE_ID)); - ResponseEntity response = underTest.delete(KEY, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.delete(KEY, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } } @Test void givenNoPayload_whenValidatePayload_thenResponseBadRequest() { - ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.invalidPayload", - null, "No KeyValue provided in the payload").mapToView(); - - ResponseEntity response = underTest.createKey(null, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); - assertThat(response.getBody(), is(expectedBody)); + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.invalidPayload", null, "No KeyValue provided in the payload").mapToView(); + + StepVerifier.create(underTest.createKey(null, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } @ParameterizedTest @@ -242,14 +287,15 @@ void givenNoPayload_whenValidatePayload_thenResponseBadRequest() { void givenVariousKeyValue_whenValidatePayload_thenResponseAccordingly(String key, String value, String errMessage, HttpStatus statusCode) { KeyValue keyValue = new KeyValue(key, value); - ResponseEntity response = underTest.createKey(keyValue, mockRequest); - assertThat(response.getStatusCode(), is(statusCode)); - - if (errMessage != null) { - ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.invalidPayload", - keyValue, errMessage).mapToView(); - assertThat(response.getBody(), is(expectedBody)); - } + StepVerifier.create(underTest.createKey(keyValue, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(statusCode)); + if (errMessage != null) { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.invalidPayload", keyValue, errMessage).mapToView(); + assertThat(response.getBody(), is(expectedBody)); + } + }) + .verifyComplete(); } private static Stream provideStringsForGivenVariousKeyValue() { @@ -262,51 +308,64 @@ private static Stream provideStringsForGivenVariousKeyValue() { @Test void givenNoCertificateInformationInHeader_whenGetAllValues_thenReturnUnauthorized() { - when(mockStorage.read(SERVICE_ID, KEY)).thenReturn(KEY_VALUE); - when(mockRequest.getHeader("X-Certificate-DistinguishedName")).thenReturn(null); - ResponseEntity response = underTest.getAllValues(mockRequest); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Certificate-DistinguishedName", null); + when(mockRequest.getHeaders()).thenReturn(headers); - assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.missingCertificate", "parameter").mapToView(); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.getAllValues(mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } @Nested class WhenUseSpecificServiceHeader { @BeforeEach void setUp() { - when(mockRequest.getHeader("X-CS-Service-ID")).thenReturn(SERVICE_ID); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Certificate-DistinguishedName", SERVICE_ID); + headers.add("X-CS-Service-ID", null); + when(mockRequest.getHeaders()).thenReturn(headers); } @Test void givenServiceIdHeader_thenReturnProperValues() { - when(mockRequest.getHeader("X-Certificate-DistinguishedName")).thenReturn(null); Map values = new HashMap<>(); values.put(KEY, new KeyValue("key2", VALUE)); when(mockStorage.readForService(SERVICE_ID)).thenReturn(values); - ResponseEntity response = underTest.getAllValues(mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.OK)); - - Map result = (Map) response.getBody(); - assertThat(result, is(values)); + StepVerifier.create(underTest.getAllValues(mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + Map result = (Map) response.getBody(); + assertThat(result, is(values)); + }) + .verifyComplete(); } @Test void givenServiceIdHeaderAndCertificateHeaderForReadForService_thenReturnProperValues() { - when(mockRequest.getHeader("X-Certificate-DistinguishedName")).thenReturn("certificate"); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Certificate-DistinguishedName", "certificate"); + headers.add("X-CS-Service-ID", SERVICE_ID); + when(mockRequest.getHeaders()).thenReturn(headers); Map values = new HashMap<>(); values.put(KEY, new KeyValue("key2", VALUE)); when(mockStorage.readForService("certificate, SERVICE=" + SERVICE_ID)).thenReturn(values); - ResponseEntity response = underTest.getAllValues(mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.OK)); - - Map result = (Map) response.getBody(); - assertThat(result, is(values)); + StepVerifier.create(underTest.getAllValues(mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + Map result = (Map) response.getBody(); + assertThat(result, is(values)); + }) + .verifyComplete(); } } @@ -314,35 +373,46 @@ void givenServiceIdHeaderAndCertificateHeaderForReadForService_thenReturnProperV class WhenInvalidatedTokenIsStored { @Test void givenCorrectPayload_thenStore() { - KeyValue keyValue = new KeyValue(KEY, VALUE); - ResponseEntity response = underTest.storeMapItem(MAP_KEY, keyValue, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.CREATED)); - assertThat(response.getBody(), is(nullValue())); + StepVerifier.create(underTest.storeMapItem(MAP_KEY, KEY_VALUE, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.CREATED)); + assertThat(response.getBody(), is(nullValue())); + }) + .verifyComplete(); } @Test void givenIncorrectPayload_thenReturnBadRequest() { KeyValue keyValue = new KeyValue(null, VALUE); - ResponseEntity response = underTest.storeMapItem(MAP_KEY, keyValue, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + + StepVerifier.create(underTest.storeMapItem(MAP_KEY, keyValue, mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST))) + .verifyComplete(); } @Test void givenErrorOnTransaction_thenReturnInternalError() throws StorageException { - when(mockStorage.storeMapItem(any(), any(), any())).thenThrow(new StorageException(Messages.INTERNAL_SERVER_ERROR.getKey(), Messages.INTERNAL_SERVER_ERROR.getStatus(), new Exception("the cause"), KEY)); - KeyValue keyValue = new KeyValue(KEY, VALUE); - ResponseEntity response = underTest.storeMapItem(MAP_KEY, keyValue, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); + when(mockStorage.storeMapItem(any(), any(), any())) + .thenThrow(new StorageException(Messages.INTERNAL_SERVER_ERROR.getKey(), Messages.INTERNAL_SERVER_ERROR.getStatus(), new Exception("the cause"), KEY)); + + StepVerifier.create(underTest.storeMapItem(MAP_KEY, KEY_VALUE, mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR))) + .verifyComplete(); } @Test void givenStorageWithExistingValue_thenResponseConflict() throws StorageException { - when(mockStorage.storeMapItem(SERVICE_ID, MAP_KEY, KEY_VALUE)).thenThrow(new StorageException(Messages.DUPLICATE_VALUE.getKey(), Messages.DUPLICATE_VALUE.getStatus(), VALUE)); + when(mockStorage.storeMapItem(SERVICE_ID, MAP_KEY, KEY_VALUE)) + .thenThrow(new StorageException(Messages.DUPLICATE_VALUE.getKey(), Messages.DUPLICATE_VALUE.getStatus(), VALUE)); + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.duplicateValue", VALUE).mapToView(); - ResponseEntity response = underTest.storeMapItem(MAP_KEY, KEY_VALUE, mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.CONFLICT)); - assertThat(response.getBody(), is(expectedBody)); + StepVerifier.create(underTest.storeMapItem(MAP_KEY, KEY_VALUE, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.CONFLICT)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } } @@ -350,48 +420,73 @@ void givenStorageWithExistingValue_thenResponseConflict() throws StorageExceptio class WhenRetrieveInvalidatedTokens { @Test void givenCorrectRequest_thenReturnList() throws StorageException { - HashMap expectedMap = new HashMap(); + HashMap expectedMap = new HashMap<>(); expectedMap.put("key", "token1"); expectedMap.put("key2", "token2"); when(mockStorage.getAllMapItems(anyString(), any())).thenReturn(expectedMap); - ResponseEntity response = underTest.getAllMapItems(any(), mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.OK)); - assertThat(response.getBody(), is(expectedMap)); + + StepVerifier.create(underTest.getAllMapItems(MAP_KEY, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + assertThat(response.getBody(), is(expectedMap)); + }) + .verifyComplete(); } @Test void givenCorrectRequest_thenReturnAllLists() throws StorageException { - Map invalidTokens = new HashMap(); + Map> expectedMap = getStringMapMap(); + + when(mockStorage.getAllMaps(anyString())).thenReturn(expectedMap); + + StepVerifier.create(underTest.getAllMaps(mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + assertThat(response.getBody(), is(expectedMap)); + }) + .verifyComplete(); + } + + private static Map> getStringMapMap() { + Map invalidTokens = new HashMap<>(); invalidTokens.put("key", "token1"); invalidTokens.put("key2", "token2"); - Map invalidTokenRules = new HashMap(); - invalidTokens.put("key", "rule1"); - invalidTokens.put("key2", "rule2"); - Map> expectedMap = new HashMap(); + + Map invalidTokenRules = new HashMap<>(); + invalidTokenRules.put("key", "rule1"); + invalidTokenRules.put("key2", "rule2"); + + Map> expectedMap = new HashMap<>(); expectedMap.put("invalidTokens", invalidTokens); expectedMap.put("invalidTokenRules", invalidTokenRules); - when(mockStorage.getAllMaps(anyString())).thenReturn(expectedMap); - ResponseEntity response = underTest.getAllMaps(mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.OK)); - assertThat(response.getBody(), is(expectedMap)); + return expectedMap; } @Test void givenNoCertificateInformation_thenReturnUnauthorized() throws StorageException { - when(mockStorage.getAllMapItems(any(), any())).thenReturn(any()); - when(mockRequest.getHeader("X-Certificate-DistinguishedName")).thenReturn(null); - ResponseEntity response = underTest.getAllMapItems(any(), mockRequest); - - assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Certificate-DistinguishedName", null); + when(mockRequest.getHeaders()).thenReturn(headers); + + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.missingCertificate", "parameter").mapToView(); + + StepVerifier.create(underTest.getAllMapItems(MAP_KEY, mockRequest)) + .assertNext(response -> { + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + assertThat(response.getBody(), is(expectedBody)); + }) + .verifyComplete(); } @Test void givenErrorReadingStorage_thenResponseBadRequest() throws StorageException { - when(mockStorage.getAllMapItems(any(), any())).thenThrow(new RuntimeException("error")); + when(mockStorage.getAllMapItems(anyString(), anyString())) + .thenThrow(new RuntimeException("error")); - ResponseEntity response = underTest.getAllMapItems(any(), mockRequest); - assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + StepVerifier.create(underTest.getAllMapItems(MAP_KEY, mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST))) + .verifyComplete(); } } @@ -399,22 +494,33 @@ void givenErrorReadingStorage_thenResponseBadRequest() throws StorageException { class WhenEvictRecord { @Test void givenCorrectRequest_thenRemoveTokensAndRules() throws StorageException { - ResponseEntity responseTokenEviction = underTest.evictTokens(MAP_KEY, mockRequest); - ResponseEntity responseScopesEviction = underTest.evictRules(MAP_KEY, mockRequest); - verify(mockStorage).removeNonRelevantTokens(SERVICE_ID, MAP_KEY); - verify(mockStorage).removeNonRelevantRules(SERVICE_ID, MAP_KEY); - assertThat(responseTokenEviction.getStatusCode(), is(HttpStatus.NO_CONTENT)); - assertThat(responseScopesEviction.getStatusCode(), is(HttpStatus.NO_CONTENT)); + StepVerifier.create(underTest.evictTokens(MAP_KEY, mockRequest)) + .assertNext(response -> { + verify(mockStorage).removeNonRelevantTokens(SERVICE_ID, MAP_KEY); + assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); + }) + .verifyComplete(); + + StepVerifier.create(underTest.evictRules(MAP_KEY, mockRequest)) + .assertNext(response -> { + verify(mockStorage).removeNonRelevantRules(SERVICE_ID, MAP_KEY); + assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); + }) + .verifyComplete(); } @Test void givenInCorrectRequest_thenReturn500() throws StorageException { doThrow(new RuntimeException()).when(mockStorage).removeNonRelevantTokens(SERVICE_ID, MAP_KEY); doThrow(new RuntimeException()).when(mockStorage).removeNonRelevantRules(SERVICE_ID, MAP_KEY); - ResponseEntity responseScopesEviction = underTest.evictRules(MAP_KEY, mockRequest); - ResponseEntity responseTokenEviction = underTest.evictTokens(MAP_KEY, mockRequest); - assertThat(responseTokenEviction.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); - assertThat(responseScopesEviction.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); + + StepVerifier.create(underTest.evictTokens(MAP_KEY, mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR))) + .verifyComplete(); + + StepVerifier.create(underTest.evictRules(MAP_KEY, mockRequest)) + .assertNext(response -> assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR))) + .verifyComplete(); } } } 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 9a5368e77b..549dfb9882 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 @@ -10,10 +10,19 @@ package org.zowe.apiml.caching.config; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +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.web.server.LocalServerPort; @@ -22,14 +31,18 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.caching.CachingService; +import org.zowe.apiml.filter.AttlsHttpHandler; import org.zowe.apiml.util.config.SslContext; import javax.net.ssl.SSLException; import static io.restassured.RestAssured.given; -import static org.hamcrest.core.StringContains.containsString; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; @SpringBootTest( classes = CachingService.class, @@ -56,8 +69,14 @@ public class AttlsConfigTest { @Nested class GivenAttlsModeEnabled { - private String getUri(String scheme, String endpoint) { - return String.format("%s://%s:%d/%s/%s", scheme, hostname, port, "api/v1", endpoint); + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + + private String getUri(String scheme) { + return String.format("%s://%s:%d/%s", scheme, hostname, port, "api/v1/cache"); } @Nested @@ -69,8 +88,8 @@ void requestFailsWithHttps() { given() .config(SslContext.clientCertUnknownUser) .header("Content-type", "application/json") - .get(getUri("https", "cache")) - .then() + .get(getUri("https")) + .then() .statusCode(HttpStatus.FORBIDDEN.value()); fail(""); } catch (Exception e) { @@ -80,14 +99,21 @@ void requestFailsWithHttps() { @Test void requestFailsWithAttlsReasonWithHttp() { + var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); given() .config(SslContext.clientCertUnknownUser) .header("Content-type", "application/json") - .get(getUri("http", "cache")) - .then() + .get(getUri("http")) + .then() .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .body(containsString("Connection is not secure.")) - .body(containsString("AttlsContext.getStatConn")); + .body(containsString("org.zowe.apiml.common.internalServerError")); + + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); } } } diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java index 3459c99e1d..b925a21e58 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java @@ -51,7 +51,7 @@ public class InMemoryFunctionalTest { public static final String SERVICE_ID_HEADER = "X-Certificate-DistinguishedName"; - String contextPath = "/api/v1"; + String contextPath = "/cachingservice/api/v1"; String getUri(String endpoint) { return String.format("https://%s:%s%s%s", hostname, port, contextPath, endpoint); @@ -72,7 +72,6 @@ class WhenCallingByTrustedClient { void createEntry() throws Exception { KeyValue keyValue = new KeyValue("first-key", "anyValue"); ObjectMapper mapper = new ObjectMapper(); - System.out.println(1); given().config(SslContext.clientCertApiml) .body(mapper.writeValueAsString(keyValue)) .header("Content-type", "application/json") @@ -85,7 +84,6 @@ void createEntry() throws Exception { @Test @Order(2) void readAllEntries() { - System.out.println(2); given().config(SslContext.clientCertApiml) .header("Content-type", "application/json") .header(SERVICE_ID_HEADER, "service1") diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorageTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorageTest.java index 09c4a5c10b..a13987d4f5 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorageTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStorageTest.java @@ -17,7 +17,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.zowe.apiml.caching.model.KeyValue; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.StorageException; import java.util.ArrayList; import java.util.HashMap; diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java index 2e4f9b586f..c97c413a50 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test; import org.zowe.apiml.caching.config.GeneralConfig; import org.zowe.apiml.caching.model.KeyValue; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.service.Strategies; import org.zowe.apiml.caching.service.inmemory.config.InMemoryConfig; import org.zowe.apiml.message.core.MessageService; diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/RejectStrategyTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/RejectStrategyTest.java index feb8364b5a..eb3524ee18 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/RejectStrategyTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/RejectStrategyTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.zowe.apiml.caching.service.RejectStrategy; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.message.log.ApimlLogger; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/redis/RedisStorageTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/redis/RedisStorageTest.java index 316e4c2789..8c97d617ed 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/service/redis/RedisStorageTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/redis/RedisStorageTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.Messages; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.service.redis.exceptions.RedisOutOfMemoryException; import java.util.ArrayList; diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamRecordTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamRecordTest.java index 28156dc3b9..ff1925642a 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamRecordTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamRecordTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.zowe.apiml.caching.model.KeyValue; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.service.vsam.config.VsamConfig; import org.zowe.apiml.zfile.ZFileConstants; diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamStorageTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamStorageTest.java index 58b6d9ca7b..10cff5df16 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamStorageTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/vsam/VsamStorageTest.java @@ -16,7 +16,7 @@ import org.zowe.apiml.caching.config.GeneralConfig; import org.zowe.apiml.caching.model.KeyValue; import org.zowe.apiml.caching.service.RejectStrategy; -import org.zowe.apiml.caching.service.StorageException; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.caching.service.Strategies; import org.zowe.apiml.caching.service.vsam.config.VsamConfig; import org.zowe.apiml.message.log.ApimlLogger; diff --git a/caching-service/src/test/resources/application.yml b/caching-service/src/test/resources/application.yml index 6955e12256..ed96280b1c 100644 --- a/caching-service/src/test/resources/application.yml +++ b/caching-service/src/test/resources/application.yml @@ -9,6 +9,7 @@ spring: matchingStrategy: ant-path-matcher # used to resolve spring fox path matching issue main: allow-circular-references: true + web-application-type: reactive caching: storage: @@ -76,9 +77,6 @@ apiml: server: port: 10016 - servlet: - contextPath: / - ssl: enabled: true clientAuth: want diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/Storage.java b/common-service-core/src/main/java/org/zowe/apiml/cache/Storage.java similarity index 99% rename from caching-service/src/main/java/org/zowe/apiml/caching/service/Storage.java rename to common-service-core/src/main/java/org/zowe/apiml/cache/Storage.java index 4c6c415794..f773eb47d8 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/Storage.java +++ b/common-service-core/src/main/java/org/zowe/apiml/cache/Storage.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.caching.service; +package org.zowe.apiml.cache; import org.zowe.apiml.caching.model.KeyValue; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/StorageException.java b/common-service-core/src/main/java/org/zowe/apiml/cache/StorageException.java similarity index 96% rename from caching-service/src/main/java/org/zowe/apiml/caching/service/StorageException.java rename to common-service-core/src/main/java/org/zowe/apiml/cache/StorageException.java index c588ea6aaf..2ee36e9718 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/StorageException.java +++ b/common-service-core/src/main/java/org/zowe/apiml/cache/StorageException.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.caching.service; +package org.zowe.apiml.cache; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/model/KeyValue.java b/common-service-core/src/main/java/org/zowe/apiml/caching/model/KeyValue.java similarity index 92% rename from caching-service/src/main/java/org/zowe/apiml/caching/model/KeyValue.java rename to common-service-core/src/main/java/org/zowe/apiml/caching/model/KeyValue.java index 9edafff8b8..0349dff145 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/model/KeyValue.java +++ b/common-service-core/src/main/java/org/zowe/apiml/caching/model/KeyValue.java @@ -15,6 +15,7 @@ import lombok.Data; import lombok.RequiredArgsConstructor; +import java.io.Serial; import java.io.Serializable; import java.util.Date; @@ -25,6 +26,9 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) @Data public class KeyValue implements Serializable { + + @Serial + private static final long serialVersionUID = 4831101523346346817L; private final String key; private final String value; private String serviceId; diff --git a/common-service-core/src/main/java/org/zowe/apiml/product/constants/CoreService.java b/common-service-core/src/main/java/org/zowe/apiml/product/constants/CoreService.java index 5432e05592..77fb78f33f 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/product/constants/CoreService.java +++ b/common-service-core/src/main/java/org/zowe/apiml/product/constants/CoreService.java @@ -22,7 +22,8 @@ public enum CoreService { ZAAS("zaas"), DISCOVERY("discovery"), API_CATALOG("apicatalog"), - AUTH("auth"); + AUTH("auth"), + CACHING("cachingservice"); private final String serviceId; diff --git a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfig.java b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfig.java index c7e7f7aa2b..5de5e17128 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfig.java +++ b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfig.java @@ -14,6 +14,8 @@ import lombok.ToString; import lombok.Value; +import java.security.cert.X509Certificate; + @Builder @Value @ToString(exclude = {"trustStorePassword", "keyStorePassword", "keyPassword"}) @@ -51,4 +53,5 @@ public class HttpsConfig { int requestConnectionTimeout = 10_000; @Builder.Default int timeToLive = 10_000; + X509Certificate certificate; } diff --git a/config/local/apiml-service-2.yml b/config/local/apiml-service-2.yml new file mode 100644 index 0000000000..51114d5aba --- /dev/null +++ b/config/local/apiml-service-2.yml @@ -0,0 +1,79 @@ +spring.profiles.active: diag +logging: + level: + org.springframework.web.socket.client.standard.AnnotatedEndpointConnectionManager: DEBUG + org.springframework.web.reactive.socket.client.StandardWebSocketClient: TRACE + +server: + max-http-request-header-size: 16348 + webSocket: + requestBufferSize: 16348 + +eureka: + server: + myUrl: https://localhost:10021/eureka/ +apiml: + discovery: + staticApiDefinitionsDirectories: config/local/api-defs + # Not used in HTTPS mode and not applicable for Zowe + # The `apiml.discovery` node contains discovery-service only configuration + userid: eureka # Userid that Eureka server will use to check authentication of its clients (other services) + password: password # Password that Eureka server will use to check authentication of its clients (other services) + allPeersUrls: https://localhost:10011/eureka/ + internal-discovery: + port: 10021 + health: + protected: false + gateway: + servicesToLimitRequestRate: discoverableclient + cookieNameForRateLimiter: apimlAuthenticationToken + service: + apimlId: apiml2 + + discoveryServiceUrls: https://localhost:10021/eureka/ + forwardClientCertEnabled: false + hostname: localhost + id: ${spring.application.name} + 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 + port: 10020 + externalUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port} + title: API ML 2 + security: + x509: + enabled: true + certificatesUrl: https://localhost:10010/gateway/certificates + allowTokenRefresh: true + webfinger: + fileLocation: config/local/webfinger.yml + personalAccessToken: + enabled: true + oidc: + enabled: false + clientId: + clientSecret: + registry: zowe.okta.com + identityMapperUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/zss/api/v1/certificate/dn + identityMapperUser: APIMTST + jwks: + uri: + auth: + jwt: + customAuthHeader: customJwtHeader + passticket: + customUserHeader: customUserHeader + customAuthHeader: customPassticketHeader + provider: zosmf + zosmf: + serviceId: mockzosmf # Replace me with the correct z/OSMF service id + useDummyCNMapper: true +caching: + storage: + mode: infinispan + infinispan: + initialHosts: localhost[7600] + +jgroups: + bind: + port: 7601 + address: localhost + diff --git a/config/local/apiml-service.yml b/config/local/apiml-service.yml index 563962a57e..52396736a1 100644 --- a/config/local/apiml-service.yml +++ b/config/local/apiml-service.yml @@ -54,5 +54,13 @@ apiml: provider: zosmf zosmf: serviceId: mockzosmf # Replace me with the correct z/OSMF service id - +caching: + storage: + mode: infinispan + infinispan: + initialHosts: localhost[7601] +jgroups: + bind: + port: 7600 + address: localhost diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java index 7790758bcb..891d51253a 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java @@ -256,7 +256,7 @@ private InstanceInfo buildInstanceInfo(StaticRegistrationResult context, setInstanceAttributes(builder, service, instanceId, instanceBaseUrl, url, ipAddress, tile); setPort(builder, service, instanceBaseUrl, url); - log.info("Adding static instance {} for service ID {} mapped to URL {}", instanceId, serviceId, + log.debug("Adding static instance {} for service ID {} mapped to URL {}", instanceId, serviceId, url); final InstanceInfo instance = builder.build(); diff --git a/discovery-service/src/main/resources/application.yml b/discovery-service/src/main/resources/application.yml index cd54547221..403db89d74 100644 --- a/discovery-service/src/main/resources/application.yml +++ b/discovery-service/src/main/resources/application.yml @@ -20,7 +20,7 @@ logging: org.apache.http.conn.ssl.DefaultHostnameVerifier: DEBUG #logs only SSLException javax.net.ssl: ERROR org.apache.tomcat.util.net.SSLUtilBase: ERROR - # com.netflix.eureka.resources: WARN + com.netflix.eureka: ERROR apiml: # The `apiml` node contains API Mediation Layer specific configuration @@ -170,6 +170,7 @@ logging: org.ehcache: INFO com.netflix.discovery.shared.transport.decorator: DEBUG javax.net.ssl: ERROR + com.netflix.eureka: DEBUG --- spring.config.activate.on-profile: diag diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClient.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClient.java index cc77d504b3..75e3ecedbb 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClient.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClient.java @@ -14,111 +14,17 @@ import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.reactive.function.client.WebClient; -import org.zowe.apiml.product.gateway.GatewayClient; import reactor.core.publisher.Mono; -import static reactor.core.publisher.Mono.empty; -import static reactor.core.publisher.Mono.error; +public interface CachingServiceClient { -@Component("gatewayCachingServiceClient") -@Slf4j -public class CachingServiceClient { + Mono create(ApiKeyValue keyValue); - private static final String CACHING_SERVICE_RETURNED = ". Caching service returned: "; + Mono update(ApiKeyValue keyValue); - @Value("${apiml.cachingServiceClient.apiPath}") - private static final String CACHING_API_PATH = "/cachingservice/api/v1/cache"; //NOSONAR parametrization provided by @Value annotation + Mono read(String key); - private final String cachingBalancerUrl; - - private static final MultiValueMap defaultHeaders = new LinkedMultiValueMap<>(); - - static { - defaultHeaders.add("Content-Type", "application/json"); - } - - private final WebClient webClient; - - public CachingServiceClient( - @Qualifier("webClientClientCert") WebClient webClientClientCert, - GatewayClient gatewayClient - ) { - this.cachingBalancerUrl = String.format("%s://%s/%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), CACHING_API_PATH); - this.webClient = webClientClientCert; - } - - - public Mono create(KeyValue keyValue) { - return webClient.post() - .uri(cachingBalancerUrl) - .bodyValue(keyValue) - .headers(c -> c.addAll(defaultHeaders)) - .exchangeToMono(handler -> { - if (handler.statusCode().is2xxSuccessful()) { - return empty(); - } else { - return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to create caching key " + keyValue.getKey() + CACHING_SERVICE_RETURNED + handler.statusCode())); - } - }); - } - - public Mono update(KeyValue keyValue) { - return webClient.put() - .uri(cachingBalancerUrl) - .bodyValue(keyValue) - .headers(c -> c.addAll(defaultHeaders)) - .exchangeToMono(handler -> { - if (handler.statusCode().is2xxSuccessful()) { - return empty(); - } else { - return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to update caching key " + keyValue.getKey() + CACHING_SERVICE_RETURNED + handler.statusCode())); - } - }); - } - - public Mono read(String key) { - return webClient.get() - .uri(cachingBalancerUrl + "/" + key) - .headers(c -> c.addAll(defaultHeaders)) - .exchangeToMono(handler -> { - if (handler.statusCode().is2xxSuccessful()) { - return handler.bodyToMono(KeyValue.class); - } else if (handler.statusCode().is4xxClientError()) { - if (log.isTraceEnabled()) { - log.trace("Key with ID " + key + "not found. Status code from caching service: " + handler.statusCode()); - } - return empty(); - } else { - return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to read caching key " + key + CACHING_SERVICE_RETURNED + handler.statusCode())); - } - }); - } - - /** - * Deletes {@link KeyValue} from Caching Service - * - * @param key Key to delete - * @return mono with status success / error - */ - public Mono delete(String key) { - return webClient.delete() - .uri(cachingBalancerUrl + "/" + key) - .headers(c -> c.addAll(defaultHeaders)) - .exchangeToMono(handler -> { - if (handler.statusCode().is2xxSuccessful()) { - return empty(); - } else { - return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to delete caching key " + key + CACHING_SERVICE_RETURNED + handler.statusCode())); - } - }); - } + Mono delete(String key); /** * Data POJO that represents entry in caching service @@ -126,12 +32,12 @@ public Mono delete(String key) { @RequiredArgsConstructor @JsonInclude(JsonInclude.Include.NON_EMPTY) @Data - public static class KeyValue { + class ApiKeyValue { private final String key; private final String value; @JsonCreator - public KeyValue() { + public ApiKeyValue() { key = ""; value = ""; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientApi.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientApi.java new file mode 100644 index 0000000000..fc63aa900f --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientApi.java @@ -0,0 +1,96 @@ +/* + * 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.caching; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Component; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.caching.model.KeyValue; +import reactor.core.publisher.Mono; + +import static reactor.core.publisher.Mono.empty; + +/** + * {@code CachingServiceClientApi} is the internal implementation of {@link CachingServiceClient} + *

+ * Unlike {@code CachingServiceClientRest}, which makes HTTP requests to the Caching service, it directly uses the storage methods. + *

+ *

+ * This bean is only active when {@code modulithConfig} is present in the Spring context. + *

+ * + * @see CachingServiceClient + * @see CachingServiceClientRest + */ +@Component +@ConditionalOnBean(name = "modulithConfig") +@Slf4j +@RequiredArgsConstructor +public class CachingServiceClientApi implements CachingServiceClient { + private final Storage storage; + + @Override + public Mono create(ApiKeyValue keyValue) { + String serviceId = extractServiceId(keyValue.getKey()); + storage.create(serviceId, mapToApiKeyValue(keyValue)); + return empty(); + } + + @Override + public Mono update(ApiKeyValue keyValue) { + String serviceId = extractServiceId(keyValue.getKey()); + storage.update(serviceId, mapToApiKeyValue(keyValue)); + return empty(); + } + + @Override + public Mono read(String key) { + + String serviceId = extractServiceId(key); + KeyValue stored = storage.read(serviceId, key); + if (stored != null) { + return Mono.just(new ApiKeyValue(stored.getKey(), stored.getValue())); + } else { + return empty(); + } + + } + + @Override + public Mono delete(String key) { + String serviceId = extractServiceId(key); + storage.delete(serviceId, key); + return empty(); + + } + + private KeyValue mapToApiKeyValue(ApiKeyValue apiValue) { + return new KeyValue(apiValue.getKey(), apiValue.getValue()); + } + + /** + * Extracts serviceId from the full key + */ + private String extractServiceId(String key) { + if (!key.startsWith(LoadBalancerCache.LOAD_BALANCER_KEY_PREFIX)) { + throw new IllegalArgumentException("Missing prefix in key: " + key); + } + String prefixRemoved = key.substring(LoadBalancerCache.LOAD_BALANCER_KEY_PREFIX.length()); + int colonPos = prefixRemoved.indexOf(':'); + if (colonPos == -1) { + throw new IllegalArgumentException("Invalid key format, cannot extract serviceId: " + key); + } + return prefixRemoved.substring(colonPos + 1); + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java new file mode 100644 index 0000000000..3e750d0262 --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java @@ -0,0 +1,128 @@ +/* + * 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.caching; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.WebClient; +import org.zowe.apiml.product.gateway.GatewayClient; +import reactor.core.publisher.Mono; + +import static reactor.core.publisher.Mono.empty; +import static reactor.core.publisher.Mono.error; + +@Component +@Slf4j +@ConditionalOnMissingBean(name = "modulithConfig") +public class CachingServiceClientRest implements CachingServiceClient { + + private static final String CACHING_SERVICE_RETURNED = ". Caching service returned: "; + + @Value("${apiml.cachingServiceClient.apiPath:/cachingservice/api/v1/cache}") + private String CACHING_API_PATH; + + private String cachingBalancerUrl; + private final GatewayClient gatewayClient; + + private static final MultiValueMap defaultHeaders = new LinkedMultiValueMap<>(); + + static { + defaultHeaders.add("Content-Type", "application/json"); + } + + private final WebClient webClient; + + public CachingServiceClientRest( + @Qualifier("webClientClientCert") WebClient webClientClientCert, + GatewayClient gatewayClient + ) { + this.gatewayClient = gatewayClient; + this.webClient = webClientClientCert; + } + + @PostConstruct + void updateUrl() { + this.cachingBalancerUrl = String.format("%s://%s/%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), CACHING_API_PATH); + } + + + public Mono create(ApiKeyValue keyValue) { + return webClient.post() + .uri(cachingBalancerUrl) + .bodyValue(keyValue) + .headers(c -> c.addAll(defaultHeaders)) + .exchangeToMono(handler -> { + if (handler.statusCode().is2xxSuccessful()) { + return empty(); + } else { + return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to create caching key " + keyValue.getKey() + CACHING_SERVICE_RETURNED + handler.statusCode())); + } + }); + } + + public Mono update(ApiKeyValue keyValue) { + return webClient.put() + .uri(cachingBalancerUrl) + .bodyValue(keyValue) + .headers(c -> c.addAll(defaultHeaders)) + .exchangeToMono(handler -> { + if (handler.statusCode().is2xxSuccessful()) { + return empty(); + } else { + return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to update caching key " + keyValue.getKey() + CACHING_SERVICE_RETURNED + handler.statusCode())); + } + }); + } + + public Mono read(String key) { + return webClient.get() + .uri(cachingBalancerUrl + "/" + key) + .headers(c -> c.addAll(defaultHeaders)) + .exchangeToMono(handler -> { + if (handler.statusCode().is2xxSuccessful()) { + return handler.bodyToMono(ApiKeyValue.class); + } else if (handler.statusCode().is4xxClientError()) { + if (log.isTraceEnabled()) { + log.trace("Key with ID {}not found. Status code from caching service: {}", key, handler.statusCode()); + } + return empty(); + } else { + return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to read caching key " + key + CACHING_SERVICE_RETURNED + handler.statusCode())); + } + }); + } + + /** + * Deletes {@link ApiKeyValue} from Caching Service + * + * @param key Key to delete + * @return mono with status success / error + */ + public Mono delete(String key) { + return webClient.delete() + .uri(cachingBalancerUrl + "/" + key) + .headers(c -> c.addAll(defaultHeaders)) + .exchangeToMono(handler -> { + if (handler.statusCode().is2xxSuccessful()) { + return empty(); + } else { + return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to delete caching key " + key + CACHING_SERVICE_RETURNED + handler.statusCode())); + } + }); + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java index 6e7a6e9a5e..dd56cc99f3 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java @@ -19,18 +19,14 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.hc.core5.http.HttpStatus; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import org.zowe.apiml.gateway.caching.CachingServiceClient.KeyValue; import reactor.core.publisher.Mono; import java.time.LocalDateTime; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import static reactor.core.publisher.Mono.empty; -import static reactor.core.publisher.Mono.error; -import static reactor.core.publisher.Mono.just; +import static reactor.core.publisher.Mono.*; @Component @Slf4j @@ -54,8 +50,7 @@ public LoadBalancerCache( mapper.registerModule(new JavaTimeModule()); } - @Cacheable - private Mono cachingServiceAvailavility() { + private Mono cachingServiceAvailability() { return Mono.fromCallable(() -> eurekaClient.getApplication(CACHING_SERVICE_ID)) .map(app -> !app.getInstances().isEmpty()) .switchIfEmpty(Mono.just(false)); @@ -71,7 +66,7 @@ private Mono cachingServiceAvailavility() { * @return Mono success / error */ public Mono store(String user, String service, LoadBalancerCacheRecord loadBalancerCacheRecord) { - return cachingServiceAvailavility() + return cachingServiceAvailability() .flatMap(available -> { if (Boolean.TRUE.equals(available)) { return storeToRemoteCache(user, service, loadBalancerCacheRecord); @@ -86,31 +81,31 @@ public Mono store(String user, String service, LoadBalancerCacheRecord loa private Mono storeToRemoteCache(String user, String service, LoadBalancerCacheRecord loadBalancerCacheRecord) { try { String serializedRecord = mapper.writeValueAsString(loadBalancerCacheRecord); - CachingServiceClient.KeyValue toStore = new KeyValue(getKey(user, service), serializedRecord); + CachingServiceClient.ApiKeyValue toStore = new CachingServiceClient.ApiKeyValue(getKey(user, service), serializedRecord); return createToRemoteCache(user, service, loadBalancerCacheRecord, toStore); } catch (JsonProcessingException e) { - log.debug("Failed to serialize record for user: {}, service: {}, record {}, with exception: {}", user, service, loadBalancerCacheRecord, e); + log.debug("Failed to serialize record for user: {}, service: {}, record {}, with exception: ", user, service, loadBalancerCacheRecord, e); return error(e); } } - private Mono createToRemoteCache(String user, String service, LoadBalancerCacheRecord loadBalancerCacheRecord, CachingServiceClient.KeyValue toStore) { + private Mono createToRemoteCache(String user, String service, LoadBalancerCacheRecord loadBalancerCacheRecord, CachingServiceClient.ApiKeyValue toStore) { return remoteCache.create(toStore) .onErrorResume(createException -> { if (isCausedByCacheConflict(createException)) { return updateToRemoteCache(user, service, loadBalancerCacheRecord, toStore); } else { - log.debug("Failed to create record for user: {}, service: {}, record {}, with exception: {}", user, service, loadBalancerCacheRecord, createException); + log.debug("Failed to create record for user: {}, service: {}, record {}, with exception: ", user, service, loadBalancerCacheRecord, createException); return error(createException); } }) .doOnSuccess(v -> log.debug("Created record to remote cache for user: {}, service: {}, record: {}", user, service, loadBalancerCacheRecord)); } - private Mono updateToRemoteCache(String user, String service, LoadBalancerCacheRecord loadBalancerCacheRecord, CachingServiceClient.KeyValue toStore) { + private Mono updateToRemoteCache(String user, String service, LoadBalancerCacheRecord loadBalancerCacheRecord, CachingServiceClient.ApiKeyValue toStore) { return remoteCache.update(toStore) .doOnSuccess(v -> log.debug("Updated record to remote cache for user: {}, service: {}, record: {}", user, service, loadBalancerCacheRecord)) - .doOnError(updateException -> log.debug("Failed to update record for user: {}, service: {}, record {}, with exception: {}", user, service, loadBalancerCacheRecord, updateException)); + .doOnError(updateException -> log.debug("Failed to update record for user: {}, service: {}, record {}, with exception: ", user, service, loadBalancerCacheRecord, updateException)); } private boolean isCausedByCacheConflict(Throwable e) { @@ -125,20 +120,21 @@ private boolean isCausedByCacheConflict(Throwable e) { * @return Retrieved record containing the instance to use for this user and its creation time. */ public Mono retrieve(String user, String service) { - return cachingServiceAvailavility() + return cachingServiceAvailability() .flatMap(available -> { if (Boolean.TRUE.equals(available)) { return remoteCache.read(getKey(user, service)) - .map(kv -> { - LoadBalancerCacheRecord loadBalancerCacheRecord; - try { - loadBalancerCacheRecord = mapper.readValue(kv.getValue(), LoadBalancerCacheRecord.class); - } catch (JsonProcessingException e) { - throw new LoadBalancerCacheException(e); - } - log.debug("Retrieved record from remote cache for user: {}, service: {}, record: {}", user, service, loadBalancerCacheRecord); - return loadBalancerCacheRecord; - }); + .handle((kv, sink) -> { + LoadBalancerCacheRecord loadBalancerCacheRecord; + try { + loadBalancerCacheRecord = mapper.readValue(kv.getValue(), LoadBalancerCacheRecord.class); + } catch (JsonProcessingException e) { + sink.error(new LoadBalancerCacheException(e)); + return; + } + log.debug("Retrieved record from remote cache for user: {}, service: {}, record: {}", user, service, loadBalancerCacheRecord); + sink.next(loadBalancerCacheRecord); + }); } else { LoadBalancerCacheRecord loadBalancerCacheRecord = localCache.get(getKey(user, service)); log.debug("Retrieved record from local cache for user: {}, service: {}, record: {}", user, service, loadBalancerCacheRecord); @@ -154,7 +150,7 @@ public Mono retrieve(String user, String service) { * @param service Service towards which is the user routed */ public Mono delete(String user, String service) { - return cachingServiceAvailavility() + return cachingServiceAvailability() .flatMap(available -> { if (Boolean.TRUE.equals(available)) { return remoteCache.delete(getKey(user, service)) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientApiTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientApiTest.java new file mode 100644 index 0000000000..69214ec80d --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientApiTest.java @@ -0,0 +1,113 @@ +/* + * 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.caching; + +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.cache.StorageException; +import org.zowe.apiml.caching.model.KeyValue; +import reactor.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CachingServiceClientApiTest { + + public static final String LB_USER_SERVICE = "lb.user:service"; + @Mock + Storage storage; + + @InjectMocks + CachingServiceClientApi client; + + CachingServiceClient.ApiKeyValue sampleKv = new CachingServiceClient.ApiKeyValue(LB_USER_SERVICE, "value"); + KeyValue mappedKv = new KeyValue(LB_USER_SERVICE, "value"); + + @Nested + class CreateTests { + + @Test + void success() throws StorageException { + + StepVerifier.create(client.create(sampleKv)) + .verifyComplete(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(KeyValue.class); + verify(storage).create(eq("service"), captor.capture()); + + KeyValue captured = captor.getValue(); + assertEquals(sampleKv.getKey(), captured.getKey()); + assertEquals(sampleKv.getValue(), captured.getValue()); + } + + } + + @Nested + class UpdateTests { + + @Test + void success() throws StorageException { + + StepVerifier.create(client.update(sampleKv)) + .verifyComplete(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(KeyValue.class); + verify(storage).update(eq("service"), captor.capture()); + + KeyValue captured = captor.getValue(); + assertEquals(LB_USER_SERVICE, captured.getKey()); + assertEquals("value", captured.getValue()); + } + + } + + @Nested + class ReadTests { + + @Test + void success_thenReturnValue() throws StorageException { + when(storage.read("service", LB_USER_SERVICE)).thenReturn(mappedKv); + + StepVerifier.create(client.read(LB_USER_SERVICE)) + .expectNext(sampleKv) + .verifyComplete(); + } + + @Test + void notFound_thenReturnEmpty() throws StorageException { + when(storage.read("service", LB_USER_SERVICE)).thenReturn(null); + + StepVerifier.create(client.read(LB_USER_SERVICE)) + .verifyComplete(); + } + + } + + @Nested + class DeleteTests { + + @Test + void success() throws StorageException { + StepVerifier.create(client.delete(LB_USER_SERVICE)) + .verifyComplete(); + + verify(storage).delete("service", LB_USER_SERVICE); + } + + } +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientRestTest.java similarity index 87% rename from gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientTest.java rename to gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientRestTest.java index a4e8d23d1f..376dfe9286 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/CachingServiceClientRestTest.java @@ -25,7 +25,7 @@ import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFunction; import org.springframework.web.reactive.function.client.WebClient; -import org.zowe.apiml.gateway.caching.CachingServiceClient.KeyValue; +import org.zowe.apiml.gateway.caching.CachingServiceClient.ApiKeyValue; import org.zowe.apiml.gateway.caching.LoadBalancerCache.LoadBalancerCacheRecord; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; @@ -34,14 +34,11 @@ import java.util.function.Predicate; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static reactor.core.publisher.Mono.empty; import static reactor.core.publisher.Mono.just; - @ExtendWith(MockitoExtension.class) -class CachingServiceClientTest { +class CachingServiceClientRestTest { @Mock private ExchangeFunction exchangeFunction; @@ -49,7 +46,7 @@ class CachingServiceClientTest { @Mock private ClientResponse clientResponse; - private CachingServiceClient client; + private CachingServiceClientRest client; private WebClient webClient; private ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); @@ -57,7 +54,7 @@ class CachingServiceClientTest { @BeforeEach void setUp() { webClient = spy(WebClient.builder().exchangeFunction(exchangeFunction).build()); - client = new CachingServiceClient(webClient, new GatewayClient(ServiceAddress.builder().build())); + client = new CachingServiceClientRest(webClient, new GatewayClient(ServiceAddress.builder().build())); lenient().when(clientResponse.releaseBody()).thenReturn(empty()); } @@ -85,7 +82,7 @@ class WhenCreate { @Test void andServerSuccess_thenSuccess() throws JsonProcessingException { var cacheRecord = new LoadBalancerCacheRecord("instanceId"); - var kv = new KeyValue("lb.anuser:aservice", mapper.writeValueAsString(cacheRecord)); + var kv = new ApiKeyValue("lb.anuser:aservice", mapper.writeValueAsString(cacheRecord)); mockResponse(200); @@ -97,7 +94,7 @@ void andServerSuccess_thenSuccess() throws JsonProcessingException { @Test void andServerError_thenError() throws JsonProcessingException { var cacheRecord = new LoadBalancerCacheRecord("instanceId"); - var kv = new KeyValue("lb.anuser:aservice", mapper.writeValueAsString(cacheRecord)); + var kv = new ApiKeyValue("lb.anuser:aservice", mapper.writeValueAsString(cacheRecord)); mockResponse(500); @@ -108,7 +105,7 @@ void andServerError_thenError() throws JsonProcessingException { @Test void andClientError_thenError() throws JsonProcessingException { var cacheRecord = new LoadBalancerCacheRecord("instanceId"); - var kv = new KeyValue("lb.anuser:aservice", mapper.writeValueAsString(cacheRecord)); + var kv = new ApiKeyValue("lb.anuser:aservice", mapper.writeValueAsString(cacheRecord)); mockResponse(404); @@ -156,8 +153,8 @@ class WhenRead { @Test void andServerSuccess_thenSuccessAndContent() { mockResponse(200); - var kv = new KeyValue("key", "value"); - when(clientResponse.bodyToMono(KeyValue.class)).thenReturn(just(kv)); + var kv = new ApiKeyValue("key", "value"); + when(clientResponse.bodyToMono(ApiKeyValue.class)).thenReturn(just(kv)); StepVerifier.create(client.read("key")) .expectNext(kv) @@ -198,7 +195,7 @@ class WhenUpdate { @Test void andServerSuccess_thenSucess() { mockResponse(200); - var kv = new KeyValue("key", "value"); + var kv = new ApiKeyValue("key", "value"); StepVerifier.create(client.update(kv)) .expectComplete() @@ -208,7 +205,7 @@ void andServerSuccess_thenSucess() { @Test void andServerError_thenError() { mockResponse(500); - var kv = new KeyValue("key", "value"); + var kv = new ApiKeyValue("key", "value"); StepVerifier.create(client.update(kv)) .verifyErrorMatches(assertCachingServiceClientException(500)); @@ -217,7 +214,7 @@ void andServerError_thenError() { @Test void andClientError_thenError() { mockResponse(404); - var kv = new KeyValue("key", "value"); + var kv = new ApiKeyValue("key", "value"); StepVerifier.create(client.update(kv)) .verifyErrorMatches(assertCachingServiceClientException(404)); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java index 45b6846a1b..f701a04a15 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java @@ -24,7 +24,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.gateway.caching.CachingServiceClient.KeyValue; import org.zowe.apiml.gateway.caching.LoadBalancerCache.LoadBalancerCacheRecord; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -34,18 +33,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import static reactor.core.publisher.Mono.empty; -import static reactor.core.publisher.Mono.error; -import static reactor.core.publisher.Mono.just; +import static org.mockito.Mockito.*; +import static reactor.core.publisher.Mono.*; @ExtendWith(MockitoExtension.class) class LoadBalancerCacheTest { @Mock - private CachingServiceClient cachingServiceClient; + private CachingServiceClientRest cachingServiceClient; @Mock private Map map; @@ -88,7 +84,7 @@ class WhenCreate { @Test void andSuccess_thenSuccess() throws JsonProcessingException { var cacheRecord = new LoadBalancerCacheRecord("instance1"); - when(cachingServiceClient.create(new KeyValue("lb.anuser:aserviceid", mapper.writeValueAsString(cacheRecord)))) + when(cachingServiceClient.create(new CachingServiceClient.ApiKeyValue("lb.anuser:aserviceid", mapper.writeValueAsString(cacheRecord)))) .thenReturn(empty()); StepVerifier.create(loadBalancerCache.store("anuser", "aserviceid", cacheRecord)) @@ -99,7 +95,7 @@ void andSuccess_thenSuccess() throws JsonProcessingException { @Test void andGenericError_thenError() throws JsonProcessingException { var cacheRecord = new LoadBalancerCacheRecord("instance1"); - when(cachingServiceClient.create(new KeyValue("lb.anuser:aserviceid", mapper.writeValueAsString(cacheRecord)))) + when(cachingServiceClient.create(new CachingServiceClient.ApiKeyValue("lb.anuser:aserviceid", mapper.writeValueAsString(cacheRecord)))) .thenReturn(error(new CachingServiceClientException(500, "error"))); StepVerifier.create(loadBalancerCache.store("anuser", "aserviceid", cacheRecord)) @@ -110,7 +106,7 @@ void andGenericError_thenError() throws JsonProcessingException { @Test void andErrorCacheConflict_thenError() throws JsonProcessingException { var cacheRecord = new LoadBalancerCacheRecord("instance1"); - var keyValue = new KeyValue("lb.anuser:aserviceid", mapper.writeValueAsString(cacheRecord)); + var keyValue = new CachingServiceClient.ApiKeyValue("lb.anuser:aserviceid", mapper.writeValueAsString(cacheRecord)); when(cachingServiceClient.create(keyValue)) .thenReturn(error(new CachingServiceClientException(409, "error"))); @@ -154,7 +150,7 @@ class WhenRetrieve { void andSuccess_thenReturnValue() throws JsonProcessingException { var key = "lb.anuser:aserviceid"; var cacheRecord = new LoadBalancerCacheRecord("instanceId"); - var keyValue = new KeyValue(key, mapper.writeValueAsString(cacheRecord)); + var keyValue = new CachingServiceClient.ApiKeyValue(key, mapper.writeValueAsString(cacheRecord)); when(cachingServiceClient.read(key)).thenReturn(just(keyValue)); StepVerifier.create(loadBalancerCache.retrieve("anuser", "aserviceid")) diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index e7c7465930..2cf06962fa 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -335,7 +335,8 @@ task runContainerModulithTests(type: Test) { 'GatewayServiceRouting', 'GatewayCentralRegistry', 'ZaasTest', - 'ApiCatalogStandaloneTest' + 'ApiCatalogStandaloneTest', + 'NonModulithTest' ) } } @@ -570,6 +571,24 @@ task runInfinispanServiceTests(type: Test) { } } +task runModulithInfinispanServiceTests(type: Test) { + group "integration tests" + description "Run APIML(modulith) with infinispan storage tests" + + outputs.cacheIf { false } + + systemProperties System.getProperties() + useJUnitPlatform { + includeTags( + 'CachingServiceTest', + 'InfinispanStorageTest' + ) + excludeTags( + 'NonModulithTest' + ) + } +} + task runHATests(type: Test) { group "Integration tests" description "Run tests verifying High Availability" diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/caching/CachingAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/caching/CachingAuthenticationTest.java index 32e4c76786..bc25a5231d 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/caching/CachingAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/caching/CachingAuthenticationTest.java @@ -23,6 +23,7 @@ import org.springframework.http.HttpStatus; import org.zowe.apiml.util.TestWithStartedInstances; import org.zowe.apiml.util.categories.CachingServiceTest; +import org.zowe.apiml.util.categories.NonModulithTest; import org.zowe.apiml.util.categories.NotAttlsTest; import org.zowe.apiml.util.config.ConfigReader; import org.zowe.apiml.util.config.ItSslConfigFactory; @@ -76,6 +77,7 @@ static Stream publicUrls() { class WhenCalledWithInvalidAuthentication { // Candidates for parametrized test. + @NonModulithTest @ParameterizedTest @MethodSource("org.zowe.apiml.functional.caching.CachingAuthenticationTest#publicUrls") void publicEndpointIsAccessible(String endpoint) { @@ -104,9 +106,11 @@ void givenNoCertificateAndHeader_cachingApiEndpointsAreInaccessible() { .when() .get(caching_url + CACHING_PATH) .then() + .log().ifValidationFails() .statusCode(HttpStatus.FORBIDDEN.value()); } + @NonModulithTest @Test void givenCertificateButNoHeader_cachingApiEndpointsAreInaccessible() { diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/AccessTokenServiceTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/AccessTokenServiceTest.java index 7610d336c6..d4581ba661 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/AccessTokenServiceTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/AccessTokenServiceTest.java @@ -69,7 +69,7 @@ void givenValidToken_invalidateTheToken() { .body(bodyContent) .when() .delete(REVOKE_ENDPOINT) - .then() + .then().log().ifValidationFails() .statusCode(204); IntStream.range(0, 3).forEach(x -> { given() @@ -89,7 +89,7 @@ void givenTokenInvalidated_returnUnauthorized() { .body(bodyContent) .when() .delete(REVOKE_ENDPOINT) - .then() + .then().log().ifValidationFails() .statusCode(204); IntStream.range(0, 3).forEach(x -> { given() @@ -97,7 +97,7 @@ void givenTokenInvalidated_returnUnauthorized() { .body(bodyContent) .when() .delete(REVOKE_ENDPOINT) - .then() + .then().log().ifValidationFails() .statusCode(401); }); } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java index fdbab3663a..7263565ab5 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java @@ -230,7 +230,7 @@ void whenCallPassTicketService(String tokenType, int status, Matcher mat .get(discoverablePassticketUrl) .then() .header(ApimlConstants.AUTH_FAIL_HEADER, matcher) - .log().all() + .log().ifValidationFails() .statusCode(is(status)); //@formatter:on } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java index 34bf6f15a3..0af6c84679 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java @@ -31,7 +31,8 @@ import java.util.Set; import static io.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.core.Is.is; import static org.zowe.apiml.util.SecurityUtils.gatewayToken; import static org.zowe.apiml.util.SecurityUtils.personalAccessToken; @@ -126,7 +127,7 @@ void forwardJWTToService() { @Test void preserveCookies() { String jwt = gatewayToken(); - Cookie.Builder builder = new Cookie.Builder("XSRF-TOKEN","another-token-in-cookies"); + Cookie.Builder builder = new Cookie.Builder("XSRF-TOKEN", "another-token-in-cookies"); Cookies cookies = new Cookies(builder.build()); given() .config(SslContext.tlsWithoutCert) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/external/CachingStorageTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/external/CachingStorageTest.java index 8e1141479e..fa5ec95d08 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/external/CachingStorageTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/external/CachingStorageTest.java @@ -317,7 +317,7 @@ void givenValidKeyAndCertificate() { .contentType(JSON) .when() .get(CACHING_PATH) - .then().log().all() + .then().log().ifValidationFails() .body("testKey3", is(not(is(emptyString()))), "testKey4", is(not(is(emptyString()))), "testKey1", is(emptyOrNullString()), @@ -433,7 +433,7 @@ void givenValidServiceParameter() { .when() .delete(CACHING_PATH) .then() - .log().all() + .log().ifValidationFails() .statusCode(is(SC_OK)); given().config(clientCert) diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java index 9c1dd056ca..ae40478d39 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java @@ -396,7 +396,6 @@ public static String personalAccessToken(Set scopes) { SSLConfig originalConfig = RestAssured.config().getSSLConfig(); RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); - try { return given() .contentType(JSON).header("Authorization", "Basic " + Base64.encode(USERNAME + ":" + PASSWORD)) @@ -421,7 +420,7 @@ public static String personalAccessTokenWithClientCert(RestAssuredConfig sslConf SSLConfig originalConfig = RestAssured.config().getSSLConfig(); try { - return given().config(sslConfig) + return given().config(sslConfig).contentType(JSON) .body(accessTokenRequest) .when() .post(gatewayGenerateAccessTokenEndpoint) diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/categories/NonModulithTest.java b/integration-tests/src/test/java/org/zowe/apiml/util/categories/NonModulithTest.java new file mode 100644 index 0000000000..6f0d15d6e9 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/util/categories/NonModulithTest.java @@ -0,0 +1,30 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.util.categories; + +import org.junit.jupiter.api.Tag; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +/** + * A category marker for tests that are only for microservice deployment. + * The functionality that is being tested is not available in the modulith. + */ +@Tag("NonModulithTest") +@Target({ TYPE, METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NonModulithTest { +} 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 3bbc3c7d01..8c1e1b6905 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 @@ -10,6 +10,7 @@ package org.zowe.apiml.util.service; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.zowe.apiml.startup.impl.ApiMediationLayerStartupChecker; @@ -43,23 +44,21 @@ public class FullApiMediationLayer { private boolean firstCheck = true; private final Map env; private static final boolean attlsEnabled = "true".equals(System.getProperty("environment.attls")); - private static final boolean IS_MODULITH_ENABLED = Boolean.parseBoolean(System.getProperty("environment.modulith")); + @Getter private static final FullApiMediationLayer instance = new FullApiMediationLayer(); private FullApiMediationLayer() { env = ConfigReader.environmentConfiguration().getInstanceEnv(); - prepareCaching(); prepareCatalog(); prepareDiscoverableClient(); prepareGateway(); prepareMockServices(); prepareDiscovery(); - if (!IS_MODULITH_ENABLED) { - prepareZaas(); - } + prepareCaching(); + prepareZaas(); prepareApiml(); if (!attlsEnabled) { prepareNodeJsSampleApp(); @@ -129,10 +128,6 @@ private void prepareDiscoverableClient() { discoverableClientService = new RunningService("discoverableclient", "discoverable-client/build/libs/discoverable-client.jar", before, after); } - public static FullApiMediationLayer getInstance() { - return instance; - } - public void start() { try { var discoveryEnv = new HashMap<>(env); @@ -162,7 +157,7 @@ public void start() { mockZosmfService.start(); log.info("Services started"); } catch (IOException ex) { - log.error("error while starting services: " + ex.getMessage(), ex.getCause()); + log.error("error while starting services: {}", ex.getMessage(), ex.getCause()); } } diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java b/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java index c3d4d4c1d0..53eafa9319 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java @@ -42,7 +42,7 @@ public RunningService(String id, String jarFile, Map parametersB } public void start(String... envs) throws IOException { - log.info("Starting new Service with JAR file {} and ID {}", jarFile, id); + log.info("Starting new Service via shell command with JAR file {} and ID {}", jarFile, id); stop(); ArrayList shellCommand = new ArrayList<>(); @@ -76,7 +76,7 @@ public void start(String... envs) throws IOException { } public void startWithScript(String binPath, Map env) { - log.info("Starting new Service with JAR file {} and ID {}", jarFile, id); + log.info("Starting new Service via start.sh script with JAR file {} and ID {}", jarFile, id); ProcessBuilder builder1 = new ProcessBuilder(binPath + "/start.sh"); Map envVariables = builder1.environment(); envVariables.putAll(env); diff --git a/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/config/EnableApiDiscoveryConfig.java b/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/config/EnableApiDiscoveryConfig.java index 9a8355cb3e..fbc7010df9 100644 --- a/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/config/EnableApiDiscoveryConfig.java +++ b/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/config/EnableApiDiscoveryConfig.java @@ -33,6 +33,7 @@ public class EnableApiDiscoveryConfig { @Bean + @ConditionalOnMissingBean public MessageService messageServiceDiscovery() { MessageService messageService = YamlMessageServiceInstance.getInstance(); messageService.loadMessages("/onboarding-enabler-spring-messages.yml"); diff --git a/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/register/RegisterToApiLayer.java b/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/register/RegisterToApiLayer.java index f289ef3662..7be8522ec1 100644 --- a/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/register/RegisterToApiLayer.java +++ b/onboarding-enabler-spring/src/main/java/org/zowe/apiml/enable/register/RegisterToApiLayer.java @@ -10,20 +10,19 @@ package org.zowe.apiml.enable.register; -import org.zowe.apiml.eurekaservice.client.ApiMediationClient; -import org.zowe.apiml.eurekaservice.client.config.ApiMediationServiceConfig; - -import org.zowe.apiml.exception.ServiceDefinitionException; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; - -import lombok.extern.slf4j.Slf4j; +import org.zowe.apiml.eurekaservice.client.ApiMediationClient; +import org.zowe.apiml.eurekaservice.client.config.ApiMediationServiceConfig; +import org.zowe.apiml.exception.ServiceDefinitionException; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.message.yaml.YamlMessageService; @Slf4j @@ -40,20 +39,25 @@ public class RegisterToApiLayer { private final ApiMediationClient apiMediationClient; private final ApiMediationServiceConfig newConfig; - private ApiMediationServiceConfig config; + private ApiMediationServiceConfig config; @Value("${apiml.enabled:true}") private boolean apimlEnabled; - @InjectApimlLogger - private final ApimlLogger logger = ApimlLogger.empty(); + private static final MessageService messages = new YamlMessageService(); + private static final ApimlLogger logger; + + static { + messages.loadMessages("/onboarding-enabler-spring-messages.yml"); + logger = new ApimlLogger(RegisterToApiLayer.class, messages); + } @EventListener(ContextRefreshedEvent.class) public void onContextRefreshedEventEvent() { if (apimlEnabled) { if (apiMediationClient.getEurekaClient() != null) { if (config != null) { - logger.log( "org.zowe.apiml.enabler.registration.renew" + logger.log("org.zowe.apiml.enabler.registration.renew" , config.getBaseUrl(), config.getServiceIpAddress(), config.getDiscoveryServiceUrls() , newConfig.getBaseUrl(), newConfig.getServiceIpAddress(), newConfig.getDiscoveryServiceUrls() ); diff --git a/package.json b/package.json index 391700d4d2..592c2bc6ec 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "jvmArgs": "--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.nio.channels.spi=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/javax.net.ssl=ALL-UNNAMED" }, "scripts": { - "api-layer-modulith": "concurrently --names \"AC,DC,ZO,CS,AL\" -c yellow,white,blue,green,orange npm:api-catalog-service npm:discoverable-client npm:mock-services npm:caching-service npm:apiml-service", + "api-layer-modulith": "concurrently --names \"AC,DC,ZO,AL\" -c yellow,white,blue,orange npm:api-catalog-service npm:discoverable-client npm:mock-services npm:apiml-service", "api-layer": "concurrently --names \"AZ,AD,AC,DC,ZO,CS,AG\" -c cyan,yellow,white,blue,green,orange,red npm:zaas-service npm:discovery-service npm:api-catalog-service npm:discoverable-client npm:mock-services npm:caching-service npm:gateway-service", "api-layer-ci": "concurrently --names \"AZ,AD,AC,DC,ZO,CS,NS,AG\" -c cyan,yellow,white,blue,green,red,orange,brown npm:zaas-service-thin npm:discovery-service-thin npm:api-catalog-service-thin npm:discoverable-client npm:mock-services npm:caching-service npm:onboarding-enabler-nodejs-sample-app npm:gateway-service", "api-layer-core": "concurrently --names \"AZ,AD,AC,AG\" -c cyan,yellow,white npm:zaas-service npm:discovery-service npm:api-catalog-service npm:gateway-service", diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java index 53b90d65c6..369f6131a9 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java @@ -51,7 +51,7 @@ public void run() { } } - private void notifyStartup() { + public void notifyStartup() { new ServiceStartupEventHandler().onServiceStartup("ZAAS", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); publisher.publishEvent(new ZaasServiceAvailableEvent(providers.isZosfmUsed() ? "zosmf" : "saf")); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingClient.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingClient.java new file mode 100644 index 0000000000..35d9bc3c42 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingClient.java @@ -0,0 +1,32 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.cache; + +import java.util.Map; + +public interface CachingClient { + + void create(CachingServiceClient.KeyValue kv); + + void appendList(String mapKey, CachingServiceClient.KeyValue kv); + + Map> readAllMaps(); + + void evictTokens(String key); + + void evictRules(String key); + + CachingServiceClient.KeyValue read(String key); + + void update(CachingServiceClient.KeyValue kv); + + void delete(String key); +} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java index effa6f0fa9..91339e0ff6 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/CachingServiceClient.java @@ -35,14 +35,14 @@ */ @Slf4j @SuppressWarnings({"squid:S1192"}) // literals are repeating in debug logs only -public class CachingServiceClient { +public class CachingServiceClient implements CachingClient { private final GatewayClient gatewayClient; private final RestTemplate restTemplate; - @Value("${apiml.cachingServiceClient.apiPath}") - private static final String CACHING_API_PATH = "/cachingservice/api/v1/cache"; //NOSONAR parametrization provided by @Value annotation - @Value("${apiml.cachingServiceClient.list.apiPath}") - private static final String CACHING_LIST_API_PATH = "/cachingservice/api/v1/cache-list/"; //NOSONAR parametrization provided by @Value annotation + @Value("${apiml.cachingServiceClient.apiPath:/cachingservice/api/v1/cache}") + private String CACHING_API_PATH; + @Value("${apiml.cachingServiceClient.list.apiPath:/cachingservice/api/v1/cache-list/}") + private String CACHING_LIST_API_PATH; private static final HttpHeaders defaultHeaders = new HttpHeaders(); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/LocalCachingClient.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/LocalCachingClient.java new file mode 100644 index 0000000000..2001ca9117 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/cache/LocalCachingClient.java @@ -0,0 +1,83 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.cache; + +import lombok.RequiredArgsConstructor; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.caching.model.KeyValue; +import org.zowe.apiml.security.HttpsConfig; + +import java.util.Map; +import java.util.Optional; + +@RequiredArgsConstructor +public class LocalCachingClient implements CachingClient { + + private final Storage storage; + + private final HttpsConfig httpsConfig; + + @Override + public void create(CachingServiceClient.KeyValue kv) { + + var serviceId = getServiceId(); + storage.create(serviceId, new KeyValue(kv.getKey(), kv.getValue())); + } + + @Override + public void appendList(String mapKey, CachingServiceClient.KeyValue kv) { + storage.storeMapItem(getServiceId(), mapKey, convert(kv)); + } + + @Override + public Map> readAllMaps() { + return storage.getAllMaps(getServiceId()); + } + + @Override + public void evictTokens(String key) { + storage.removeNonRelevantTokens(getServiceId(), key); + } + + @Override + public void evictRules(String key) { + storage.removeNonRelevantRules(getServiceId(), key); + } + + @Override + public CachingServiceClient.KeyValue read(String key) { + return convert(storage.read(getServiceId(), key)); + } + + @Override + public void update(CachingServiceClient.KeyValue kv) { + storage.update(getServiceId(), convert(kv)); + } + + @Override + public void delete(String key) { + storage.delete(getServiceId(), key); + } + + KeyValue convert(CachingServiceClient.KeyValue kv) { + return new KeyValue(kv.getKey(), kv.getValue()); + } + + CachingServiceClient.KeyValue convert(KeyValue kv) { + return new CachingServiceClient.KeyValue(kv.getKey(), kv.getValue()); + } + + String getServiceId() { + return Optional.ofNullable(httpsConfig.getCertificate()) + .map(certificate -> certificate.getSubjectX500Principal().getName()) + .orElse("apiml service"); + } +} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/config/CacheConfig.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/config/CacheConfig.java index 3d71594326..061d3f5e07 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/config/CacheConfig.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/config/CacheConfig.java @@ -28,6 +28,7 @@ import org.ehcache.jsr107.EhcacheCachingProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; @@ -36,14 +37,19 @@ import org.springframework.cache.support.NoOpCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.http.HttpHeaders; import org.springframework.web.client.RestTemplate; import org.zowe.apiml.cache.CompositeKeyGenerator; import org.zowe.apiml.cache.CompositeKeyGeneratorWithoutLast; +import org.zowe.apiml.cache.Storage; import org.zowe.apiml.product.gateway.GatewayClient; +import org.zowe.apiml.security.HttpsConfig; import org.zowe.apiml.security.common.token.TokenAuthentication; import org.zowe.apiml.util.CacheUtils; +import org.zowe.apiml.zaas.cache.CachingClient; import org.zowe.apiml.zaas.cache.CachingServiceClient; +import org.zowe.apiml.zaas.cache.LocalCachingClient; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import javax.cache.Caching; @@ -89,8 +95,10 @@ public void afterPropertiesSet() { } } + @Primary @Bean("cacheManager") @ConditionalOnProperty(value = "apiml.caching.enabled", havingValue = "true", matchIfMissing = true) + @ConditionalOnProperty(name = "caching.storage.mode", havingValue = "inMemory", matchIfMissing = true) public CacheManager cacheManager() { var caches = new HashMap>(); @@ -174,6 +182,7 @@ public CacheManager cacheManager() { @ConditionalOnProperty(value = "apiml.caching.enabled", havingValue = "false") @Bean("cacheManager") + @ConditionalOnMissingBean(name = "modulithConfig") public CacheManager cacheManagerNoOp() { return new NoOpCacheManager(); } @@ -194,8 +203,15 @@ public CacheUtils cacheUtils() { } @Bean - public CachingServiceClient cachingServiceClient(GatewayClient gatewayClient, @Qualifier("restTemplateWithKeystore") RestTemplate restTemplate) { + @ConditionalOnMissingBean(name = "modulithConfig") + public CachingClient cachingServiceClient(GatewayClient gatewayClient, @Qualifier("restTemplateWithKeystore") RestTemplate restTemplate) { return new CachingServiceClient(restTemplate, gatewayClient); } + @Bean + @ConditionalOnMissingBean + public CachingClient cachingClient(Storage storage, HttpsConfig httpsConfig) { + return new LocalCachingClient(storage, httpsConfig); + } + } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProvider.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProvider.java index 9b5bf79b6b..ebc0b574ae 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProvider.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProvider.java @@ -16,9 +16,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import org.zowe.apiml.cache.StorageException; import org.zowe.apiml.models.AccessTokenContainer; import org.zowe.apiml.security.common.token.AccessTokenProvider; import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.zaas.cache.CachingClient; import org.zowe.apiml.zaas.cache.CachingServiceClient; import org.zowe.apiml.zaas.cache.CachingServiceClientException; import org.zowe.apiml.zaas.security.service.AuthenticationService; @@ -43,7 +45,7 @@ public class ApimlAccessTokenProvider implements AccessTokenProvider { static final String INVALID_USERS_KEY = "invalidUsers"; static final String INVALID_SCOPES_KEY = "invalidScopes"; - private final CachingServiceClient cachingServiceClient; + private final CachingClient cachingServiceClient; private final AuthenticationService authenticationService; @Qualifier("oidcJwkMapper") private final ObjectMapper objectMapper; @@ -149,12 +151,12 @@ public String getHash(String token) throws CachingServiceClientException { return getSecurePassword(token, getSalt()); } - private String initializeSalt() throws CachingServiceClientException,SecureTokenInitializationException { + private String initializeSalt() throws CachingServiceClientException, SecureTokenInitializationException { String localSalt; try { CachingServiceClient.KeyValue keyValue = cachingServiceClient.read("salt"); localSalt = keyValue.getValue(); - } catch (CachingServiceClientException e) { + } catch (CachingServiceClientException | StorageException e) { byte[] newSalt = generateSalt(); storeSalt(newSalt); localSalt = new String(newSalt); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java index 38f30b124a..afa641ee7d 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java @@ -128,7 +128,7 @@ public static class ZosmfInfo { private ZosmfService meAsProxy; private TokenCreationService tokenCreationService; - private DefaultResourceRetriever resourceRetriever; + private final DefaultResourceRetriever resourceRetriever; public ZosmfService( final AuthConfigurationProperties authConfigurationProperties, @@ -269,7 +269,7 @@ public boolean isAccessible() { final HttpHeaders headers = new HttpHeaders(); headers.add(ZOSMF_CSRF_HEADER, ""); - String infoURIEndpoint = ""; + String infoURIEndpoint; try { infoURIEndpoint = getURI(getZosmfServiceId(), ZOSMF_INFO_END_POINT); } catch (ServiceNotAccessibleException e) { @@ -289,7 +289,7 @@ public boolean isAccessible() { if (info.getStatusCode() != HttpStatus.OK) { log.error("Unexpected status code {} from z/OSMF accessing URI {}\n" - + "Response from z/OSMF was \"{}\"", info.getStatusCodeValue(), infoURIEndpoint, info.getBody()); + + "Response from z/OSMF was \"{}\"", info.getStatusCode(), infoURIEndpoint, info.getBody()); } return info.getStatusCode() == HttpStatus.OK; @@ -358,7 +358,7 @@ protected ResponseEntity issueChangePasswordRequest(Authentication authe throw new ServiceNotAccessibleException("Change password endpoint is not available in z/OSMF", e); } catch (HttpClientErrorException e) { // TODO https://github.com/zowe/api-layer/issues/2995 - API ML will return 401 in these cases now, the message is still not accurate - log.debug("Request to {} failed with status {}: {}", url, e.getRawStatusCode(), e.getMessage()); + log.debug("Request to {} failed with status {}: {}", url, e.getStatusCode(), e.getMessage()); throw new BadCredentialsException("Client error in change password: " + e.getResponseBodyAsString(), e); } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); @@ -390,7 +390,7 @@ private RuntimeException handleServerErrorOnChangePasswordCall(HttpServerErrorEx */ @Cacheable(value = "zosmfAuthenticationEndpoint", key = "#httpMethod.name()") public boolean authenticationEndpointExists(HttpMethod httpMethod, HttpHeaders headers) { - String url = ""; + String url; try { url = getURI(getZosmfServiceId(), ZOSMF_AUTHENTICATE_END_POINT); } catch (ServiceNotAccessibleException e) { @@ -407,8 +407,7 @@ public boolean authenticationEndpointExists(HttpMethod httpMethod, HttpHeaders h apimlLog.log("org.zowe.apiml.security.auth.zosmf.jwtNotFound"); return false; } else { - log.warn("z/OSMF authentication endpoint with HTTP method " + httpMethod.name() + - " has failed with status code: " + hce.getStatusCode(), hce); + log.warn("z/OSMF authentication endpoint with HTTP method {} has failed with status code: {}", httpMethod.name(), hce.getStatusCode(), hce); return false; } } catch (HttpServerErrorException serverError) { @@ -424,7 +423,7 @@ public boolean authenticationEndpointExists(HttpMethod httpMethod, HttpHeaders h */ @Cacheable(value = "zosmfJwtEndpoint") public boolean jwtEndpointExists(HttpHeaders headers) { - String url = ""; + String url; try { url = getURI(getZosmfServiceId(), authConfigurationProperties.getZosmf().getJwtEndpoint()); } catch (ServiceNotAccessibleException e) { @@ -534,7 +533,7 @@ public void invalidate(TokenType type, String token) { if (re.getStatusCode().is2xxSuccessful()) return; - apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getStatusCodeValue()); + apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getStatusCode()); throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); @@ -573,7 +572,7 @@ public JWKSet getPublicKeys() { } catch (HttpClientErrorException.NotFound nf) { log.debug("Cannot get public keys from z/OSMF", nf); } catch (IOException me) { - log.debug("Can't read JWK due to the exception " + me.getMessage(), me.getCause()); + log.debug("Can't read JWK due to the exception {}", me.getMessage(), me.getCause()); } return new JWKSet(); } diff --git a/zaas-service/src/main/resources/application.yml b/zaas-service/src/main/resources/application.yml index 17c9a2becd..dbd922531b 100644 --- a/zaas-service/src/main/resources/application.yml +++ b/zaas-service/src/main/resources/application.yml @@ -102,7 +102,9 @@ spring: ipAddress: ${apiml.service.ipAddress} gateway: mvc.enabled: false - enabled: false + server: + webflux: + enabled: false mvc: log-resolved-exception: false # Suppress useless logs from AbstractHandlerExceptionResolver output: diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/cache/CachingServiceClientTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/cache/CachingServiceClientTest.java index 9bcaa1f7d3..11d24333b6 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/cache/CachingServiceClientTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/cache/CachingServiceClientTest.java @@ -20,6 +20,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.zowe.apiml.models.AccessTokenContainer; @@ -44,6 +45,8 @@ void setUp() { ServiceAddress gatewayAddress = ServiceAddress.builder().scheme("https").hostname("localhost:10010").build(); GatewayClient gatewayClient = new GatewayClient(gatewayAddress); underTest = new CachingServiceClient(restTemplate, gatewayClient); + ReflectionTestUtils.setField(underTest,"CACHING_API_PATH","/cachingservice/api/v1/cache"); + ReflectionTestUtils.setField(underTest,"CACHING_LIST_API_PATH","/cachingservice/api/v1/cache-list/"); } @Nested diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/cache/InMemoryCachingClientTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/cache/InMemoryCachingClientTest.java new file mode 100644 index 0000000000..10339ec601 --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/cache/InMemoryCachingClientTest.java @@ -0,0 +1,129 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.cache; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.cache.Storage; +import org.zowe.apiml.caching.model.KeyValue; +import org.zowe.apiml.security.HttpsConfig; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class InMemoryCachingClientTest { + + private Storage storage; + private LocalCachingClient cachingClient; + private HttpsConfig httpsConfig; + + /** + * Subclass that overrides getServiceId() to avoid certificate initialization. + */ + static class TestLocalCachingClient extends LocalCachingClient { + TestLocalCachingClient(Storage storage, HttpsConfig httpsConfig) { + super(storage, httpsConfig); + } + + @Override + String getServiceId() { + return "CN=TestService"; + } + } + + @BeforeEach + void setUp() { + storage = mock(Storage.class); + httpsConfig = mock(HttpsConfig.class); + cachingClient = new TestLocalCachingClient(storage, httpsConfig); + } + + @Test + void testCreate() { + var kv = new CachingServiceClient.KeyValue("key1", "value1"); + + cachingClient.create(kv); + + verify(storage).create(eq("CN=TestService"), argThat(kv1 -> + kv1.getKey().equals("key1") && kv1.getValue().equals("value1"))); + } + + @Test + void testUpdate() { + var kv = new CachingServiceClient.KeyValue("key2", "value2"); + + cachingClient.update(kv); + + verify(storage).update(eq("CN=TestService"), argThat(kv1 -> + kv1.getKey().equals("key2") && kv1.getValue().equals("value2"))); + } + + @Test + void testDelete() { + cachingClient.delete("toDelete"); + + verify(storage).delete("CN=TestService", "toDelete"); + } + + @Test + void testRead() { + when(storage.read("CN=TestService", "keyX")) + .thenReturn(new KeyValue("keyX", "valueX")); + + var result = cachingClient.read("keyX"); + + assertNotNull(result); + assertEquals("keyX", result.getKey()); + assertEquals("valueX", result.getValue()); + } + + @Test + void testAppendList() { + var kv = new CachingServiceClient.KeyValue("listKey", "item1"); + + cachingClient.appendList("listMap", kv); + + verify(storage).storeMapItem(eq("CN=TestService"), eq("listMap"), argThat(kv1 -> + kv1.getKey().equals("listKey") && kv1.getValue().equals("item1"))); + } + + @Test + void testReadAllMaps() { + Map> mockData = Map.of("map1", Map.of("k1", "v1")); + when(storage.getAllMaps("CN=TestService")).thenReturn(mockData); + + var result = cachingClient.readAllMaps(); + + assertEquals(mockData, result); + } + + @Test + void testEvictTokens() { + cachingClient.evictTokens("userToken"); + + verify(storage).removeNonRelevantTokens("CN=TestService", "userToken"); + } + + @Test + void testEvictRules() { + cachingClient.evictRules("ruleKey"); + + verify(storage).removeNonRelevantRules("CN=TestService", "ruleKey"); + } +} From f62f638f62a5911d1ba042f3d0c2685f24753667 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:35:01 +0200 Subject: [PATCH 021/152] fix: unresponsive eureka (#4223) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 60 +++++++++++++------ .../org/zowe/apiml/EurekaRestController.java | 34 +++++------ .../java/org/zowe/apiml/ModulithConfig.java | 3 - apiml/src/main/resources/application.yml | 6 +- apiml/src/test/resources/application.yml | 2 +- .../src/main/resources/application.yml | 4 +- .../api-catalog-services-standalone.yml | 4 +- config/docker/api-catalog-services.yml | 4 +- config/docker/apiml.yml | 4 +- config/docker/discoverable-client.yml | 4 +- config/docker/discovery-service.yml | 4 +- config/docker/gateway-service.yml | 4 +- config/docker/zaas-service.yml | 4 +- config/local-multi/discovery-service-1.yml | 4 +- config/local-multi/discovery-service-2.yml | 4 +- config/local-multi/gateway-service-1.yml | 4 +- config/local-multi/gateway-service-2.yml | 4 +- config/local-multi/zaas-service-1.yml | 4 +- config/local-multi/zaas-service-2.yml | 4 +- config/local/api-catalog-service.yml | 4 +- .../local/api-catalog-services-standalone.yml | 4 +- config/local/discoverable-client.yml | 4 +- .../src/main/resources/application.yml | 4 +- .../src/main/resources/application.yml | 2 +- .../src/main/resources/application.yml | 2 +- .../apiml/util/service/VirtualService.java | 12 ++-- .../src/main/resources/application.yml | 4 +- .../zowe/apiml/zaas/ZaasStartupListener.java | 2 +- 28 files changed, 110 insertions(+), 89 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 78bdc2d0b6..8f49c02b02 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -74,7 +74,7 @@ jobs: - uses: ./.github/actions/setup - name: Run Modulith CI Tests - timeout-minutes: 6 + timeout-minutes: 4 run: > ENV_CONFIG=docker-modulith ./gradlew runStartUpCheck :integration-tests:runContainerModulithTests --info -Ddiscoverableclient.instances=1 -Denvironment.config=-docker-modulith -Denvironment.modulith=true @@ -160,7 +160,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=1 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true @@ -194,6 +194,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 strategy: + fail-fast: false matrix: type: ["normal", "gateway-chaotic", "discoverableclient-chaotic", "websocket-chaotic"] services: @@ -244,7 +245,7 @@ jobs: env: APIML_SERVICE_HOSTNAME: api-catalog-services-2 APIML_HEALTH_PROTECTED: false - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml-2:10011/eureka,https://apiml:10011/eureka APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} @@ -254,11 +255,9 @@ jobs: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_HOSTNAME: discoverable-client-2 - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml-2:10011/eureka,https://apiml:10011/eureka mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka steps: - uses: actions/checkout@v4 with: @@ -268,7 +267,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true @@ -354,6 +353,7 @@ jobs: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} discovery-service: image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} +# needs to run in isolation from another DS for multi-tenancy setup discovery-service-2: image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} env: @@ -371,6 +371,8 @@ jobs: SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken + EUREKA_CLIENT_INSTANCEINFOREPLICATIONINTERVALSECONDS: 1 + EUREKA_CLIENT_REGISTRYFETCHINTERVALSECONDS: 1 zaas-service: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: @@ -387,10 +389,19 @@ jobs: - uses: ./.github/actions/setup + - name: Run Startup Check + if: always() + timeout-minutes: 4 + run: > + ./gradlew runStartUpCheck --info -Denvironment.config=-docker + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + - name: Run CI Tests - timeout-minutes: 6 + timeout-minutes: 4 run: > - ./gradlew runStartUpCheck :integration-tests:runContainerTests --info -Denvironment.config=-docker + ./gradlew :integration-tests:runContainerTests --info -Denvironment.config=-docker -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} - uses: ./.github/actions/dump-jacoco if: always() @@ -419,21 +430,31 @@ jobs: env: APIML_SERVICE_HOSTNAME: api-catalog-services-2 APIML_HEALTH_PROTECTED: false + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/,https://discovery-service:10011/eureka/ api-catalog-services: image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} volumes: - /api-defs:/api-defs + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ caching-service: image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ discovery-service: image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: discovery-service + APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service-2:10011/eureka discovery-service-2: image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_HOSTNAME: discovery-service-2 - APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service-2:10011/eureka + APIML_DISCOVERY_ALLPEERSURLS: https://discovery-service:10011/eureka gateway-service: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: @@ -444,7 +465,7 @@ jobs: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_HOSTNAME: gateway-service-2 - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/,https://discovery-service:10011/eureka/ logbackService: ZWEAGW2 zaas-service: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} @@ -453,14 +474,16 @@ jobs: APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates APIML_SECURITY_AUTH_PROVIDER: saf + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/,https://discovery-service-2:10011/eureka/ zaas-service-2: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SECURITY_X509_ENABLED: true APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true - APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates + APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service-2:10010/gateway/certificates APIML_SECURITY_AUTH_PROVIDER: saf APIML_SERVICE_HOSTNAME: zaas-service-2 + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/,https://discovery-service:10011/eureka/ steps: - uses: actions/checkout@v4 with: @@ -470,7 +493,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-ha -Ddiscoverableclient.instances=1 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD @@ -728,7 +751,7 @@ jobs: - uses: ./.github/actions/setup - name: Run CI Tests - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck :integration-tests:runGatewayCentralRegistryTest --info -Denvironment.config=-docker -Dgateway.instances=2 -Ddiscoverableclient.instances=0 @@ -1230,7 +1253,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD -Denvironment.modulith=true @@ -1448,6 +1471,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 strategy: + fail-fast: false matrix: type: ["normal", "discovery-chaotic", "gateway-chaotic", "discoverableclient-chaotic", "websocket-chaotic"] services: @@ -1512,7 +1536,7 @@ jobs: - name: Run Startup Check if: always() - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info -Denvironment.config=-ha -Ddiscoverableclient.instances=2 -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD @@ -1816,7 +1840,7 @@ jobs: api-catalog-ui/frontend/node_modules key: my-cache-${{ runner.os }}-${{ hashFiles('api-catalog-ui/frontend/*.json') }} - name: Run startup check - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info --scan -Denvironment.config=-ha -Ddiscoverableclient.instances=1 - name: Show status when APIML is not ready yet @@ -1930,7 +1954,7 @@ jobs: api-catalog-ui/frontend/node_modules key: my-cache-${{ runner.os }}-${{ hashFiles('api-catalog-ui/frontend/*.json') }} - name: Run startup check - timeout-minutes: 6 + timeout-minutes: 4 run: > ./gradlew runStartUpCheck --info --scan -Denvironment.config=-docker-modulith-ha -Ddiscoverableclient.instances=1 -Denvironment.modulith=true -Denvironment.gwCount=1 -Dgateway.host=gateway-service,gateway-service-2 -Ddiscovery.host=gateway-service -Ddiscovery.additionalHost=gateway-service-2 diff --git a/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java b/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java index c667b0f00a..dc8ffd2e5c 100644 --- a/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java +++ b/apiml/src/main/java/org/zowe/apiml/EurekaRestController.java @@ -75,7 +75,7 @@ @SuppressWarnings("java:S1452") // Generic type wildcard needed due to legacy code usage @RestController @RequiredArgsConstructor -@RequestMapping(path = "/eureka", produces = { "application/xml", "application/json" }) +@RequestMapping(path = "/eureka", produces = {"application/xml", "application/json"}) @DependsOn("modulithConfig") @Slf4j public class EurekaRestController { @@ -103,7 +103,7 @@ private ResponseEntity convertResponse(Response response) { .body(response.getEntity()); } - @GetMapping(value = {"/apps", "/apps/"}, produces = { "application/xml", "application/json" }) + @GetMapping(value = {"/apps", "/apps/"}, produces = {"application/xml", "application/json"}) public Mono> getContainers( ServerWebExchange serverWebExchange, @Nullable @RequestHeader(ACCEPT) String acceptHeader, @@ -277,12 +277,12 @@ public Mono> asgStatusUpdate( return just(convertResponse(asgResource.statusUpdate(asgName, newStatus, isReplication))); } - @PostMapping({ "/peerreplication/batch/", "/peerreplication/batch" }) + @PostMapping({"/peerreplication/batch/", "/peerreplication/batch"}) public Mono> batchReplication( @RequestBody String replicationListString ) throws IOException { var replicationList = JACKSON_JSON.decode(replicationListString, ReplicationList.class); - return just(convertResponse(peerReplicationResource.batchReplication(replicationList))); + return Mono.fromCallable(() -> convertResponse(peerReplicationResource.batchReplication(replicationList))); } @RequiredArgsConstructor @@ -303,20 +303,20 @@ public String getPath(boolean decode) { @Override public List getPathSegments() { return request.getPath().contextPath().elements().stream().map( - e -> new PathSegment() { - @Override - public String getPath() { - return e.value(); + e -> new PathSegment() { + @Override + public String getPath() { + return e.value(); + } + + @Override + public MultivaluedMap getMatrixParameters() { + return new MultivaluedHashMap<>(); + } } - - @Override - public MultivaluedMap getMatrixParameters() { - return new MultivaluedHashMap<>(); - } - } - ) - .map(PathSegment.class::cast) - .toList(); + ) + .map(PathSegment.class::cast) + .toList(); } @Override diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index c682ebcd9d..f54cad1585 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -52,7 +52,6 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.zaas.ZaasStartupListener; import org.zowe.apiml.zaas.security.service.JwtSecurity; import reactor.core.publisher.Flux; @@ -178,8 +177,6 @@ public void periodicJwtInit() { var jwtSec = applicationContext.getBean(JwtSecurity.class); if (!jwtSec.getZosmfListener().isZosmfReady()) { jwtSec.getZosmfListener().getZosmfRegisteredListener().onEvent(new CacheRefreshedEvent()); - ZaasStartupListener listener = applicationContext.getBean(ZaasStartupListener.class); - listener.notifyStartup(); } } diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 2ced71cae0..43c54fd390 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -4,16 +4,16 @@ eureka: client: fetchRegistry: false registerWithEureka: false - instanceInfoReplicationIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 region: default serviceUrl: defaultZone: ${apiml.discovery.allPeersUrls} healthcheck: enabled: true server: - max-threads-for-peer-replication: 2 + max-threads-for-peer-replication: 6 useReadOnlyResponseCache: false - peer-node-read-timeout-ms: 10000 + peer-node-read-timeout-ms: 8000 spring: cloud: gateway: diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml index 699114858b..d319ffb562 100644 --- a/apiml/src/test/resources/application.yml +++ b/apiml/src/test/resources/application.yml @@ -9,7 +9,7 @@ eureka: client: fetchRegistry: false registerWithEureka: false - instanceInfoReplicationIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 region: default serviceUrl: defaultZone: ${apiml.discovery.allPeersUrls} diff --git a/caching-service/src/main/resources/application.yml b/caching-service/src/main/resources/application.yml index f158658fc0..9ebaa1f286 100644 --- a/caching-service/src/main/resources/application.yml +++ b/caching-service/src/main/resources/application.yml @@ -44,8 +44,8 @@ logging: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 apiml: enabled: true service: diff --git a/config/docker/api-catalog-services-standalone.yml b/config/docker/api-catalog-services-standalone.yml index aa3945149f..642e618722 100644 --- a/config/docker/api-catalog-services-standalone.yml +++ b/config/docker/api-catalog-services-standalone.yml @@ -6,8 +6,8 @@ logging: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 instance: leaseExpirationDurationInSeconds: 6 leaseRenewalIntervalInSeconds: 1 diff --git a/config/docker/api-catalog-services.yml b/config/docker/api-catalog-services.yml index 5fd10e0d3b..cfc21dff51 100644 --- a/config/docker/api-catalog-services.yml +++ b/config/docker/api-catalog-services.yml @@ -2,8 +2,8 @@ spring.profiles.active: debug,diag eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 instance: leaseExpirationDurationInSeconds: 6 leaseRenewalIntervalInSeconds: 1 diff --git a/config/docker/apiml.yml b/config/docker/apiml.yml index 2939fa34ec..d3b444dca9 100644 --- a/config/docker/apiml.yml +++ b/config/docker/apiml.yml @@ -41,8 +41,8 @@ spring: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 management: endpoints: diff --git a/config/docker/discoverable-client.yml b/config/docker/discoverable-client.yml index 75be6f9fe9..07ba48795b 100644 --- a/config/docker/discoverable-client.yml +++ b/config/docker/discoverable-client.yml @@ -17,8 +17,8 @@ apiml: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 server: address: 0.0.0.0 ssl: diff --git a/config/docker/discovery-service.yml b/config/docker/discovery-service.yml index 9055df12a1..657655ee89 100644 --- a/config/docker/discovery-service.yml +++ b/config/docker/discovery-service.yml @@ -18,8 +18,8 @@ spring: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 management: endpoints: diff --git a/config/docker/gateway-service.yml b/config/docker/gateway-service.yml index 2a0768a12f..1ce58ab2c0 100644 --- a/config/docker/gateway-service.yml +++ b/config/docker/gateway-service.yml @@ -7,8 +7,8 @@ apiml: banner: console eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 spring: output: ansi: diff --git a/config/docker/zaas-service.yml b/config/docker/zaas-service.yml index 157e7ac9a0..d55e8f0500 100644 --- a/config/docker/zaas-service.yml +++ b/config/docker/zaas-service.yml @@ -31,8 +31,8 @@ apiml: banner: console eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 spring: output: ansi: diff --git a/config/local-multi/discovery-service-1.yml b/config/local-multi/discovery-service-1.yml index aa7a2336e1..40b08e2a9e 100644 --- a/config/local-multi/discovery-service-1.yml +++ b/config/local-multi/discovery-service-1.yml @@ -22,8 +22,8 @@ spring: spring.profiles: https eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 server: ssl: diff --git a/config/local-multi/discovery-service-2.yml b/config/local-multi/discovery-service-2.yml index 1652396d9d..156949e411 100644 --- a/config/local-multi/discovery-service-2.yml +++ b/config/local-multi/discovery-service-2.yml @@ -22,8 +22,8 @@ spring: spring.profiles: https eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 server: ssl: diff --git a/config/local-multi/gateway-service-1.yml b/config/local-multi/gateway-service-1.yml index bb85d273fd..2915c8d01c 100644 --- a/config/local-multi/gateway-service-1.yml +++ b/config/local-multi/gateway-service-1.yml @@ -9,8 +9,8 @@ apiml: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 spring: output: ansi: diff --git a/config/local-multi/gateway-service-2.yml b/config/local-multi/gateway-service-2.yml index 0c00d91fd6..aebf4791fa 100644 --- a/config/local-multi/gateway-service-2.yml +++ b/config/local-multi/gateway-service-2.yml @@ -22,8 +22,8 @@ apiml: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 spring: output: ansi: diff --git a/config/local-multi/zaas-service-1.yml b/config/local-multi/zaas-service-1.yml index 69c214ed01..cde8fd4ff6 100644 --- a/config/local-multi/zaas-service-1.yml +++ b/config/local-multi/zaas-service-1.yml @@ -22,8 +22,8 @@ apiml: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 spring: output: ansi: diff --git a/config/local-multi/zaas-service-2.yml b/config/local-multi/zaas-service-2.yml index 6e6efe80c4..86dbf0944e 100644 --- a/config/local-multi/zaas-service-2.yml +++ b/config/local-multi/zaas-service-2.yml @@ -22,8 +22,8 @@ apiml: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 spring: output: ansi: diff --git a/config/local/api-catalog-service.yml b/config/local/api-catalog-service.yml index 379c6146f8..f3a0360600 100644 --- a/config/local/api-catalog-service.yml +++ b/config/local/api-catalog-service.yml @@ -2,8 +2,8 @@ spring.profiles.active: debug,diag eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 instance: leaseExpirationDurationInSeconds: 6 leaseRenewalIntervalInSeconds: 1 diff --git a/config/local/api-catalog-services-standalone.yml b/config/local/api-catalog-services-standalone.yml index aa3945149f..642e618722 100644 --- a/config/local/api-catalog-services-standalone.yml +++ b/config/local/api-catalog-services-standalone.yml @@ -6,8 +6,8 @@ logging: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 instance: leaseExpirationDurationInSeconds: 6 leaseRenewalIntervalInSeconds: 1 diff --git a/config/local/discoverable-client.yml b/config/local/discoverable-client.yml index dc57ef1923..edb3c4a74a 100644 --- a/config/local/discoverable-client.yml +++ b/config/local/discoverable-client.yml @@ -17,8 +17,8 @@ apiml: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 server: ssl: keyAlias: localhost diff --git a/discoverable-client/src/main/resources/application.yml b/discoverable-client/src/main/resources/application.yml index 4d86b52d25..ca91f2babc 100644 --- a/discoverable-client/src/main/resources/application.yml +++ b/discoverable-client/src/main/resources/application.yml @@ -37,8 +37,8 @@ spring: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 apiml: enabled: true service: diff --git a/discovery-service/src/main/resources/application.yml b/discovery-service/src/main/resources/application.yml index 403db89d74..083994f712 100644 --- a/discovery-service/src/main/resources/application.yml +++ b/discovery-service/src/main/resources/application.yml @@ -89,7 +89,7 @@ eureka: serviceUrl: defaultZone: ${apiml.discovery.allPeersUrls} server: - max-threads-for-peer-replication: 2 + max-threads-for-peer-replication: 6 useReadOnlyResponseCache: false management: diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 5f8a70e280..5103206087 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -43,7 +43,7 @@ eureka: client: fetchRegistry: true registerWithEureka: true - instanceInfoReplicationIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 region: default serviceUrl: defaultZone: ${apiml.service.discoveryServiceUrls} diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/service/VirtualService.java b/integration-tests/src/test/java/org/zowe/apiml/util/service/VirtualService.java index 30eedc6859..296346429f 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/service/VirtualService.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/service/VirtualService.java @@ -219,7 +219,7 @@ public VirtualService addServlet(String name, String pattern, Servlet servlet) { * Register servlet to get instance information * * @param name under which the servlet is registered to Tomcat - * @param url where it will be exposed + * @param url where it will be exposed * @return this instance to next command */ public VirtualService addInstanceServlet(String name, String url) { @@ -272,7 +272,7 @@ public VirtualService addVerifyServlet() { * The check means make serviceCount calls. There is a preposition that load balancer is based on cyclic queue and * if it will call multiple times (same to count of instances of same service), it should call all instances. * - * @param timeoutSec Timeout in secs to break waiting + * @param timeoutSec Timeout in secs to break waiting */ public VirtualService waitForGatewayRegistration(int timeoutSec) { final long time0 = System.currentTimeMillis(); @@ -300,7 +300,7 @@ public VirtualService waitForGatewayRegistration(int timeoutSec) { } if (slept) { - log.info("Slept for waiting for gateways took {}s", (System.currentTimeMillis() - time0) / 1000); + log.info("Service registration check slept {}s while waiting for gateways ", (System.currentTimeMillis() - time0) / 1000); } return this; @@ -339,7 +339,7 @@ public VirtualService waitForGatewayUnregistering(int instanceCountBefore, int t } if (slept) { - log.info("Slept for waiting for gateways took {}s", (System.currentTimeMillis() - time0) / 1000); + log.info("Service un-registration slept {}s while waiting for gateways", (System.currentTimeMillis() - time0) / 1000); } return this; @@ -411,7 +411,7 @@ public void register(String status) throws UnknownHostException, JSONException { } public Response postRegistration(String status) throws UnknownHostException, JSONException { - return given().when() + return given().log().all().when() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(new JSONObject() .put("instance", new JSONObject() @@ -559,7 +559,7 @@ private void addDefaultRouteIfMissing() { */ public List getGatewayUrls() { return DiscoveryUtils.getGatewayUrls().stream() - .map(x -> String.format("%s/%s%s", x, serviceId.toLowerCase(), gatewayPath)) + .map(x -> String.format("%s/%s%s", x, serviceId.toLowerCase(), gatewayPath)) .toList(); } diff --git a/mock-services/src/main/resources/application.yml b/mock-services/src/main/resources/application.yml index 61df059349..4637c125fe 100644 --- a/mock-services/src/main/resources/application.yml +++ b/mock-services/src/main/resources/application.yml @@ -30,8 +30,8 @@ server: eureka: client: - initialInstanceInfoReplicationIntervalSeconds: 1 - registryFetchIntervalSeconds: 1 + instanceInfoReplicationIntervalSeconds: 30 + registryFetchIntervalSeconds: 30 zosmf: enableMock: true username: USER,APIMTST,USER1,USER2 diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java index 369f6131a9..ec2ef28d4b 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java @@ -16,8 +16,8 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; -import org.zowe.apiml.zaas.security.login.Providers; import org.zowe.apiml.product.service.ServiceStartupEventHandler; +import org.zowe.apiml.zaas.security.login.Providers; import java.time.Duration; import java.util.Timer; From f7f8c75601603c66c6d5873619fcb6357b5ae4f5 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 18 Jul 2025 00:44:41 +0000 Subject: [PATCH 022/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.2.23'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 20eca9acaf..79c4d2bd61 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.23-SNAPSHOT +version=3.2.23 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From f930bfdb4f7d9e5aa7c1c31f823195219d3d4c62 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 18 Jul 2025 00:44:43 +0000 Subject: [PATCH 023/152] [Gradle Release plugin] Create new version: 'v3.2.24-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 79c4d2bd61..8ccf7c5c3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.23 +version=3.2.24-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 8673d5248297368bc4bfb0b02357c31159a6a79d Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 18 Jul 2025 00:44:45 +0000 Subject: [PATCH 024/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 5764830851..bc73564653 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,4 +8,4 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.2.23-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.2.24-SNAPSHOT From a3429cca97b948bc29b9cd81fe5c378f41b57a4e Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 18 Jul 2025 12:53:09 +0200 Subject: [PATCH 025/152] fix: update start.sh settings for caching service (#4226) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- apiml-package/src/main/resources/bin/start.sh | 20 +++++++++---------- config/docker/apiml.yml | 2 ++ config/local/gateway-service.yml | 3 +++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 3ee2882ec5..831637f765 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -212,7 +212,7 @@ else fi if [ -n "${ZWE_configs_storage_vsam_name}" ]; then - VSAM_FILE_NAME=//\'${ZWE_configs_storage_vsam_name}\' + VSAM_FILE_NAME=//\'${ZWE_configs_storage_vsam_name:-${ZWE_components_caching_service_storage_vsam_name}}\' fi LIBPATH="$LIBPATH":"/lib" @@ -365,6 +365,11 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ -Dapiml.service.port=${ZWE_configs_port:-${ZWE_components_gateway_port:-7554}} \ -Dapiml.zoweManifest=${ZWE_zowe_runtimeDirectory}/manifest.json \ + -Dcaching.storage.evictionStrategy=${ZWE_configs_storage_evictionStrategy:-${ZWE_components_caching_service_storage_evictionStrategy:-reject}} \ + -Dcaching.storage.infinispan.initialHosts=${ZWE_configs_storage_infinispan_initialHosts:-${ZWE_components_caching_service_storage_infinispan_initialHosts:-"localhost[7600]"}} \ + -Dcaching.storage.mode=${ZWE_configs_storage_mode:-${ZWE_components_caching_service_storage_mode:-infinispan}} \ + -Dcaching.storage.size=${ZWE_configs_storage_size:${ZWE_components_caching_service_storage_size:-10000}} \ + -Dcaching.storage.vsam.name=${VSAM_FILE_NAME} \ -Deureka.client.serviceUrl.defaultZone=${ZWE_DISCOVERY_SERVICES_LIST} \ -Dfile.encoding=UTF-8 \ -Dibm.serversocket.recover=true \ @@ -373,6 +378,10 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ -Djavax.net.debug=${ZWE_configs_sslDebug:-${ZWE_components_gateway_sslDebug:-${ZWE_components_discovery_sslDebug:-""}}} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ + -Djgroups.bind.address=${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_components_caching_service_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}}} \ + -Djgroups.bind.port=${ZWE_configs_storage_infinispan_jgroups_port:-${ZWE_components_caching_service_storage_infinispan_jgroups_port:-7600}} \ + -Djgroups.keyExchange.port=${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-${ZWE_components_caching_service_storage_infinispan_jgroups_keyExchange_port:-7601}} \ + -Djgroups.tcp.diag.enabled=${ZWE_configs_storage_infinispan_jgroups_tcp_diag_enabled:-${ZWE_components_caching_service_storage_infinispan_jgroups_tcp_diag_enabled:-false}} \ -Dloader.path=${APIML_LOADER_PATH} \ -Dlogging.charset.console=${ZOWE_CONSOLE_LOG_CHARSET} \ -Dserver.address=${ZWE_configs_zowe_network_server_listenAddresses_0:-${ZWE_zowe_network_server_listenAddresses_0:-"0.0.0.0"}} \ @@ -395,15 +404,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dserver.webSocket.maxIdleTimeout=${ZWE_configs_server_webSocket_maxIdleTimeout:-${ZWE_components_gateway_server_webSocket_maxIdleTimeout:-3600000}} \ -Dserver.webSocket.requestBufferSize=${ZWE_configs_server_webSocket_requestBufferSize:-${ZWE_components_gateway_server_webSocket_requestBufferSize:-8192}} \ -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-https} \ - -Dcaching.storage.evictionStrategy=${ZWE_configs_storage_evictionStrategy:-reject} \ - -Dcaching.storage.size=${ZWE_configs_storage_size:-10000} \ - -Dcaching.storage.mode=${ZWE_configs_storage_mode:-infinispan} \ - -Dcaching.storage.vsam.name=${VSAM_FILE_NAME} \ - -Djgroups.bind.address=${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}} \ - -Djgroups.bind.port=${ZWE_configs_storage_infinispan_jgroups_port:-7600} \ - -Djgroups.keyExchange.port=${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-7601} \ - -Djgroups.tcp.diag.enabled=${ZWE_configs_storage_infinispan_jgroups_tcp_diag_enabled:-false} \ - -Dcaching.storage.infinispan.initialHosts=${ZWE_configs_storage_infinispan_initialHosts:-localhost[7600]} \ -jar "${JAR_FILE}" & pid=$! diff --git a/config/docker/apiml.yml b/config/docker/apiml.yml index d3b444dca9..94891dfa8a 100644 --- a/config/docker/apiml.yml +++ b/config/docker/apiml.yml @@ -53,6 +53,8 @@ management: endpoint: shutdown: access: unrestricted + loggers: + enabled: true server: address: 0.0.0.0 diff --git a/config/local/gateway-service.yml b/config/local/gateway-service.yml index 319ca61eff..8f9db2eeae 100644 --- a/config/local/gateway-service.yml +++ b/config/local/gateway-service.yml @@ -8,6 +8,9 @@ apiml: hostname: localhost port: 10010 banner: console + security: + x509: + enabled: true eureka: client: serviceUrl: From b733de97ec01d3b65f11d4191d3a7e01bf37d486 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 18 Jul 2025 13:52:39 +0200 Subject: [PATCH 026/152] feat: support independent response time route setting (#3981) Signed-off-by: Pablo Carle Signed-off-by: ac892247 Co-authored-by: Pablo Carle Co-authored-by: ac892247 Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- .../src/main/resources/application.yml | 3 ++ .../config/NettyRoutingFilterApiml.java | 26 +++++------- .../config/NettyRoutingFilterApimlTest.java | 17 ++++++-- .../gateway/GatewayRoutingTest.java | 41 ++++++++++++++----- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/discoverable-client/src/main/resources/application.yml b/discoverable-client/src/main/resources/application.yml index ca91f2babc..623f9c6fe3 100644 --- a/discoverable-client/src/main/resources/application.yml +++ b/discoverable-client/src/main/resources/application.yml @@ -128,7 +128,10 @@ apiml: trustStore: ${server.ssl.trustStore} trustStorePassword: ${server.ssl.trustStorePassword} customMetadata: + apiml: + responseTimeout: 10000 + connectTimeout: 6500 enableUrlEncodedCharacters: true gatewayPort: 10010 gatewayAuthEndpoint: /gateway/api/v1/auth diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java index eb52e470bf..bc7a9c9aaf 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Optional; -import static org.springframework.cloud.gateway.support.RouteMetadataUtils.CONNECT_TIMEOUT_ATTR; import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE; @Slf4j @@ -66,23 +65,20 @@ static Integer getInteger(Object connectTimeoutAttr) { @Override protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) { // select proper HttpClient instance by attribute apiml.useClientCert - boolean useClientCert = Optional.ofNullable((Boolean) exchange.getAttribute(HTTP_CLIENT_USE_CLIENT_CERTIFICATE)).orElse(Boolean.FALSE); - HttpClient httpClient = useClientCert ? httpClientClientCert : httpClientNoCert; + var useClientCert = Optional.ofNullable((Boolean) exchange.getAttribute(HTTP_CLIENT_USE_CLIENT_CERTIFICATE)).orElse(Boolean.FALSE); + var httpClient = useClientCert ? httpClientClientCert : httpClientNoCert; log.debug("Using client with keystore {}", useClientCert); - Object connectTimeoutAttr = route.getMetadata().get(CONNECT_TIMEOUT_ATTR); - if (connectTimeoutAttr != null) { - // if there is configured timeout, respect it - Integer connectTimeout = getInteger(connectTimeoutAttr); - return httpClient - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout) - .responseTimeout(Duration.ofMillis(connectTimeout)); - } + var connectTimeoutAttr = route.getMetadata().get("apiml.connectTimeout"); + var responseTimeoutAttr = route.getMetadata().get("apiml.responseTimeout"); + + var responseTimeoutResult = responseTimeoutAttr != null ? Long.parseLong(String.valueOf(responseTimeoutAttr)) : requestTimeout; + var connectTimeoutResult = connectTimeoutAttr != null ? getInteger(connectTimeoutAttr) : requestTimeout; + httpClient = httpClient + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutResult) + .responseTimeout(Duration.ofMillis(responseTimeoutResult)); - // otherwise just return selected HttpClient with the default configured timeouts - return httpClient - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, requestTimeout) - .responseTimeout(Duration.ofMillis(requestTimeout)); + return httpClient; } @Override diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java index 7cc215ae9e..dcd50f4602 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java @@ -28,13 +28,13 @@ import reactor.netty.http.client.HttpClient; import javax.net.ssl.SSLException; +import java.time.Duration; import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.cloud.gateway.support.RouteMetadataUtils.CONNECT_TIMEOUT_ATTR; import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE; class NettyRoutingFilterApimlTest { @@ -92,10 +92,13 @@ class GetHttpClient { NettyRoutingFilterApiml nettyRoutingFilterApiml; private final Route ROUTE_NO_TIMEOUT = Route.async() - .id("1").uri("http://localhost/").predicate(__ -> true) + .id("1").uri("http://localhost/").predicate(__ -> true) .build(); private final Route ROUTE_TIMEOUT = Route.async() - .id("2").uri("http://localhost/").predicate(__ -> true).metadata(CONNECT_TIMEOUT_ATTR, "100") + .id("2").uri("http://localhost/").predicate(__ -> true).metadata("apiml.connectTimeout", "100") + .build(); + private final Route ROUTE_RESPONSE_TIMEOUT = Route.async() + .id("3").uri("http://localhost/").predicate(__ -> true).metadata("apiml.responseTimeout", "23") .build(); MockServerWebExchange serverWebExchange; @@ -143,6 +146,14 @@ void givenTimeoutAndRequirementsForClientCert_whenGetHttpClient_thenCallWithoutC verify(httpClientWithCert).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 100); } + @Test + void givenTimeoutAndRequirementsForClientCert_whenGetHttpClient_thenCallWithoutClientCertWithCorrectTimeout() { + setUpClient(httpClientWithCert); + serverWebExchange.getAttributes().put(HTTP_CLIENT_USE_CLIENT_CERTIFICATE, Boolean.TRUE); + nettyRoutingFilterApiml.getHttpClient(ROUTE_RESPONSE_TIMEOUT, serverWebExchange); + verify(httpClientWithCert).responseTimeout(Duration.ofMillis(23)); + } + } } diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java index df5a9e87cc..505ae831a2 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/GatewayRoutingTest.java @@ -56,7 +56,10 @@ static void setup() { }) void testRoutingWithBasePath(String basePath) throws URISyntaxException { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), basePath); - given().get(new URI(scgUrl)).then().statusCode(200); + given() + .get(new URI(scgUrl)) + .then() + .statusCode(200); } @ParameterizedTest(name = "When header X-Forward-To is set to {0} should return 200") @@ -66,8 +69,11 @@ void testRoutingWithBasePath(String basePath) throws URISyntaxException { }) void testRoutingWithHeader(String forwardTo) throws URISyntaxException { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), DISCOVERABLE_GREET); - given().header(HEADER_X_FORWARD_TO, forwardTo) - .get(new URI(scgUrl)).then().statusCode(200); + given() + .header(HEADER_X_FORWARD_TO, forwardTo) + .get(new URI(scgUrl)) + .then() + .statusCode(200); } @ParameterizedTest(name = "When base path is {0} should return 404") @@ -77,7 +83,10 @@ void testRoutingWithHeader(String forwardTo) throws URISyntaxException { }) void testRoutingWithIncorrectServiceInBasePath(String basePath) throws URISyntaxException { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), basePath); - given().get(new URI(scgUrl)).then().statusCode(404); + given() + .get(new URI(scgUrl)) + .then() + .statusCode(404); } @ParameterizedTest(name = "When header X-Forward-To is set to {0} should return 404") @@ -87,8 +96,11 @@ void testRoutingWithIncorrectServiceInBasePath(String basePath) throws URISyntax }) void testRoutingWithIncorrectServiceInHeader(String forwardTo) throws URISyntaxException { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), NON_EXISTING_SERVICE_ENDPOINT); - given().header(HEADER_X_FORWARD_TO, forwardTo) - .get(new URI(scgUrl)).then().statusCode(404); + given() + .header(HEADER_X_FORWARD_TO, forwardTo) + .get(new URI(scgUrl)) + .then() + .statusCode(404); } @ParameterizedTest(name = "When header X-Forward-To is set to {0} and base path is {1} should return 200 - loopback") @@ -97,8 +109,11 @@ void testRoutingWithIncorrectServiceInHeader(String forwardTo) throws URISyntaxE }) void testWrongRoutingWithHeader(String forwardTo, String endpoint) throws URISyntaxException { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), endpoint); - given().header(HEADER_X_FORWARD_TO, forwardTo) - .get(new URI(scgUrl)).then().statusCode(200); + given() + .header(HEADER_X_FORWARD_TO, forwardTo) + .get(new URI(scgUrl)) + .then() + .statusCode(200); } @ParameterizedTest(name = "When base path is {0} should return 404") @@ -108,13 +123,19 @@ void testWrongRoutingWithHeader(String forwardTo, String endpoint) throws URISyn }) void testWrongRoutingWithBasePath(String basePath) throws URISyntaxException { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), basePath); - given().get(new URI(scgUrl)).then().statusCode(404); + given() + .get(new URI(scgUrl)) + .then() + .statusCode(404); } @Test void givenEndpointDoesNotExistOnRegisteredService() throws URISyntaxException { String scgUrl = String.format("%s://%s:%s%s", conf.getScheme(), conf.getHost(), conf.getPort(), "/dcpassticket/api/v1/unknown"); - given().get(new URI(scgUrl)).then().statusCode(404); + given() + .get(new URI(scgUrl)) + .then() + .statusCode(404); } @ParameterizedTest From d4c77c9b7c9e5583d72bb942b4164cb133c4f435 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:22:52 +0200 Subject: [PATCH 027/152] chore: Update all non-major dependencies (v3.x.x) (#4218) Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 10 +- api-catalog-ui/frontend/package.json | 4 +- gradle/versions.gradle | 10 +- .../package-lock.json | 4 +- .../package.json | 2 +- onboarding-enabler-nodejs/package-lock.json | 2 +- onboarding-enabler-nodejs/package.json | 2 +- .../package-lock.json | 122 +++++++++--------- zowe-cli-id-federation-plugin/package.json | 10 +- 9 files changed, 83 insertions(+), 83 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index bc1316ba2c..98a1808aa2 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -55,7 +55,7 @@ "rxjs": "7.8.2", "sass": "1.89.2", "stream": "0.0.3", - "swagger-ui-react": "5.26.2", + "swagger-ui-react": "5.27.0", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -124,7 +124,7 @@ "yaml": "2.8.0" }, "engines": { - "node": "=20.19.3", + "node": "=20.19.4", "npm": "=10.9.3" } }, @@ -29651,9 +29651,9 @@ } }, "node_modules/swagger-ui-react": { - "version": "5.26.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.26.2.tgz", - "integrity": "sha512-2QHyN/vl3HaihXpBhJm7du7gfSibM3T3cSdk7Od98u2oFtraNTAe1r1k9cw/XBKBpl5XTye6rvACe8VP/NO2og==", + "version": "5.27.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.27.0.tgz", + "integrity": "sha512-KQ1NPzRfpVICvYHmVZCmw79VJK9NYvT8+f9dTRE2ZOkZAG/hlBprCk0x1AC9ERiaPb2Wrwxuq94PkZoMM+J6fQ==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.27.1", diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 191dc6b12d..68a01c05ec 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -51,7 +51,7 @@ "rxjs": "7.8.2", "sass": "1.89.2", "stream": "0.0.3", - "swagger-ui-react": "5.26.2", + "swagger-ui-react": "5.27.0", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -165,7 +165,7 @@ }, "engines": { "npm": "=10.9.3", - "node": "=20.19.3" + "node": "=20.19.4" }, "browserslist": [ ">0.2%", diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 9884cd25a7..4bce3c145f 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -11,7 +11,7 @@ dependencyResolutionManagement { version('springCloudCommons', '4.3.0') version('springCloudCB', '3.3.0') version('springCloudGateway', '4.3.0') - version('springFramework', '6.2.8') + version('springFramework', '6.2.9') version('springRetry', '2.0.12') version('modulith', '1.4.1') @@ -19,7 +19,7 @@ dependencyResolutionManagement { version('glassfishHk2', '3.1.1') version('zosUtils', '2.0.7') - version('aws', '1.12.787') + version('aws', '1.12.788') version('awaitility', '4.3.0') version('bouncyCastle', '1.81') version('caffeine', '3.2.2') @@ -38,7 +38,7 @@ dependencyResolutionManagement { version('hamcrest', '3.0') version('httpClient4', '4.5.14') version('httpClient5', '5.5') - version('infinispan', '15.2.4.Final') + version('infinispan', '15.2.5.Final') version('jacksonCore', '2.19.1') version('jacksonDatabind', '2.19.1') version('jacksonDataformatYaml', '2.19.1') @@ -75,7 +75,7 @@ dependencyResolutionManagement { version('lombok', '1.18.38') version('netty', '4.2.3.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 - version('nettyReactor', '1.2.7') + version('nettyReactor', '1.2.8') version('nimbusJoseJwt', '9.48') version('openApiDiff', '2.1.2') version('picocli', '4.7.7') @@ -101,7 +101,7 @@ dependencyResolutionManagement { version('gradleTestLogger', '4.0.0') version('testLogger', '4.0.0') version('micronautPlatform', '4.9.0') - version('micronaut', '4.9.7') + version('micronaut', '4.9.8') version('micronautPlugin', '4.5.4') version('shadow', '8.1.1') version('checkstyle', '10.17.0') diff --git a/onboarding-enabler-nodejs-sample-app/package-lock.json b/onboarding-enabler-nodejs-sample-app/package-lock.json index 0c361bc466..2221bce94d 100644 --- a/onboarding-enabler-nodejs-sample-app/package-lock.json +++ b/onboarding-enabler-nodejs-sample-app/package-lock.json @@ -13,7 +13,7 @@ "express": "4.21.2" }, "engines": { - "node": "=20.19.3", + "node": "=20.19.4", "npm": "=10.9.3" } }, @@ -46,7 +46,7 @@ "sinon-chai": "4.0.0" }, "engines": { - "node": "=20.19.3", + "node": "=20.19.4", "npm": "=10.9.3" } }, diff --git a/onboarding-enabler-nodejs-sample-app/package.json b/onboarding-enabler-nodejs-sample-app/package.json index d7ce4ac95b..7a9f54a8e1 100755 --- a/onboarding-enabler-nodejs-sample-app/package.json +++ b/onboarding-enabler-nodejs-sample-app/package.json @@ -23,6 +23,6 @@ }, "engines": { "npm": "=10.9.3", - "node": "=20.19.3" + "node": "=20.19.4" } } diff --git a/onboarding-enabler-nodejs/package-lock.json b/onboarding-enabler-nodejs/package-lock.json index 4d29425d7f..3b89a5ab42 100644 --- a/onboarding-enabler-nodejs/package-lock.json +++ b/onboarding-enabler-nodejs/package-lock.json @@ -33,7 +33,7 @@ "sinon-chai": "4.0.0" }, "engines": { - "node": "=20.19.3", + "node": "=20.19.4", "npm": "=10.9.3" } }, diff --git a/onboarding-enabler-nodejs/package.json b/onboarding-enabler-nodejs/package.json index 4b70dd8366..31fc90ccd9 100644 --- a/onboarding-enabler-nodejs/package.json +++ b/onboarding-enabler-nodejs/package.json @@ -53,6 +53,6 @@ }, "engines": { "npm": "=10.9.3", - "node": "=20.19.3" + "node": "=20.19.4" } } diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index 39e2dfdc47..456505ebdc 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -17,9 +17,9 @@ "@types/node": "20.19.8", "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", - "@zowe/cli": "8.24.4", - "@zowe/cli-test-utils": "8.24.2", - "@zowe/imperative": "8.24.2", + "@zowe/cli": "8.24.5", + "@zowe/cli-test-utils": "8.24.5", + "@zowe/imperative": "8.24.5", "copyfiles": "2.4.1", "env-cmd": "10.1.0", "eslint": "9.31.0", @@ -42,11 +42,11 @@ "typescript": "5.8.3" }, "engines": { - "node": "=20.19.3", + "node": "=20.19.4", "npm": "=10.9.3" }, "peerDependencies": { - "@zowe/imperative": "8.24.2" + "@zowe/imperative": "8.24.5" } }, "node_modules/@ampproject/remapping": { @@ -2471,25 +2471,25 @@ "dev": true }, "node_modules/@zowe/cli": { - "version": "8.24.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.24.4.tgz", - "integrity": "sha512-4rbJfEmlPeLVzAvnoKEw1sg9K+uVov9G9kEg1xZbi30bXv/hG92T1+rSifUhsXDxcv1nB+yH1J0JODKj4+apFw==", + "version": "8.24.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.24.5.tgz", + "integrity": "sha512-JDQJrOVNTkA/YL7fihs/bHiFhJ9DRS8+WT5h5rMCgXRyE3WNSI8qvc8yLC5D1PMKDNSxmaFpERZOrGFwAzK/GA==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, "license": "EPL-2.0", "dependencies": { - "@zowe/core-for-zowe-sdk": "8.24.2", - "@zowe/imperative": "8.24.2", - "@zowe/provisioning-for-zowe-sdk": "8.24.2", - "@zowe/zos-console-for-zowe-sdk": "8.24.2", - "@zowe/zos-files-for-zowe-sdk": "8.24.2", - "@zowe/zos-jobs-for-zowe-sdk": "8.24.2", - "@zowe/zos-logs-for-zowe-sdk": "8.24.2", - "@zowe/zos-tso-for-zowe-sdk": "8.24.3", - "@zowe/zos-uss-for-zowe-sdk": "8.24.2", - "@zowe/zos-workflows-for-zowe-sdk": "8.24.2", - "@zowe/zosmf-for-zowe-sdk": "8.24.2", + "@zowe/core-for-zowe-sdk": "8.24.5", + "@zowe/imperative": "8.24.5", + "@zowe/provisioning-for-zowe-sdk": "8.24.5", + "@zowe/zos-console-for-zowe-sdk": "8.24.5", + "@zowe/zos-files-for-zowe-sdk": "8.24.5", + "@zowe/zos-jobs-for-zowe-sdk": "8.24.5", + "@zowe/zos-logs-for-zowe-sdk": "8.24.5", + "@zowe/zos-tso-for-zowe-sdk": "8.24.5", + "@zowe/zos-uss-for-zowe-sdk": "8.24.5", + "@zowe/zos-workflows-for-zowe-sdk": "8.24.5", + "@zowe/zosmf-for-zowe-sdk": "8.24.5", "find-process": "1.4.7", "lodash": "4.17.21", "minimatch": "9.0.5", @@ -2506,9 +2506,9 @@ } }, "node_modules/@zowe/cli-test-utils": { - "version": "8.24.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.24.2.tgz", - "integrity": "sha512-kJcKgFPFv3IFw0Nty9RcExOINb/M0wiYuYbYPiXfvx1mdEvf0V4kOFCOBlpuDO+rKhYpg+QDg7B2mB9IU2Nc3A==", + "version": "8.24.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.24.5.tgz", + "integrity": "sha512-UiJizEYQmxEq40RjEYDmpjRFERCpLQ0SPdjl+CNljKi47cQ/uPin94CGTjrYQoVf8kJU0SYprj5shr7dktniag==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -3088,9 +3088,9 @@ "license": "MIT" }, "node_modules/@zowe/cli/node_modules/@zowe/core-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-eo2JbXKREM1E6S6plynOu8aT7JQprzxkAAED0IkcOqzUPkU4X0zHhTeRexxkH1b50xD780ZXM/YttjcBusl7CA==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-4t8MHBqDCj6e7/GnU31aN3JRwXmHoukM/x7GlcOfvrvU2ADTwY6Eetvr1z7oaLGEhkRB5FewofqnQfvu2Hn++A==", "dev": true, "dependencies": { "comment-json": "~4.2.3", @@ -3104,9 +3104,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/imperative": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.24.2.tgz", - "integrity": "sha512-gMUYEVBfPMi9/g8VtNheP5qkc7Y1CSoU3I/LrnX57cJTi++UczZu4CeRMDCLAYKb266DIXIiHRmRVNhP17z+jQ==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.24.5.tgz", + "integrity": "sha512-wM3K5qEeCBKxrJVg4jBSzRfoZSQg5U82tYfLQuEUZPziN7LUsH04wLs9qy0yx+VEiO6q6EzajAEgyivtmecEDQ==", "dev": true, "dependencies": { "@types/yargs": "^17.0.32", @@ -3232,9 +3232,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/provisioning-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-7W3vRqtZDsMzGhWZes7/FDqTlfPB0Q4BfQE6ejhsTAW3IYonZoAbtTrX9ewA7oHQqIOX8pdwa9TtSgUCRtSKLg==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-mYbuKSb+8Blyo7MgD1VdLKRImiR2d8LY3w79hab8cncKURe9lYPxBawd/2h1xRlsnLUQVT2DfOjnS/4Yvk8pdg==", "dev": true, "dependencies": { "js-yaml": "^4.1.0" @@ -3259,9 +3259,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-console-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-dsfHAgFWhDbiD0/lNn3CiEnTDBXtDRZTjrIGTZDWEUo16gUvSrg+CoOmknZ1zzoMkYPLqyr8lVzkHcFeK5CWZw==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-Ke5VwZNGUO8CRwNiQMgbHerxCrZTigVAi0ahorAYXURs9VW+ciey0zLcWULxuS7FHcf6Fjv5yK0iodLlO639PQ==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3272,9 +3272,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-files-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-1UwLC5zsFTNqtc6+kLBHAUjIP/DD9mTpbqyGe7imYJyK0/1sU+z5nzpMl5MnTYZVQ3Q+bzqYzHLlv4J/d7AuCw==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-E/XuqdjdczrKl0IsUYmR1EpV6qqJRXaFJAvs8PrSklaqVkpFphg2wxq4yZorz8d75eHbtC+AF6Z9CRWQAT0mQg==", "dev": true, "dependencies": { "lodash": "^4.17.21", @@ -3289,12 +3289,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-jobs-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-R/y7SaaLZul6pHWadCaCt7Zrmwm3amOQ4OMm6O22vT1vfGf+bUKYleYu+nHmdS5/OIns79GdwQiHfLE4w2HNGA==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-dybtaT0Ag2BHK58ShihELAaStiXHmAFjFKIBU8GN6IGTXhEwEcujHfcUTZqOg0dXKxJ7gEABn11QdGo0Z3IRXA==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.24.2" + "@zowe/zos-files-for-zowe-sdk": "8.24.5" }, "engines": { "node": ">=18.12.0" @@ -3305,9 +3305,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-logs-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-Na8EjKHJNt8qVoTKwEqoVknLTuDiVvLF2VIgGd9+GnoCNX+CtmWJAAFg3quZ36QtC6ssJYCTc2hHT3qxhobX5g==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-43MyutusZugBfVWUZS6S0TcgKv5orM3cmLpbKmxzsUxjf1Zk1s9cbelgPgOqrPLKwMAqxy8vLBReWFufhFvMTA==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3318,12 +3318,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-tso-for-zowe-sdk": { - "version": "8.24.3", - "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.24.3.tgz", - "integrity": "sha512-WICorY6KchPojvGrDYh0xITXEdkcskkdj6bS1gzC3x1uKqRusnjVbfV+A03PtaoCtJzKDVSOcElkH46yrU98rw==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-LLdm77o5pTzybsICKFtnf4xZA+PTd6edHRzxOImoa7yUnpPrIiddm09ndde+O2Wtyj0B7UNwpLPyaeFS7C50wQ==", "dev": true, "dependencies": { - "@zowe/zosmf-for-zowe-sdk": "8.24.2" + "@zowe/zosmf-for-zowe-sdk": "8.24.5" }, "engines": { "node": ">=18.12.0" @@ -3334,9 +3334,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-uss-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-8Eurw5yEqGALhXPMhsJpQaiwE2a9wD9v/Am21nWK17+TMYiNQi/CocZCszsSGHRo/+FbAklTP/i/XgyJDavC3g==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-CoUBednyC1q3d5cu32cT1HiG3pE87WGUCKx13wbNuvWVaQ+xT/WmE4qRZbet9+pntvl74zWsQxoq6wi+n/NNvg==", "dev": true, "dependencies": { "ssh2": "^1.15.0" @@ -3349,12 +3349,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-workflows-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-Sdlv7R4nCWfw91qBd5fa7sa+Jg8KIPZ6eOTxrfL72LWvCwASJT47hEdk+FeOJYpcGZuOpnFdZQOL/WajymwOvg==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-yikJyC05qRiiehBVArVyhA8JcLXEEavoNDPaUjVokgq74UYMP3OtaCiB+ky+NXBrjoF2c83K+68qyx+EKVpgyA==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.24.2" + "@zowe/zos-files-for-zowe-sdk": "8.24.5" }, "engines": { "node": ">=18.12.0" @@ -3365,9 +3365,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zosmf-for-zowe-sdk": { - "version": "8.24.2", - "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.24.2.tgz", - "integrity": "sha512-ZbB0o0NBONf5q9mW3Gp48iIsdmi++NFi7u8CceFpEerszwHFcRippDJZspPiDHNDThwxbvRK/ATwK/C0Pw5vVQ==", + "version": "8.24.5", + "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.24.5.tgz", + "integrity": "sha512-moWr2JqZTmxKtaULWm4CvUsVaKpiF9jv6YBTj/1ZNK5Mny98FoOR4iG0EyCiaayjW4IohdDPP4phPX3aySSm6A==", "dev": true, "engines": { "node": ">=18.12.0" @@ -6036,9 +6036,9 @@ } }, "node_modules/@zowe/imperative": { - "version": "8.24.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.24.2.tgz", - "integrity": "sha512-gMUYEVBfPMi9/g8VtNheP5qkc7Y1CSoU3I/LrnX57cJTi++UczZu4CeRMDCLAYKb266DIXIiHRmRVNhP17z+jQ==", + "version": "8.24.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.24.5.tgz", + "integrity": "sha512-wM3K5qEeCBKxrJVg4jBSzRfoZSQg5U82tYfLQuEUZPziN7LUsH04wLs9qy0yx+VEiO6q6EzajAEgyivtmecEDQ==", "dev": true, "license": "EPL-2.0", "dependencies": { diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 2eeabd7060..b3327305c5 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -54,9 +54,9 @@ "@types/node": "20.19.8", "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", - "@zowe/cli": "8.24.4", - "@zowe/cli-test-utils": "8.24.2", - "@zowe/imperative": "8.24.2", + "@zowe/cli": "8.24.5", + "@zowe/cli-test-utils": "8.24.5", + "@zowe/imperative": "8.24.5", "copyfiles": "2.4.1", "env-cmd": "10.1.0", "eslint": "9.31.0", @@ -82,11 +82,11 @@ "@babel/traverse": "7.28.0" }, "peerDependencies": { - "@zowe/imperative": "8.24.2" + "@zowe/imperative": "8.24.5" }, "engines": { "npm": "=10.9.3", - "node": "=20.19.3" + "node": "=20.19.4" }, "jest": { "modulePathIgnorePatterns": [ From 57c6841cfa82d5b3aed168d21d9b2e4b9a81320d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Fri, 18 Jul 2025 17:51:14 +0200 Subject: [PATCH 028/152] fix: Secure x-forwarded-* headers from untrusted proxies (#4171) Signed-off-by: Richard Salac Co-authored-by: Elena Kubantseva Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 70 ++++--- .../zowe/apiml/product/web/HttpConfig.java | 55 +++-- .../apiml/product/web/HttpConfigTest.java | 47 +++-- .../common/verify/CertificateValidator.java | 2 + .../zowe/apiml/security/SecurityUtils.java | 5 + .../src/main/resources/bin/start.sh | 1 + .../gateway/config/ConnectionsConfig.java | 150 +++++--------- .../apiml/gateway/config/WebSecurity.java | 19 ++ ...AdditionalRegistrationGatewayRegistry.java | 123 +++++++++++ ...X509AndGwAwareXForwardedHeadersFilter.java | 156 ++++++++++++++ .../service/CertificateChainService.java | 4 +- .../AcceptanceTestWithMockServices.java | 69 ++++++- .../MutateRemoteAddressFilter.java | 54 +++++ .../XForwardedHeadersProxyTest.java | 149 ++++++++++++++ .../XForwardedHeadersTrustedProxyTest.java | 137 +++++++++++++ .../config/AdditionalRegistrationTest.java | 20 +- .../gateway/config/ConnectionsConfigTest.java | 87 +++----- ...tionalRegistrationGatewayRegistryTest.java | 194 ++++++++++++++++++ .../service/CertificateChainServiceTest.java | 21 +- .../src/test/resources/application.yml | 10 + integration-tests/build.gradle | 2 + .../gateway/CentralRegistryTest.java | 13 +- .../proxy/XForwardHeadersProxyTest.java | 147 +++++++++++++ 23 files changed, 1289 insertions(+), 246 deletions(-) create mode 100644 gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistry.java create mode 100644 gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/X509AndGwAwareXForwardedHeadersFilter.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/MutateRemoteAddressFilter.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersProxyTest.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersTrustedProxyTest.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistryTest.java create mode 100644 integration-tests/src/test/java/org/zowe/apiml/integration/proxy/XForwardHeadersProxyTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8f49c02b02..5c44ebd78d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -687,61 +687,67 @@ jobs: timeout-minutes: 10 services: - # First group of services represents central apiml instance with central gateway registry - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs + # Domain apiml instance which registers it's gateway in central's discovery service discovery-service: image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs zaas-service: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SECURITY_X509_ENABLED: true APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true - APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates - logbackService: ZWEAAZ1 + APIML_SECURITY_X509_CERTIFICATESURL: https://central-gateway-service:10010/gateway/certificates + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/ + logbackService: ZWEAAZ2 gateway-service: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: - APIML_SERVICE_APIMLID: central-apiml - APIML_SERVICE_HOSTNAME: gateway-service + APIML_SERVICE_APIMLID: domain-apiml APIML_SERVICE_EXTERNALURL: https://gateway-service:10010 + APIML_GATEWAY_REGISTRY_ENABLED: false + APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka/ + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: / + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: / + logbackService: ZWEAGW2 + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + + # Central apiml instance with central gateway registry + central-gateway-service: + image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_APIMLID: central-apiml + APIML_SERVICE_HOSTNAME: central-gateway-service + APIML_SERVICE_EXTERNALURL: https://central-gateway-service:10010 APIML_GATEWAY_REGISTRY_ENABLED: true APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/ logbackService: ZWEAGW1 - mock-services: - image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - - # Second group of services represents domain apiml instance which registers it's gateway in central's discovery service - discovery-service-2: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} + api-catalog-services-2: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} volumes: - /api-defs:/api-defs + env: + APIML_SERVICE_HOSTNAME: api-catalog-services-2 + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/ + discovery-service-2: + image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_HOSTNAME: discovery-service-2 - APIML_SERVICE_PORT: 10031 zaas-service-2: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: + APIML_SERVICE_HOSTNAME: zaas-service-2 + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/ APIML_SECURITY_X509_ENABLED: true APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true - APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/ - logbackService: ZWEAAZ2 - gateway-service-2: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_APIMLID: domain-apiml - APIML_SERVICE_HOSTNAME: gateway-service-2 - APIML_SERVICE_EXTERNALURL: https://gateway-service-2:10010 - APIML_GATEWAY_REGISTRY_ENABLED: false - APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/ - ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka - ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: / - ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: / - logbackService: ZWEAGW2 + APIML_SECURITY_X509_CERTIFICATESURL: https://central-gateway-service:10010/gateway/certificates + logbackService: ZWEAAZ1 + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} steps: - uses: actions/checkout@v4 diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java index aa9a397f5e..d000a8a8e7 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java @@ -12,6 +12,7 @@ import jakarta.annotation.PostConstruct; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -21,12 +22,18 @@ import org.apache.hc.core5.http.config.RegistryBuilder; import org.apache.hc.core5.util.Timeout; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; -import org.zowe.apiml.security.*; +import org.zowe.apiml.security.ApimlPoolingHttpClientConnectionManager; +import org.zowe.apiml.security.HttpsConfig; +import org.zowe.apiml.security.HttpsConfigError; +import org.zowe.apiml.security.HttpsFactory; +import org.zowe.apiml.security.SecurityUtils; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -39,6 +46,8 @@ @Slf4j @Configuration +@RequiredArgsConstructor +@Getter public class HttpConfig { private static final char[] KEYRING_PASSWORD = "password".toCharArray(); @@ -51,7 +60,7 @@ public class HttpConfig { private String[] ciphers; @Value("${server.ssl.trustStore:#{null}}") - private String trustStore; + private String trustStorePath; @Value("${server.ssl.trustStorePassword:#{null}}") private char[] trustStorePassword; @@ -63,7 +72,7 @@ public class HttpConfig { private String keyAlias; @Value("${server.ssl.keyStore:#{null}}") - private String keyStore; + private String keyStorePath; @Value("${server.ssl.keyStorePassword:#{null}}") private char[] keyStorePassword; @@ -99,23 +108,24 @@ public class HttpConfig { "ApimlHttpClientConfiguration.connectionManagerTimer", true); private CloseableHttpClient secureHttpClient; private CloseableHttpClient secureHttpClientWithoutKeystore; - @Getter private HttpsConfig httpsConfig; - @Getter + private HttpsFactory httpsFactory; private SSLContext secureSslContext; - @Getter private SSLContext secureSslContextWithoutKeystore; - @Getter private HostnameVerifier secureHostnameVerifier; private Set publicKeyCertificatesBase64; + private final ApplicationContext context; void updateStorePaths() { - if (SecurityUtils.isKeyring(keyStore)) { - keyStore = SecurityUtils.formatKeyringUrl(keyStore); + ServerProperties serverProperties = context.getBean(ServerProperties.class); + if (SecurityUtils.isKeyring(keyStorePath)) { + keyStorePath = SecurityUtils.formatKeyringUrl(keyStorePath); + serverProperties.getSsl().setKeyStore(keyStorePath); if (keyStorePassword == null) keyStorePassword = KEYRING_PASSWORD; } - if (SecurityUtils.isKeyring(trustStore)) { - trustStore = SecurityUtils.formatKeyringUrl(trustStore); + if (SecurityUtils.isKeyring(trustStorePath)) { + trustStorePath = SecurityUtils.formatKeyringUrl(trustStorePath); + serverProperties.getSsl().setTrustStore(trustStorePath); if (trustStorePassword == null) trustStorePassword = KEYRING_PASSWORD; } } @@ -126,14 +136,14 @@ public void init() { try { X509Certificate certificate = null; - if (keyStore != null) { - KeyStore ks = SecurityUtils.loadKeyStore(keyStoreType, keyStore, keyStorePassword); + if (keyStorePath != null) { + KeyStore ks = SecurityUtils.loadKeyStore(keyStoreType, keyStorePath, keyStorePassword); certificate = (X509Certificate) ks.getCertificate(keyAlias); } Supplier httpsConfigSupplier = () -> HttpsConfig.builder() .protocol(protocol).enabledProtocols(supportedProtocols).cipherSuite(ciphers) - .trustStore(trustStore).trustStoreType(trustStoreType) + .trustStore(trustStorePath).trustStoreType(trustStoreType) .trustStorePassword(trustStorePassword).trustStoreRequired(trustStoreRequired) .verifySslCertificatesOfServices(verifySslCertificatesOfServices) .nonStrictVerifySslCertificatesOfServices(nonStrictVerifySslCertificatesOfServices) @@ -142,7 +152,7 @@ public void init() { .timeToLive(timeToLive); httpsConfig = httpsConfigSupplier.get() - .keyAlias(keyAlias).keyStore(keyStore).keyPassword(keyPassword) + .keyAlias(keyAlias).keyStore(keyStorePath).keyPassword(keyPassword) .keyStorePassword(keyStorePassword).keyStoreType(keyStoreType).certificate(certificate) .build(); @@ -150,11 +160,11 @@ public void init() { log.debug("Using HTTPS configuration: {}", httpsConfig.toString()); - HttpsFactory factory = new HttpsFactory(httpsConfig); - ApimlPoolingHttpClientConnectionManager secureConnectionManager = getConnectionManager(factory); - secureHttpClient = factory.buildHttpClient(secureConnectionManager); - secureSslContext = factory.getSslContext(); - secureHostnameVerifier = factory.getHostnameVerifier(); + httpsFactory = new HttpsFactory(httpsConfig); + ApimlPoolingHttpClientConnectionManager secureConnectionManager = getConnectionManager(httpsFactory); + secureHttpClient = httpsFactory.buildHttpClient(secureConnectionManager); + secureSslContext = httpsFactory.getSslContext(); + secureHostnameVerifier = httpsFactory.getHostnameVerifier(); HttpsFactory factoryWithoutKeystore = new HttpsFactory(httpsConfigWithoutKeystore); ApimlPoolingHttpClientConnectionManager connectionManagerWithoutKeystore = getConnectionManager(factoryWithoutKeystore); secureHttpClientWithoutKeystore = factoryWithoutKeystore.buildHttpClient(connectionManagerWithoutKeystore); @@ -206,6 +216,11 @@ public HttpsConfig httpsConfig() { return httpsConfig; } + @Bean + public HttpsFactory httpsFactory() { + return httpsFactory; + } + /** * Returns RestTemplate with keystore. This RestTemplate makes calls to other systems with a certificate to sign to * other systems by certificate. It is necessary to call systems like DiscoverySystem etc. diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/web/HttpConfigTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/web/HttpConfigTest.java index 32829ecf6f..fe43429c1a 100644 --- a/apiml-common/src/test/java/org/zowe/apiml/product/web/HttpConfigTest.java +++ b/apiml-common/src/test/java/org/zowe/apiml/product/web/HttpConfigTest.java @@ -10,45 +10,68 @@ package org.zowe.apiml.product.web; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.server.Ssl; +import org.springframework.context.ApplicationContext; import org.springframework.test.util.ReflectionTestUtils; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) class HttpConfigTest { + @Mock + ApplicationContext context; + + @InjectMocks + HttpConfig httpConfig; + @Nested class KeyringFormatAndPasswordUpdate { + @BeforeEach + void setup() { + ServerProperties properties = new ServerProperties(); + properties.setSsl(new Ssl()); + when(context.getBean(ServerProperties.class)).thenReturn(properties); + } + @Test void whenKeyringHasWrongFormatAndMissingPasswords_thenFixIt() { - HttpConfig httpConfig = new HttpConfig(); - ReflectionTestUtils.setField(httpConfig, "keyStore", "safkeyring:///userId/ringId1"); - ReflectionTestUtils.setField(httpConfig, "trustStore", "safkeyring:////userId/ringId2"); + ReflectionTestUtils.setField(httpConfig, "keyStorePath", "safkeyring:///userId/ringId1"); + ReflectionTestUtils.setField(httpConfig, "trustStorePath", "safkeyring:////userId/ringId2"); httpConfig.updateStorePaths(); - assertEquals("safkeyring://userId/ringId1", ReflectionTestUtils.getField(httpConfig, "keyStore")); - assertEquals("safkeyring://userId/ringId2", ReflectionTestUtils.getField(httpConfig, "trustStore")); + assertEquals("safkeyring://userId/ringId1", ReflectionTestUtils.getField(httpConfig, "keyStorePath")); + assertEquals("safkeyring://userId/ringId2", ReflectionTestUtils.getField(httpConfig, "trustStorePath")); assertArrayEquals("password".toCharArray(), (char[]) ReflectionTestUtils.getField(httpConfig, "keyStorePassword")); assertArrayEquals("password".toCharArray(), (char[]) ReflectionTestUtils.getField(httpConfig, "trustStorePassword")); } @Test void whenKeystore_thenDoNothing() { - HttpConfig httpConfig = new HttpConfig(); - ReflectionTestUtils.setField(httpConfig, "keyStore", "/path1"); - ReflectionTestUtils.setField(httpConfig, "trustStore", "/path2"); + ReflectionTestUtils.setField(httpConfig, "keyStorePath", "/path1"); + ReflectionTestUtils.setField(httpConfig, "trustStorePath", "/path2"); httpConfig.updateStorePaths(); - assertEquals("/path1", ReflectionTestUtils.getField(httpConfig, "keyStore")); - assertEquals("/path2", ReflectionTestUtils.getField(httpConfig, "trustStore")); + assertEquals("/path1", ReflectionTestUtils.getField(httpConfig, "keyStorePath")); + assertEquals("/path2", ReflectionTestUtils.getField(httpConfig, "trustStorePath")); assertNull(ReflectionTestUtils.getField(httpConfig, "keyStorePassword")); assertNull(ReflectionTestUtils.getField(httpConfig, "trustStorePassword")); } } -} \ No newline at end of file +} diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java index db2131035c..3659df3ae7 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java @@ -57,6 +57,8 @@ public CertificateValidator(TrustedCertificatesProvider trustedCertificatesProvi * @return true if all given certificates are known false otherwise */ public boolean hasGatewayChain(X509Certificate[] certs) { + if (certs == null || certs.length == 0) return false; + if ((proxyCertificatesEndpoints == null) || (proxyCertificatesEndpoints.length == 0)) { log.debug("No endpoint configured to retrieve trusted certificates. Provide URL via apiml.security.x509.certificatesUrls"); return false; 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 e36e7fccac..ebac7387d3 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 @@ -24,6 +24,7 @@ import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Base64; import java.util.Enumeration; @@ -126,6 +127,10 @@ public static Set loadCertificateChainBase64(HttpsConfig config) throws return out; } + public String base64EncodePublicKey(X509Certificate cert) { + return Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded()); + } + /** * Loads public key from keystore or key ring, if keystore URL has proper format {@link #KEYRING_PATTERN} * diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 105397f4f9..0bc23d898c 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -315,6 +315,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.security.authorization.endpoint.enabled=${ZWE_configs_apiml_security_authorization_endpoint_enabled:-false} \ -Dapiml.security.authorization.endpoint.url=${ZWE_configs_apiml_security_authorization_endpoint_url:-${ZWE_components_gateway_apiml_security_authorization_endpoint_url:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf-auth"}} \ -Dapiml.security.authorization.provider=${ZWE_configs_apiml_security_authorization_provider:-"native"} \ + -Dapiml.security.forwardHeader.trustedProxies=${ZWE_configs_apiml_security_forwardHeader_trustedProxies:-} \ -Dapiml.security.ssl.nonStrictVerifySslCertificatesOfServices=${nonStrictVerifySslCertificatesOfServices:-false} \ -Dapiml.security.ssl.verifySslCertificatesOfServices=${verifySslCertificatesOfServices} \ -Dapiml.security.x509.acceptForwardedCert=${ZWE_configs_apiml_security_x509_acceptForwardedCert:-false} \ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index 9012d06b14..492579af56 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -11,7 +11,11 @@ package org.zowe.apiml.gateway.config; import com.google.common.annotations.VisibleForTesting; -import com.netflix.appinfo.*; +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.appinfo.HealthCheckHandler; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.LeaseInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; @@ -19,7 +23,6 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.resolver.DefaultAddressResolverGroup; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; @@ -50,7 +53,11 @@ import org.springframework.cloud.netflix.eureka.http.RestClientTransportClientFactories; import org.springframework.cloud.util.ProxyUtils; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestClient; @@ -62,11 +69,12 @@ import org.zowe.apiml.config.AdditionalRegistrationCondition; import org.zowe.apiml.config.AdditionalRegistrationParser; import org.zowe.apiml.constants.EurekaMetadataDefinition; +import org.zowe.apiml.gateway.filters.proxyheaders.AdditionalRegistrationGatewayRegistry; +import org.zowe.apiml.gateway.filters.proxyheaders.X509AndGwAwareXForwardedHeadersFilter; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; -import org.zowe.apiml.security.HttpsConfig; +import org.zowe.apiml.product.web.HttpConfig; import org.zowe.apiml.security.HttpsConfigError; -import org.zowe.apiml.security.HttpsFactory; import org.zowe.apiml.security.SecurityUtils; import org.zowe.apiml.util.CorsUtils; import reactor.netty.http.client.HttpClient; @@ -88,107 +96,34 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import static org.springframework.cloud.netflix.eureka.EurekaClientConfigBean.DEFAULT_ZONE; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.REGISTRATION_TYPE; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; //TODO this configuration should be removed as redundancy of the HttpConfig in the apiml-common @Configuration @Slf4j +@RequiredArgsConstructor public class ConnectionsConfig { - private static final char[] KEYRING_PASSWORD = "password".toCharArray(); - - @Value("${server.ssl.protocol:TLSv1.2}") - private String protocol; - - @Value("${server.ssl.trustStore:#{null}}") - private String trustStorePath; - - @Value("${server.ssl.trustStorePassword:#{null}}") - private char[] trustStorePassword; - - @Value("${server.ssl.trustStoreType:PKCS12}") - private String trustStoreType; - - @Value("${server.ssl.keyAlias:#{null}}") - private String keyAlias; - - @Value("${server.ssl.keyStore:#{null}}") - private String keyStorePath; - - @Value("${server.ssl.keyStorePassword:#{null}}") - private char[] keyStorePassword; - - @Value("${server.ssl.keyPassword:#{null}}") - private char[] keyPassword; - - @Value("${server.ssl.keyStoreType:PKCS12}") - private String keyStoreType; - - @Value("${apiml.security.ssl.verifySslCertificatesOfServices:true}") - private boolean verifySslCertificatesOfServices; - - @Value("${apiml.security.ssl.nonStrictVerifySslCertificatesOfServices:false}") - private boolean nonStrictVerifySslCertificatesOfServices; - - @Value("${server.ssl.trustStoreRequired:false}") - private boolean trustStoreRequired; - @Value("${eureka.client.serviceUrl.defaultZone}") private String eurekaServerUrl; - @Value("${apiml.connection.idleConnectionTimeoutSeconds:#{5}}") - private int idleConnTimeoutSeconds; - - @Value("${apiml.connection.timeout:60000}") - private int requestTimeout; @Value("${apiml.service.corsEnabled:false}") private boolean corsEnabled; private final ApplicationContext context; + private final HttpConfig config; private static final ApimlLogger apimlLog = ApimlLogger.of(ConnectionsConfig.class, YamlMessageServiceInstance.getInstance()); - private HttpsFactory httpsFactory; - - public ConnectionsConfig(ApplicationContext context) { - this.context = context; - } @Value("${apiml.service.externalUrl:}") private String externalUrl; - @PostConstruct - public void updateConfigParameters() { - ServerProperties serverProperties = context.getBean(ServerProperties.class); - if (SecurityUtils.isKeyring(keyStorePath)) { - keyStorePath = SecurityUtils.formatKeyringUrl(keyStorePath); - serverProperties.getSsl().setKeyStore(keyStorePath); - if (keyStorePassword == null) keyStorePassword = KEYRING_PASSWORD; - } - if (SecurityUtils.isKeyring(trustStorePath)) { - trustStorePath = SecurityUtils.formatKeyringUrl(trustStorePath); - serverProperties.getSsl().setTrustStore(trustStorePath); - if (trustStorePassword == null) trustStorePassword = KEYRING_PASSWORD; - } - httpsFactory = factory(); - } - - public HttpsFactory factory() { - HttpsConfig config = HttpsConfig.builder() - .protocol(protocol) - .verifySslCertificatesOfServices(verifySslCertificatesOfServices) - .nonStrictVerifySslCertificatesOfServices(nonStrictVerifySslCertificatesOfServices) - .trustStorePassword(trustStorePassword).trustStoreRequired(trustStoreRequired) - .idleConnTimeoutSeconds(idleConnTimeoutSeconds).requestConnectionTimeout(requestTimeout) - .trustStore(trustStorePath).trustStoreType(trustStoreType) - .keyAlias(keyAlias).keyStore(keyStorePath).keyPassword(keyPassword) - .keyStorePassword(keyStorePassword).keyStoreType(keyStoreType).build(); - log.info("Using HTTPS configuration: {}", config.toString()); - - return new HttpsFactory(config); - } - /** * @param httpClient default http client * @param headersFiltersProvider header filter for spring gateway router @@ -202,7 +137,7 @@ NettyRoutingFilterApiml createNettyRoutingFilterApiml(HttpClient httpClient, Obj public HttpClient getHttpClient(HttpClient httpClient, boolean useClientCert) { var sslContextBuilder = SslProvider.builder().sslContext(getSslContext(useClientCert)); - if (!nonStrictVerifySslCertificatesOfServices) { + if (!config.isNonStrictVerifySslCertificatesOfServices()) { sslContextBuilder.handlerConfigurator(HttpClientSecurityUtils.HOSTNAME_VERIFICATION_CONFIGURER); } return httpClient.secure(sslContextBuilder.build()); @@ -234,7 +169,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro @VisibleForTesting X509KeyManager x509KeyManagerSelectedAlias(KeyManagerFactory keyManagerFactory) { - return new X509KeyManagerSelectedAlias(keyManagerFactory, keyAlias); + return new X509KeyManagerSelectedAlias(keyManagerFactory, config.getKeyAlias()); } /** @@ -244,16 +179,18 @@ SslContext getSslContext(boolean setKeystore) { try { SslContextBuilder builder = SslContextBuilder.forClient(); - KeyStore trustStore = SecurityUtils.loadKeyStore(trustStoreType, trustStorePath, trustStorePassword); + KeyStore trustStore = SecurityUtils.loadKeyStore( + config.getTrustStoreType(), config.getTrustStorePath(), config.getTrustStorePassword()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(trustStore); builder.trustManager(trustManagerFactory); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); if (setKeystore) { - log.info("Loading keystore: {}: {}", keyStoreType, keyStorePath); - KeyStore keyStore = SecurityUtils.loadKeyStore(keyStoreType, keyStorePath, keyStorePassword); - keyManagerFactory.init(keyStore, keyStorePassword); + log.info("Loading keystore: {}: {}", config.getKeyStoreType(), config.getKeyStorePath()); + KeyStore keyStore = SecurityUtils.loadKeyStore( + config.getKeyStoreType(), config.getKeyStorePath(), config.getKeyStorePassword()); + keyManagerFactory.init(keyStore, config.getKeyStorePassword()); builder.keyManager(x509KeyManagerSelectedAlias(keyManagerFactory)); } else { KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); @@ -262,7 +199,7 @@ SslContext getSslContext(boolean setKeystore) { builder.keyManager(keyManagerFactory); } - if (verifySslCertificatesOfServices && nonStrictVerifySslCertificatesOfServices) { + if (config.isVerifySslCertificatesOfServices() && config.isNonStrictVerifySslCertificatesOfServices()) { builder.endpointIdentificationAlgorithm(null); } @@ -270,7 +207,7 @@ SslContext getSslContext(boolean setKeystore) { } catch (Exception e) { apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, - HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, factory().getConfig()); + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config.httpsConfig()); } } @@ -306,8 +243,8 @@ public RestClientDiscoveryClientOptionalArgs defaultArgs(DefaultEurekaClientHttp if (eurekaServerUrl.startsWith("http://")) { apimlLog.log("org.zowe.apiml.common.insecureHttpWarning"); } else { - clientArgs.setSSLContext(httpsFactory.getSslContext()); - clientArgs.setHostnameVerifier(httpsFactory.getHostnameVerifier()); + clientArgs.setSSLContext(config.httpsFactory().getSslContext()); + clientArgs.setHostnameVerifier(config.httpsFactory().getHostnameVerifier()); } return clientArgs; @@ -324,17 +261,24 @@ List additionalRegistration() { @Bean(destroyMethod = "shutdown") @Conditional(AdditionalRegistrationCondition.class) @RefreshScope - AdditionalEurekaClientsHolder additionalEurekaClientsHolder(ApplicationInfoManager manager, - EurekaClientConfig config, - List additionalRegistrations, - EurekaFactory eurekaFactory, - @Autowired(required = false) HealthCheckHandler healthCheckHandler + AdditionalEurekaClientsHolder additionalEurekaClientsHolder( + ApplicationInfoManager manager, + EurekaClientConfig config, + List additionalRegistrations, + EurekaFactory eurekaFactory, + @Autowired(required = false) HealthCheckHandler healthCheckHandler, + AdditionalRegistrationGatewayRegistry additionalRegistrationGatewayRegistry, + Optional x509awareXForwardedHeadersFilter ) { List additionalClients = new ArrayList<>(additionalRegistrations.size()); for (AdditionalRegistration apimlRegistration : additionalRegistrations) { CloudEurekaClient cloudEurekaClient = registerInTheApimlInstance(config, apimlRegistration, manager, eurekaFactory); additionalClients.add(cloudEurekaClient); cloudEurekaClient.registerHealthCheck(healthCheckHandler); + + x509awareXForwardedHeadersFilter + .ifPresent(__ -> + additionalRegistrationGatewayRegistry.registerCacheRefreshEventListener(cloudEurekaClient)); } return new AdditionalEurekaClientsHolder(additionalClients); } @@ -388,7 +332,11 @@ private void updateMetadata(InstanceInfo instanceInfo, AdditionalRegistration ad @Bean Customizer defaultCustomizer() { return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) - .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()).timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(requestTimeout)).build()).build()); + .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) + .timeLimiterConfig( + TimeLimiterConfig.custom() + .timeoutDuration(Duration.ofMillis(config.getRequestConnectionTimeout())) + .build()).build()); } @Bean diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java index e307643306..e865e21088 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java @@ -17,7 +17,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -68,20 +70,26 @@ import org.springframework.web.server.WebFilter; import org.zowe.apiml.gateway.config.oidc.ClientConfiguration; import org.zowe.apiml.gateway.controllers.GatewayExceptionHandler; +import org.zowe.apiml.gateway.filters.proxyheaders.AdditionalRegistrationGatewayRegistry; +import org.zowe.apiml.gateway.filters.proxyheaders.X509AndGwAwareXForwardedHeadersFilter; import org.zowe.apiml.gateway.filters.security.AuthExceptionHandlerReactive; import org.zowe.apiml.gateway.filters.security.BasicAuthFilter; import org.zowe.apiml.gateway.filters.security.TokenAuthFilter; import org.zowe.apiml.gateway.service.BasicAuthProvider; import org.zowe.apiml.gateway.service.TokenProvider; +import org.zowe.apiml.security.HttpsConfig; import org.zowe.apiml.security.common.util.X509Util; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.config.SafSecurityConfigurationProperties; import reactor.core.publisher.Mono; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -532,4 +540,15 @@ StrictServerWebExchangeFirewall httpFirewall() { return firewall; } + @Bean + @Primary + @ConditionalOnProperty(name = "spring.cloud.gateway.x-forwarded.enabled", matchIfMissing = true) + public XForwardedHeadersFilter xForwardedHeadersFilter( + @Value("${apiml.security.forwardHeader.trustedProxies:#{null}}") String trustedProxies, + HttpsConfig httpsConfig, + AdditionalRegistrationGatewayRegistry additionalRegistrationGatewayRegistry + ) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + return new X509AndGwAwareXForwardedHeadersFilter(httpsConfig, trustedProxies, additionalRegistrationGatewayRegistry); + } + } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistry.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistry.java new file mode 100644 index 0000000000..49ca128710 --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistry.java @@ -0,0 +1,123 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.filters.proxyheaders; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.CacheRefreshedEvent; +import com.netflix.discovery.DiscoveryClient; +import com.netflix.discovery.EurekaEvent; +import com.netflix.discovery.shared.Application; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.zowe.apiml.product.constants.CoreService; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + * Registry for APIML gateways discovered through additional registrations. + *

+ * This class maintains a cache of cloud (central) and standard APIML gateways obtained via additional + * registration sources. The cache is automatically updated when the + * {@code registerAdditionalRegistrationsGatewayRegistryRefresh} handler is registered with the + * {@link DiscoveryClient} associated with the additional registration. + *

+ * The primary purpose of this class is to retain the IP addresses of other APIML gateways, so they + * can be used to evaluate trusted proxy headers. + */ + +@Component +@Slf4j +public class AdditionalRegistrationGatewayRegistry { + + @Value("${apiml.forwardHeader.trustedProxiesCacheTimeout:5m}") + Duration registryExpiration; + + @Getter + AtomicReference> additionalGatewayIpAddressesReference = new AtomicReference<>(Collections.emptySet()); + Cache> knownAdditionalGateways; + + @PostConstruct + public void init() { + knownAdditionalGateways = CacheBuilder.newBuilder().expireAfterWrite(registryExpiration.toMillis(), MILLISECONDS).build(); + log.debug("AdditionalRegistrationGatewayRegistry initialized"); + } + + public void registerCacheRefreshEventListener(DiscoveryClient additionalApimlRegistration) { + additionalApimlRegistration.registerEventListener( + event -> cacheRefreshEventHandler(event, additionalApimlRegistration)); + log.debug("AdditionalRegistrationGatewayRegistry refresh registered for additional registration: {}", + additionalApimlRegistration.getEurekaClientConfig().getEurekaServerServiceUrls(null)); + } + + void cacheRefreshEventHandler(EurekaEvent event, DiscoveryClient additionalApimlRegistration) { + if (event instanceof CacheRefreshedEvent) { + Set additionalGateways = Stream.of( + additionalApimlRegistration.getApplication(CoreService.GATEWAY.getServiceId()) + ) + .filter(Objects::nonNull) + .map(Application::getInstances) + .flatMap(List::stream) + .flatMap(this::processInstanceInfoForIpAddresses) + .collect(Collectors.toSet()); + log.debug("Additional registrations gateway ip addresses resolved: {}", additionalGateways); + additionalGatewayIpAddressesReference.set(additionalGateways); + } + } + + private InetAddress[] getInetAddressesByName(String instanceId, String networkName) { + try { + return InetAddress.getAllByName(networkName); + } catch (UnknownHostException e) { + log.debug("Unknown host or address for instance {} by {}", instanceId, networkName, e); + return new InetAddress[0]; + } + } + + private Stream processInstanceInfoForIpAddresses(InstanceInfo instanceInfo) { + try { + return knownAdditionalGateways.get(instanceInfo.getInstanceId(), () -> { + List addresses = Stream.of( + getInetAddressesByName(instanceInfo.getInstanceId(), instanceInfo.getHostName()), + getInetAddressesByName(instanceInfo.getInstanceId(), instanceInfo.getIPAddr()) + ) + .filter(Objects::nonNull) + .flatMap(Stream::of) + .map(InetAddress::getHostAddress) + .distinct() + .collect(Collectors.toList()); + log.debug("Additional registrations gateway ip addresses for instance {} resolved: {}", instanceInfo.getInstanceId(), addresses); + return addresses; + } + ).stream(); + } catch (ExecutionException e) { + log.debug("Unable to update additional gateway registry for instance {}.", instanceInfo, e); + return Stream.empty(); + } + } +} + diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/X509AndGwAwareXForwardedHeadersFilter.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/X509AndGwAwareXForwardedHeadersFilter.java new file mode 100644 index 0000000000..ab1070de5d --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/proxyheaders/X509AndGwAwareXForwardedHeadersFilter.java @@ -0,0 +1,156 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.filters.proxyheaders; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.http.server.reactive.SslInfo; +import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.security.HttpsConfig; +import org.zowe.apiml.security.SecurityUtils; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * The aim of the class is to ensure that the content of X-Forwarded-* or Forwarded headers contains only trusted data. + * Forged values of these headers can pose a security risk as identified by CVE-2025-41235. + * Trusted proxies can be defined via config property `apiml.security.forwardHeader.trustedProxies` that is equivalent of + * the Spring's original one `spring.cloud.gateway.mvc.trustedProxies`. The benefit of this bean is that it is not + * necessary to validate headers if the request is signed by trusted Gateway certificate. It solves, for example, + * the case when multiple APIML Gateways routes each other. The http context cannot be compromised when the request + * is signed by a trusted certificate, so the content of headers is considered valid, and it is not necessary to verify + * the host against the list. Otherwise, if the request is not signed, the client address should be validated against configuration. + *

+ * The implementation supports empty configuration value. The empty value means when the request is signed, the + * content of headers is accepted otherwise the headers are considered vulnerable and are removed from the request. + *

+ * Signed / defined pattern | empty list of trusted proxies | a trusted proxy is defined + * -------------------------+-------------------------------+--------------------------- + * untrusted signature/no | headers removed | check against the list + * -------------------------+-------------------------------+--------------------------- + * trusted signature | forward headers | forward headers + */ + +@Slf4j +public class X509AndGwAwareXForwardedHeadersFilter extends XForwardedHeadersFilter { + + // Generic all-in-one Forwarded header not handled by the default spring filter + public static final String FORWARDED_HEADER = "Forwarded"; + + final Set certificateChainBase64; + final Predicate isProxyTrusted; + final String trustedProxiesRegex; + final AtomicReference> trustedAdditionalGateways; + + /* + * + * @param httpsConfig gateway certificate configuration + * @param trustedProxiesPattern configuration value of a pattern on how validate proxy + * @param additionalRegistrationGatewayRegistry cache of apiml gateway ip addresses from additional registrations + * + */ + public X509AndGwAwareXForwardedHeadersFilter( + HttpsConfig httpsConfig, + String trustedProxiesPattern, + AdditionalRegistrationGatewayRegistry additionalRegistrationGatewayRegistry) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + // Trustworthiness of a proxy is evaluated by this class, + // hence the spring filter must trust everything and not interfere + super(".*"); + certificateChainBase64 = SecurityUtils.loadCertificateChainBase64(httpsConfig); + trustedProxiesRegex = trustedProxiesPattern; + trustedAdditionalGateways = additionalRegistrationGatewayRegistry.getAdditionalGatewayIpAddressesReference(); + + Predicate isTrusted = host -> trustedAdditionalGateways.get().contains(host); + if (StringUtils.isEmpty(trustedProxiesRegex)) { + isTrusted = isTrusted.or(host -> false); + } else { + Pattern pattern = Pattern.compile(trustedProxiesRegex); + isTrusted = isTrusted.or(host -> host != null && pattern.matcher(host).matches()); + } + this.isProxyTrusted = isTrusted; + } + + @Override + public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { + if (!hasXForwardedHeader(input)) return super.filter(input, exchange); + + boolean trustedSourceByX509 = Optional.ofNullable(exchange.getRequest().getSslInfo()) + .map(SslInfo::getPeerCertificates) + .filter(certs -> certs.length > 0) + .map(certs -> Arrays.stream(certs) + .map(SecurityUtils::base64EncodePublicKey) + .allMatch(certificateChainBase64::contains) + ) + .orElse(false); + + if (!trustedSourceByX509) { + ServerHttpRequest request = exchange.getRequest(); + InetSocketAddress remoteAddress = request.getRemoteAddress(); + if (remoteAddress == null) { + log.debug("Remote address is null and cannot be evaluated for trusted proxy."); + return super.filter(removeXForwardHttpHeaders(input), exchange); + } + if (!isProxyTrusted.test(remoteAddress.getHostString())) { + //Mask the address if it is not trusted so it cannot be used to build the forward headers + ServerWebExchange sanitizedExchange = exchange.mutate().request( + new ServerHttpRequestDecorator(request) { + @Override + public InetSocketAddress getRemoteAddress() { + return null; + } + } + ).build(); + log.debug("Remote address not trusted. Trusted proxies pattern: {}, remote address: {}", trustedProxiesRegex, remoteAddress); + return super.filter(removeXForwardHttpHeaders(input), sanitizedExchange); + } + } + return super.filter(input, exchange); + } + + private HttpHeaders removeXForwardHttpHeaders(HttpHeaders input) { + HttpHeaders h = new HttpHeaders(); + input.forEach((header, values) -> { + if (!isXForwardedHeader(header)) { + h.put(header, values); + } + }); + return h; + } + + private boolean isXForwardedHeader(String header) { + return header.equalsIgnoreCase(X_FORWARDED_FOR_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_HOST_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_PORT_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_PROTO_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_PREFIX_HEADER) || + header.equalsIgnoreCase(FORWARDED_HEADER); + } + + private boolean hasXForwardedHeader(HttpHeaders headers) { + return headers.keySet().stream() + .anyMatch(this::isXForwardedHeader); + } +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java index e47dc495a1..5bc665267d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java @@ -14,7 +14,6 @@ import lombok.extern.slf4j.Slf4j; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.springframework.stereotype.Service; -import org.zowe.apiml.gateway.config.ConnectionsConfig; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.security.HttpsConfig; @@ -36,7 +35,7 @@ public class CertificateChainService { private static final ApimlLogger apimlLog = ApimlLogger.of(CertificateChainService.class, YamlMessageServiceInstance.getInstance()); Certificate[] certificates; - private final ConnectionsConfig connectionsConfig; + private final HttpsConfig config; public String getCertificatesInPEMFormat() { StringWriter stringWriter = new StringWriter(); @@ -56,7 +55,6 @@ public String getCertificatesInPEMFormat() { @PostConstruct void loadCertChain() { - HttpsConfig config = connectionsConfig.factory().getConfig(); try { certificates = SecurityUtils.loadCertificateChain(config); } catch (Exception e) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java index ea6254107e..6996226c79 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java @@ -10,20 +10,67 @@ package org.zowe.apiml.gateway.acceptance.common; +import io.restassured.config.RestAssuredConfig; +import io.restassured.config.SSLConfig; +import lombok.SneakyThrows; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.ssl.PrivateKeyDetails; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.TrustStrategy; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.ResourceUtils; import org.zowe.apiml.gateway.ApplicationRegistry; import org.zowe.apiml.gateway.MockService; +import javax.net.ssl.SSLContext; +import java.net.Socket; +import java.security.cert.X509Certificate; +import java.util.Map; + @MicroservicesAcceptanceTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class AcceptanceTestWithMockServices extends AcceptanceTestWithBasePath { + public RestAssuredConfig apimlCert; + public RestAssuredConfig clientCert; + + @Value("${test.proxyAddress}") + public String proxyAddress; + + public String additionalGatewayAddress = "7.7.7.7"; + + public String clientAddress = "11.11.11.11"; + + @Value("${server.ssl.keyStore}") + private String apimlKeyStorePath; + + @Value("${server.ssl.keyStorePassword}") + private char[] apimlKeyStorePassword; + + @Value("${server.ssl.keyPassword:}") + private char[] apimlKeyPassword; + + @Value("${server.ssl.clientKeyStore:}") + private String clientKeyStorePath; + + @Value("${server.ssl.clientKeyStorePassword}") + private char[] clientKeyStorePassword; + + @Value("${server.ssl.keyPassword}") + private char[] clientKeyPassword; + + @Value("${server.ssl.clientCN}") + private String clientCN; + @Autowired private ApplicationEventPublisher applicationEventPublisher; @@ -40,7 +87,27 @@ void checkAssertionErrorsOnMockServices() { MockService.checkAssertionErrors(); } - protected void updateRoutingRules() { + @BeforeAll + @SneakyThrows + void init() { + TrustStrategy trustStrategy = (X509Certificate[] chain, String authType) -> true; + X509HostnameVerifier hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; + + SSLContext apimlSSLContext = SSLContextBuilder.create() + .loadKeyMaterial(ResourceUtils.getFile(apimlKeyStorePath), apimlKeyStorePassword, apimlKeyPassword) + .loadTrustMaterial(null, trustStrategy).build(); + apimlCert = RestAssuredConfig.newConfig() + .sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(apimlSSLContext, hostnameVerifier))); + + SSLContext sslContext = SSLContextBuilder.create() + .loadKeyMaterial(ResourceUtils.getFile(clientKeyStorePath), clientKeyStorePassword, clientKeyPassword, + (Map aliases, Socket socket) -> clientCN) + .loadTrustMaterial(null, trustStrategy).build(); + clientCert = RestAssuredConfig.newConfig() + .sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(sslContext, hostnameVerifier))); + } + + protected void updateRoutingRules() { applicationEventPublisher.publishEvent(new RefreshRoutesEvent("List of services changed")); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/MutateRemoteAddressFilter.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/MutateRemoteAddressFilter.java new file mode 100644 index 0000000000..fcdb9dc9b5 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/MutateRemoteAddressFilter.java @@ -0,0 +1,54 @@ +/* + * 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.acceptance.xForwardHeaders; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicReference; + +@Configuration +@Profile("forward-headers-proxy-test") +public class MutateRemoteAddressFilter { + + public AtomicReference proxyAddressReference; + private final String originalProxyAddressProperty; + + public MutateRemoteAddressFilter(@Value("${test.proxyAddress}") String value) { + this.originalProxyAddressProperty = value; + this.proxyAddressReference = new AtomicReference<>(originalProxyAddressProperty); + } + + // A helper filter for tests only that will replace the remote address host in the request so it will appear to come from a remote proxy. + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + WebFilter mutateWebExchangeAddress() { + return (exchange, chain) -> { + ServerWebExchange exchangeFromProxy = exchange.mutate().request( + exchange.getRequest().mutate() + .remoteAddress(new InetSocketAddress(proxyAddressReference.get(), 0)) + .build() + ).build(); + return chain.filter(exchangeFromProxy); + }; + } + + public void reset() { + this.proxyAddressReference = new AtomicReference<>(originalProxyAddressProperty); + } +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersProxyTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersProxyTest.java new file mode 100644 index 0000000000..f4f808e5c3 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersProxyTest.java @@ -0,0 +1,149 @@ +/* + * 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.acceptance.xForwardHeaders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.gateway.MockService; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; +import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; +import org.zowe.apiml.gateway.filters.proxyheaders.AdditionalRegistrationGatewayRegistry; +import org.zowe.apiml.gateway.filters.proxyheaders.X509AndGwAwareXForwardedHeadersFilter; + +import java.io.IOException; +import java.util.Collections; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.Matchers.is; +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; + +@MicroservicesAcceptanceTest +@TestPropertySource(properties = { + "apiml.security.forwardHeader.trustedProxies=" +}) +@ActiveProfiles("forward-headers-proxy-test") +class XForwardedHeadersProxyTest extends AcceptanceTestWithMockServices { + + @Autowired + public AdditionalRegistrationGatewayRegistry additionalGatewayRegistry; + + @Autowired + public MutateRemoteAddressFilter mutateRemoteAddressFilter; + + @BeforeEach + void initMockService() throws IOException { + mockService("untrusted-proxies") + .scope(MockService.Scope.CLASS) + .addEndpoint("/untrusted-proxies/xForwardedHeadersCreated") + .assertion(he -> assertEquals(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER), clientAddress)) + .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_HOST_HEADER))) + .responseCode(SC_OK) + .and() + .addEndpoint("/untrusted-proxies/xForwardedHeadersForwarded") + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER).contains("/test"))) + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER).contains(proxyAddress))) + .responseCode(SC_OK) + .and() + .addEndpoint("/untrusted-proxies/xForwardedHeadersForwardedFromAdditionalGateway") + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER).contains("/test"))) + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER).contains(additionalGatewayAddress))) + .responseCode(SC_OK) + .and() + .addEndpoint("/untrusted-proxies/noXForwardedHeadersForwarded") + // All request headers are stripped, and the untrusted proxy is not present in X-forwarded-for + // Note: X_FORWARDED_PREFIX_HEADER is processed differently than in the zuul gateway + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER))) + .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_HOST_HEADER))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.FORWARDED_HEADER))) + .responseCode(SC_OK) + .and() + .start(); + } + + @Test + void whenNoXForwardHeadersInRequest_thenXForwardHeadersCreated() { + mutateRemoteAddressFilter.proxyAddressReference.set(clientAddress); + given() + .log().all() + .when() + .get(basePath + "/untrusted-proxies/api/v1/xForwardedHeadersCreated") + .then() + .statusCode(is(SC_OK)); + mutateRemoteAddressFilter.reset(); + } + + @Test + void whenXForwardHeadersInRequest_thenNoXForwardHeadersForwarded() { + given() + .log().all() + .header("x-Forwarded-for", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .header("forwarded", "for=1.1.1.1;prefix=/test") + .when() + .get(basePath + "/untrusted-proxies/api/v1/noXForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequestFromGW_thenXForwardHeadersForwarded() { + given() + .config(apimlCert) + .log().all() + .header("x-forwarded-For", "1.1.1.1") + .header("X-forwarded-Prefix", "/test") + .when() + .get(basePath + "/untrusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequestFromAdditionalGateway_thenXForwardHeadersForwarded() { + mutateRemoteAddressFilter.proxyAddressReference.set(additionalGatewayAddress); + additionalGatewayRegistry.getAdditionalGatewayIpAddressesReference() + .set(Collections.singleton(additionalGatewayAddress)); + + given() + .log().all() + .header("x-forwarded-For", "1.1.1.1") + .header("X-forwarded-Prefix", "/test") + .when() + .get(basePath + "/untrusted-proxies/api/v1/xForwardedHeadersForwardedFromAdditionalGateway") + .then() + .statusCode(is(SC_OK)); + + mutateRemoteAddressFilter.reset(); + additionalGatewayRegistry.getAdditionalGatewayIpAddressesReference().set(Collections.emptySet()); + } + + @Test + void whenXForwardHeadersInRequestWithClientCert_thenNoXForwardHeadersForwarded() { + given() + .config(clientCert) + .log().all() + .header("x-Forwarded-for", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .header("forwarded", "for=1.1.1.1;prefix=/test") + .when() + .get(basePath + "/untrusted-proxies/api/v1/noXForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersTrustedProxyTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersTrustedProxyTest.java new file mode 100644 index 0000000000..14d3b5b326 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersTrustedProxyTest.java @@ -0,0 +1,137 @@ +/* + * 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.acceptance.xForwardHeaders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.gateway.MockService; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; +import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; +import org.zowe.apiml.gateway.filters.proxyheaders.AdditionalRegistrationGatewayRegistry; +import org.zowe.apiml.gateway.filters.proxyheaders.X509AndGwAwareXForwardedHeadersFilter; + +import java.util.Collections; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicroservicesAcceptanceTest +@TestPropertySource(properties = { + "apiml.security.forwardHeader.trustedProxies=${test.trustedProxiesPattern}" +}) +@ActiveProfiles("forward-headers-proxy-test") +class XForwardedHeadersTrustedProxyTest extends AcceptanceTestWithMockServices { + + @Autowired + AdditionalRegistrationGatewayRegistry additionalGatewayRegistry; + @Autowired + MutateRemoteAddressFilter mutateRemoteAddressFilter; + + @BeforeEach + void initMockService() { + mockService("trusted-proxies") + .scope(MockService.Scope.CLASS) + .addEndpoint("/trusted-proxies/xForwardedHeadersCreated") + .assertion(he -> assertEquals(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER), clientAddress)) + .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_HOST_HEADER))) + .responseCode(SC_OK) + .and() + .addEndpoint("/trusted-proxies/xForwardedHeadersForwarded") + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER).contains("/test"))) + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER).contains(proxyAddress))) + .responseCode(SC_OK) + .and() + .addEndpoint("/trusted-proxies/xForwardedHeadersForwardedFromAdditionalGateway") + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER).contains("/test"))) + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509AndGwAwareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER).contains(additionalGatewayAddress))) + .responseCode(SC_OK) + .and() + .start(); + } + + + @Test + void whenNoXForwardHeadersInRequest_thenXForwardHeadersCreated() { + mutateRemoteAddressFilter.proxyAddressReference.set(clientAddress); + given() + .log().all() + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersCreated") + .then() + .statusCode(is(SC_OK)); + mutateRemoteAddressFilter.reset(); + } + + @Test + void whenXForwardHeadersInRequest_thenXForwardedHeadersForwarded() { + given() + .log().all() + .header("X-forwarded-For", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequest_fromAdditionalGateway_thenXForwardedHeadersForwarded() { + mutateRemoteAddressFilter.proxyAddressReference.set(additionalGatewayAddress); + additionalGatewayRegistry.getAdditionalGatewayIpAddressesReference() + .set(Collections.singleton(additionalGatewayAddress)); + + given() + .log().all() + .header("X-forwarded-For", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersForwardedFromAdditionalGateway") + .then() + .statusCode(is(SC_OK)); + + mutateRemoteAddressFilter.reset(); + additionalGatewayRegistry.getAdditionalGatewayIpAddressesReference().set(Collections.emptySet()); + } + + @Test + void whenXForwardHeadersInRequestFromGW_thenXForwardedHeadersForwarded() { + given() + .config(apimlCert) + .log().all() + .header("x-forwarded-For", "1.1.1.1") + .header("X-forwarded-Prefix", "/test") + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequestWithClientCert_thenXForwardedHeadersForwarded() { + given() + .config(clientCert) + .log().all() + .header("x-Forwarded-for", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } +} + diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java index 8bc5626dcb..cfc22c4d00 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java @@ -31,18 +31,23 @@ import org.springframework.context.ApplicationContext; import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.config.AdditionalRegistration; +import org.zowe.apiml.product.web.HttpConfig; import org.zowe.apiml.security.HttpsFactory; import java.util.AbstractMap; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Optional; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.springframework.cloud.netflix.eureka.EurekaClientConfigBean.DEFAULT_ZONE; @ExtendWith(MockitoExtension.class) @@ -56,11 +61,13 @@ public class AdditionalRegistrationTest { @Mock private EurekaFactory eurekaFactory; @Mock - ApplicationContext context; + private ApplicationContext context; + @Mock + private HttpConfig config; @BeforeEach void setUp() { - connectionsConfig = new ConnectionsConfig(context); + connectionsConfig = new ConnectionsConfig(context, config); } @ExtendWith(MockitoExtension.class) @@ -87,9 +94,8 @@ class WhenInitializingAdditionalRegistrations { @BeforeEach public void setUp() throws Exception { ReflectionTestUtils.setField(connectionsConfig, "eurekaServerUrl", "https://host:2222"); - ReflectionTestUtils.setField(connectionsConfig, "httpsFactory", httpsFactory); configSpy = Mockito.spy(connectionsConfig); - lenient().doReturn(httpsFactory).when(configSpy).factory(); + lenient().when(config.httpsFactory()).thenReturn(httpsFactory); lenient().when(httpsFactory.getSslContext()).thenReturn(SSLContexts.custom().build()); lenient().when(httpsFactory.getHostnameVerifier()).thenReturn(new NoopHostnameVerifier()); lenient().when(eurekaFactory.createCloudEurekaClient(any(), any(), clientConfigCaptor.capture(), any(), any(), any())).thenReturn(additionalClientOne, additionalClientTwo); @@ -104,7 +110,7 @@ public void setUp() throws Exception { @Test void shouldCreateEurekaClientForAdditionalDiscoveryUrl() { - AdditionalEurekaClientsHolder holder = configSpy.additionalEurekaClientsHolder(manager, clientConfig, singletonList(registration), eurekaFactory, healthCheckHandler); + AdditionalEurekaClientsHolder holder = configSpy.additionalEurekaClientsHolder(manager, clientConfig, singletonList(registration), eurekaFactory, healthCheckHandler, null, Optional.empty()); assertThat(holder.getDiscoveryClients()).hasSize(1); EurekaClientConfigBean eurekaClientConfigBean = clientConfigCaptor.getValue(); @@ -114,7 +120,7 @@ void shouldCreateEurekaClientForAdditionalDiscoveryUrl() { @Test void shouldCreateTwoAdditionalRegistrations() { AdditionalRegistration secondRegistration = AdditionalRegistration.builder().discoveryServiceUrls("https://another-eureka-2").build(); - AdditionalEurekaClientsHolder holder = configSpy.additionalEurekaClientsHolder(manager, clientConfig, asList(registration, secondRegistration), eurekaFactory, healthCheckHandler); + AdditionalEurekaClientsHolder holder = configSpy.additionalEurekaClientsHolder(manager, clientConfig, asList(registration, secondRegistration), eurekaFactory, healthCheckHandler, null, Optional.empty()); assertThat(holder.getDiscoveryClients()).hasSize(2); verify(additionalClientOne).registerHealthCheck(healthCheckHandler); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java index 7ffcb470e9..42feefa4b7 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java @@ -16,7 +16,6 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClientConfig; import io.netty.handler.ssl.util.KeyManagerFactoryWrapper; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -24,11 +23,8 @@ import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.boot.web.server.Ssl; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -37,6 +33,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.WebFilter; import org.zowe.apiml.gateway.GatewayServiceApplication; +import org.zowe.apiml.product.web.HttpConfig; import reactor.netty.http.client.HttpClient; import reactor.netty.tcp.SslProvider; @@ -52,8 +49,23 @@ import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class ConnectionsConfigTest { @@ -96,56 +108,11 @@ void thenCreateIt() { } - @Nested - @SpringBootTest - @ComponentScan(basePackages = "org.zowe.apiml.gateway") - class KeyringFormatAndPasswordUpdate { - - ApplicationContext context; - - ConnectionsConfig noContextConnectionsConfig = new ConnectionsConfig(null); - - @BeforeEach - void setup() { - context = mock(ApplicationContext.class); - ServerProperties properties = new ServerProperties(); - properties.setSsl(new Ssl()); - when(context.getBean(ServerProperties.class)).thenReturn(properties); - } - - @Test - void whenKeyringHasWrongFormatAndMissingPasswords_thenFixIt() { - ReflectionTestUtils.setField(noContextConnectionsConfig, "keyStorePath", "safkeyring:///userId/ringId1"); - ReflectionTestUtils.setField(noContextConnectionsConfig, "trustStorePath", "safkeyring:////userId/ringId2"); - ReflectionTestUtils.setField(noContextConnectionsConfig, "context", context); - noContextConnectionsConfig.updateConfigParameters(); - - assertThat(ReflectionTestUtils.getField(noContextConnectionsConfig, "keyStorePath")).isEqualTo("safkeyring://userId/ringId1"); - assertThat(ReflectionTestUtils.getField(noContextConnectionsConfig, "trustStorePath")).isEqualTo("safkeyring://userId/ringId2"); - assertThat((char[]) ReflectionTestUtils.getField(noContextConnectionsConfig, "keyStorePassword")).isEqualTo("password".toCharArray()); - assertThat((char[]) ReflectionTestUtils.getField(noContextConnectionsConfig, "trustStorePassword")).isEqualTo("password".toCharArray()); - } - - @Test - void whenKeystore_thenDoNothing() { - ReflectionTestUtils.setField(noContextConnectionsConfig, "keyStorePath", "/path1"); - ReflectionTestUtils.setField(noContextConnectionsConfig, "trustStorePath", "/path2"); - ReflectionTestUtils.setField(noContextConnectionsConfig, "context", context); - noContextConnectionsConfig.updateConfigParameters(); - - assertThat(ReflectionTestUtils.getField(noContextConnectionsConfig, "keyStorePath")).isEqualTo("/path1"); - assertThat(ReflectionTestUtils.getField(noContextConnectionsConfig, "trustStorePath")).isEqualTo("/path2"); - assertThat(ReflectionTestUtils.getField(noContextConnectionsConfig, "keyStorePassword")).isNull(); - assertThat(ReflectionTestUtils.getField(noContextConnectionsConfig, "trustStorePassword")).isNull(); - } - - } - @Nested @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = { "management.port=-1" }, - classes = { GatewayServiceApplication.class, ConnectionsConfigTest.SslDetectorConfig.class } + properties = {"management.port=-1"}, + classes = {GatewayServiceApplication.class, ConnectionsConfigTest.SslDetectorConfig.class} ) class ChooseAlias { @@ -191,10 +158,12 @@ class Negative { @Autowired private ConnectionsConfig connectionsConfig; + @MockitoSpyBean + private HttpConfig httpConfig; @Test void whenAliasIsInvalid_thenNoCertificateProvided() { - ReflectionTestUtils.setField(connectionsConfig, "keyAlias", "invalid"); + when(httpConfig.getKeyAlias()).thenReturn("invalid"); var sslContext = connectionsConfig.getSslContext(true); var sslProvider = SslProvider.builder().sslContext(sslContext).build(); @@ -213,9 +182,9 @@ class Wrapper { private static final String CONFIG_ALIAS = "configAlias"; private static final String ALIAS = "alias"; - private static final String[] ALIASES = new String[] { "alias" }; + private static final String[] ALIASES = new String[]{"alias"}; private static final String KEY_TYPE = "keyType"; - private static final String[] KEY_TYPES = new String[] { KEY_TYPE }; + private static final String[] KEY_TYPES = new String[]{KEY_TYPE}; private static final Principal[] ISSUERS = new Principal[0]; private static final Socket SOCKET = mock(Socket.class); private static final X509Certificate[] CERTIFICATES = new X509Certificate[0]; @@ -328,7 +297,7 @@ private EurekaInstanceConfig createConfig() { @Test void givenInvalidUrl_whenCreate_thenThrowAnException() { - var connectionsConfig = new ConnectionsConfig(null); + var connectionsConfig = new ConnectionsConfig(null, null); ReflectionTestUtils.setField(connectionsConfig, "externalUrl", "invalidUrl"); var e = assertThrows(RuntimeException.class, () -> connectionsConfig.create(createConfig())); assertInstanceOf(MalformedURLException.class, e.getCause()); @@ -337,7 +306,7 @@ void givenInvalidUrl_whenCreate_thenThrowAnException() { @Test void givenValidInputs_whenCreate_thenCreateIt() { var config = createConfig(); - var connectionsConfig = new ConnectionsConfig(null); + var connectionsConfig = new ConnectionsConfig(null, null); ReflectionTestUtils.setField(connectionsConfig, "externalUrl", "https://domain:1234/"); InstanceInfo instanceInfo = connectionsConfig.create(config); @@ -357,7 +326,7 @@ void givenMetadataWithUrl_whenCreate_thenUpdateThem() { var config = createConfig(); doReturn(metadata).when(config).getMetadataMap(); - var connectionsConfig = new ConnectionsConfig(null); + var connectionsConfig = new ConnectionsConfig(null, null); ReflectionTestUtils.setField(connectionsConfig, "externalUrl", "https://domain:1234/"); InstanceInfo instanceInfo = connectionsConfig.create(config); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistryTest.java new file mode 100644 index 0000000000..517571407d --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/proxyheaders/AdditionalRegistrationGatewayRegistryTest.java @@ -0,0 +1,194 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.filters.proxyheaders; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.CacheRefreshedEvent; +import com.netflix.discovery.DiscoveryClient; +import com.netflix.discovery.EurekaClientConfig; +import com.netflix.discovery.shared.Application; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.product.constants.CoreService; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.List; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class AdditionalRegistrationGatewayRegistryTest { + + private DiscoveryClient discoveryClientMock; + private AdditionalRegistrationGatewayRegistry gatewayRegistry; + + // Gateway with single IP address + private final String GW1_INSTANCE_ID = "gw1-instance-id"; + private final String GW1_IP_ADDRESS = "2.2.2.2"; + private final String GW1_HOSTNAME = "gw1-hostname"; + + private final InstanceInfo GW1_INSTANCE_INFO = InstanceInfo.Builder.newBuilder() + .setAppName(CoreService.GATEWAY.getServiceId()) + .setInstanceId(GW1_INSTANCE_ID) + .setHostName(GW1_HOSTNAME) + .setIPAddr(GW1_IP_ADDRESS) + .build(); + + private final Application GW1_APPLICATION = + new Application(CoreService.GATEWAY.getServiceId(), List.of(GW1_INSTANCE_INFO)); + + // Gateway resolved to multiple ip addresses + private final String GW2_INSTANCE_ID = "gw2-instance-id"; + private final String GW2_IP_ADDRESS = "3.3.3.3"; + private final String GW2_IP_ADDRESS_FROM_DNS = "4.4.4.4"; + private final String GW2_HOSTNAME = "gw2-hostname"; + + private final InstanceInfo GW2_INSTANCE_INFO = + InstanceInfo.Builder.newBuilder() + .setAppName(CoreService.GATEWAY.getServiceId()) + .setInstanceId(GW2_INSTANCE_ID) + .setHostName(GW2_HOSTNAME) + .setIPAddr(GW2_IP_ADDRESS) + .build(); + + private final Application GW2_APPLICATION = + new Application(CoreService.GATEWAY.getServiceId(), List.of(GW2_INSTANCE_INFO)); + + // Application with both gateways + private Application ALL_GW_APPLICATION = + new Application(CoreService.GATEWAY.getServiceId(), List.of(GW1_INSTANCE_INFO, GW2_INSTANCE_INFO)); + + @BeforeEach + @SneakyThrows + void setup() { + discoveryClientMock = Mockito.mock(DiscoveryClient.class); + gatewayRegistry = new AdditionalRegistrationGatewayRegistry(); + gatewayRegistry.registryExpiration = Duration.ofSeconds(300); + gatewayRegistry.init(); + } + + @Test + void eventListenerRegistered() { + EurekaClientConfig mockEurekaClientConfig = Mockito.mock(EurekaClientConfig.class); + when(discoveryClientMock.getEurekaClientConfig()).thenReturn(mockEurekaClientConfig); + when(mockEurekaClientConfig.getEurekaServerServiceUrls(any())) + .thenReturn(List.of("dummy")); + + gatewayRegistry.registerCacheRefreshEventListener(discoveryClientMock); + + verify(discoveryClientMock, times(1)) + .registerEventListener(any()); + } + + @Test + void whenGateway_thenAddIpToRegistry() { + when(discoveryClientMock.getApplication(CoreService.GATEWAY.getServiceId())).thenReturn(GW1_APPLICATION); + + gatewayRegistry.cacheRefreshEventHandler(new CacheRefreshedEvent(), discoveryClientMock); + + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().size(), is(1)); + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().get(GW1_INSTANCE_ID), is(List.of(GW1_IP_ADDRESS))); + + assertThat(gatewayRegistry.additionalGatewayIpAddressesReference.get().size(), is(1)); + assertTrue(gatewayRegistry.additionalGatewayIpAddressesReference.get().contains(GW1_IP_ADDRESS)); + } + + @Test + void whenGatewayWithDNSResolution_thenAddIpToRegistry() throws UnknownHostException { + when(discoveryClientMock.getApplication(CoreService.GATEWAY.getServiceId())).thenReturn(GW2_APPLICATION); + + InetAddress[] resolvedGw2Hostname = new InetAddress[]{InetAddress.getByName(GW2_IP_ADDRESS_FROM_DNS)}; + InetAddress[] resolvedGw2IpAddress = new InetAddress[]{InetAddress.getByName(GW2_IP_ADDRESS)}; + + try (MockedStatic inetAddressMocked = Mockito.mockStatic(InetAddress.class)) { + // We cannot use .callReaLMethod() as it relies on native call that cannot be used via mock + inetAddressMocked.when(() -> InetAddress.getAllByName(GW2_IP_ADDRESS)).thenReturn(resolvedGw2IpAddress); + inetAddressMocked.when(() -> InetAddress.getAllByName(GW2_HOSTNAME)).thenReturn(resolvedGw2Hostname); + + gatewayRegistry.cacheRefreshEventHandler(new CacheRefreshedEvent(), discoveryClientMock); + } + + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().size(), is(1)); + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().get(GW2_INSTANCE_ID).size(), is(2)); + assertTrue(gatewayRegistry.knownAdditionalGateways.asMap().get(GW2_INSTANCE_ID).containsAll(List.of(GW2_IP_ADDRESS, GW2_IP_ADDRESS_FROM_DNS))); + + assertThat(gatewayRegistry.additionalGatewayIpAddressesReference.get().size(), is(2)); + assertTrue(gatewayRegistry.additionalGatewayIpAddressesReference.get().containsAll(List.of(GW2_IP_ADDRESS, GW2_IP_ADDRESS_FROM_DNS))); + } + + @Test + void whenGatewayWithDNSResolutionFailed_thenAddIpToRegistry() throws UnknownHostException { + when(discoveryClientMock.getApplication(CoreService.GATEWAY.getServiceId())).thenReturn(GW2_APPLICATION); + + InetAddress[] resolvedGw2IpAddress = new InetAddress[]{InetAddress.getByName(GW2_IP_ADDRESS)}; + + try (MockedStatic inetAddressMocked = Mockito.mockStatic(InetAddress.class)) { + // We cannot use .callReaLMethod() as it relies on native call that cannot be used via mock + inetAddressMocked.when(() -> InetAddress.getAllByName(GW2_IP_ADDRESS)).thenReturn(resolvedGw2IpAddress); + inetAddressMocked.when(() -> InetAddress.getAllByName(GW2_HOSTNAME)).thenThrow(new UnknownHostException()); + + gatewayRegistry.cacheRefreshEventHandler(new CacheRefreshedEvent(), discoveryClientMock); + } + + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().size(), is(1)); + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().get(GW2_INSTANCE_ID).size(), is(1)); + assertTrue(gatewayRegistry.knownAdditionalGateways.asMap().get(GW2_INSTANCE_ID).contains(GW2_IP_ADDRESS)); + + assertThat(gatewayRegistry.additionalGatewayIpAddressesReference.get().size(), is(1)); + assertTrue(gatewayRegistry.additionalGatewayIpAddressesReference.get().contains(GW2_IP_ADDRESS)); + } + + @Test + void whenMultipleGateways_thenAddIpToRegistry() { + when(discoveryClientMock.getApplication(CoreService.GATEWAY.getServiceId())).thenReturn(ALL_GW_APPLICATION); + + gatewayRegistry.cacheRefreshEventHandler(new CacheRefreshedEvent(), discoveryClientMock); + + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().size(), is(2)); + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().get(GW1_INSTANCE_ID), is(List.of(GW1_IP_ADDRESS))); + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().get(GW2_INSTANCE_ID), is(List.of(GW2_IP_ADDRESS))); + + assertThat(gatewayRegistry.additionalGatewayIpAddressesReference.get().size(), is(2)); + assertTrue(gatewayRegistry.additionalGatewayIpAddressesReference.get().containsAll(List.of(GW1_IP_ADDRESS, GW2_IP_ADDRESS))); + } + + @Test + void clearGwRegistryCache() { + gatewayRegistry.registryExpiration = Duration.ofSeconds(1); + gatewayRegistry.init(); + + whenGateway_thenAddIpToRegistry(); + assertThat(gatewayRegistry.knownAdditionalGateways.asMap().get(GW1_INSTANCE_ID).size(), is(1)); + + await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> + // We cannot validate on size as the entries may still be in the cache but masked + assertNull(gatewayRegistry.knownAdditionalGateways.asMap().get(GW1_INSTANCE_ID)) + ); + + when(discoveryClientMock.getApplication(CoreService.GATEWAY.getServiceId())).thenReturn(null); + + gatewayRegistry.cacheRefreshEventHandler(new CacheRefreshedEvent(), discoveryClientMock); + assertNull(gatewayRegistry.knownAdditionalGateways.asMap().get(GW1_INSTANCE_ID)); + assertThat(gatewayRegistry.additionalGatewayIpAddressesReference.get().size(), is(0)); + } +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java index 8765e1862f..b816e8ce86 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.gateway.config.ConnectionsConfig; import org.zowe.apiml.security.HttpsConfigError; import org.zowe.apiml.security.SecurityUtils; @@ -27,12 +26,18 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; class CertificateChainServiceTest { private CertificateChainService certificateChainService; - ConnectionsConfig connectionsConfig = new ConnectionsConfig(null); private static final String CERTIFICATE_1 = """ @@ -100,7 +105,7 @@ class GivenValidCertificateChain { void setup() throws CertificateException { certificates[0] = generateCert(CERTIFICATE_1); certificates[1] = generateCert(CERTIFICATE_2); - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(null); ReflectionTestUtils.setField(certificateChainService, "certificates", certificates, Certificate[].class); } @@ -120,7 +125,7 @@ void whenGetCertificates_thenPEMIsProduced() { class GivenNoCertificatesInChain { @BeforeEach void setup() { - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(null); ReflectionTestUtils.setField(certificateChainService, "certificates", new Certificate[0], Certificate[].class); } @@ -139,7 +144,7 @@ void setup() throws CertificateException { certificates[0] = generateCert(CERTIFICATE_1); certificates[1] = mock(Certificate.class); when(certificates[1].getEncoded()).thenReturn("INVALID_CERT_CONTENT".getBytes()); - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(null); ReflectionTestUtils.setField(certificateChainService, "certificates", certificates, Certificate[].class); } @@ -155,7 +160,7 @@ class GivenExceptionDuringChainLoad { @BeforeEach void setup() { - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(null); } @Test diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml index 25ef837242..3152dc7914 100644 --- a/gateway-service/src/test/resources/application.yml +++ b/gateway-service/src/test/resources/application.yml @@ -29,6 +29,16 @@ server: trustStore: ../keystore/localhost/localhost.truststore.p12 trustStorePassword: password trustStoreType: PKCS12 + # Custom properties for tests only + clientKeyStore: ../keystore/client_cert/client-certs.p12 + clientCN: apimtst # case-sensitive + clientKeyStorePassword: password + +#For testing forwarded headers with (un)trusted proxies +test: + proxyAddress: 6.6.6.6 + trustedProxiesPattern: 6\.6\.6\.6 + spring: main: diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 2cf06962fa..d56d1cb155 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -199,6 +199,7 @@ task runAllIntegrationTestsForZoweNonHaTestingOnZos(type: Test) { 'ApiCatalogStandaloneTest', 'SAFProviderTest', 'GatewayProxyTest', + 'GatewayCentralRegistry', 'SafIdTokenTest' ) } @@ -240,6 +241,7 @@ task runAllIntegrationTestsForZoweHaTestingOnZos(type: Test) { 'ApiCatalogStandaloneTest', 'SAFProviderTest', 'GatewayProxyTest', + 'GatewayCentralRegistry', 'SafIdTokenTest', 'ChaoticHATest', 'GraphQLTest' diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java index b0b9a142d3..d05757f40a 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java @@ -27,7 +27,12 @@ import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.util.TestWithStartedInstances; import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; -import org.zowe.apiml.util.config.*; +import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.config.DiscoveryServiceConfiguration; +import org.zowe.apiml.util.config.ServiceConfiguration; +import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.SslContextConfigurer; +import org.zowe.apiml.util.config.TlsConfiguration; import java.net.MalformedURLException; import java.net.URI; @@ -42,7 +47,9 @@ import static io.restassured.RestAssured.with; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.zowe.apiml.util.SecurityUtils.GATEWAY_TOKEN_COOKIE_NAME; @@ -56,7 +63,7 @@ class CentralRegistryTest implements TestWithStartedInstances { public static final String DOMAIN_APIML = "domain-apiml"; public static final String CENTRAL_APIML = "central-apiml"; - static ServiceConfiguration conf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + static ServiceConfiguration conf = ConfigReader.environmentConfiguration().getCentralGatewayServiceConfiguration(); static DiscoveryServiceConfiguration discoveryConf = ConfigReader.environmentConfiguration().getDiscoveryServiceConfiguration(); @BeforeAll diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/XForwardHeadersProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/XForwardHeadersProxyTest.java new file mode 100644 index 0000000000..7537b58472 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/XForwardHeadersProxyTest.java @@ -0,0 +1,147 @@ +/* + * 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.RestAssured; +import io.restassured.specification.RequestSpecification; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.zowe.apiml.util.config.CentralGatewayServiceConfiguration; +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 java.net.InetAddress; +import java.util.Arrays; +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.zowe.apiml.util.SecurityUtils.COOKIE_NAME; +import static org.zowe.apiml.util.SecurityUtils.gatewayToken; +import static org.zowe.apiml.util.requests.Endpoints.REQUEST_INFO_ENDPOINT; + +@Tag("GatewayCentralRegistry") +@Slf4j +class XForwardHeadersProxyTest { + + private static final String HEADER_X_FORWARD_TO = "X-Forward-To"; + private static final String FORWARD_TO_GATEWAY = "domain-apiml"; + + static CentralGatewayServiceConfiguration cgwConf; + static GatewayServiceConfiguration dgwConf; + + static String cgwUrl; + static String dgwUrl; + static String jwt; + + static String cgwIp; + static String localIp; + + @BeforeAll + static void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + SslContext.prepareSslAuthentication(ItSslConfigFactory.integrationTests()); + + cgwConf = ConfigReader.environmentConfiguration().getCentralGatewayServiceConfiguration(); + dgwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + + cgwUrl = String.format("%s://%s:%s%s", cgwConf.getScheme(), cgwConf.getHost(), cgwConf.getPort(), REQUEST_INFO_ENDPOINT); + dgwUrl = String.format("%s://%s:%s%s", dgwConf.getScheme(), dgwConf.getHost(), dgwConf.getPort(), REQUEST_INFO_ENDPOINT); + + jwt = gatewayToken(); + + cgwIp = InetAddress.getByName(cgwConf.getHost()).getHostAddress(); + localIp = InetAddress.getLocalHost().getHostAddress(); + + log.debug("Central GW hostname and IP Address: {}: {}", cgwConf.getHost(), Arrays.toString(InetAddress.getAllByName(cgwConf.getHost()))); + log.debug("Domain GW hostname and IP Address: {}: {}", dgwConf.getHost(), Arrays.toString(InetAddress.getAllByName(dgwConf.getHost()))); + log.debug("Local IP Address: {}", InetAddress.getLocalHost().getHostAddress()); + } + + private static Stream authenticationRequestSpecifications() { + return Stream.of( + Arguments.of(given().config(SslContext.clientCertValid)), + Arguments.of(given().cookie(COOKIE_NAME, jwt)) + ); + } + + @ParameterizedTest + @MethodSource("authenticationRequestSpecifications") + void throughCGW_throughGW_noXForwardHeadersProvided_newXForwardHeadersCreated(RequestSpecification requestSpecs) { + requestSpecs + .header(HEADER_X_FORWARD_TO, FORWARD_TO_GATEWAY) + .when() + .get(cgwUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body("headers.x-forwarded-proto", is("https,https")) + .body("headers.x-forwarded-prefix", emptyOrNullString()) + .body("headers.x-forwarded-port", is(cgwConf.getPort() + "," + dgwConf.getInternalPorts())) + .body("headers.x-forwarded-for", containsString(cgwIp)) + .body("headers.x-forwarded-for", containsString(localIp)) + .body("headers.x-forwarded-host", containsString(cgwConf.getHost())) + .body("headers.x-forwarded-host", containsString(dgwConf.getHost())); + } + + @ParameterizedTest + @MethodSource("authenticationRequestSpecifications") + void fromUntrustedProxy_throughCGW_throughGW_xForwardHeadersProvided_untrustedXForwardHeadersNotForwarded(RequestSpecification requestSpecs) { + requestSpecs + .header(HEADER_X_FORWARD_TO, FORWARD_TO_GATEWAY) + .header("x-forwarded-proto", "http") + .header("x-forwarded-prefix", "/untrusted-proxy") + .header("x-forwarded-port", "666") + .header("x-forwarded-for", "6.6.6.6") + .header("x-forwarded-host", "9.9.9.9") + .when() + .get(cgwUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body("headers.x-forwarded-proto", is("https,https")) + .body("headers.x-forwarded-prefix", emptyOrNullString()) + .body("headers.x-forwarded-port", is(cgwConf.getPort() + "," + dgwConf.getInternalPorts())) + .body("headers.x-forwarded-for", not(containsString("6.6.6.6"))) + .body("headers.x-forwarded-for", is(cgwIp)) + .body("headers.x-forwarded-host", not(containsString("9.9.9.9"))) + .body("headers.x-forwarded-host", containsString(cgwConf.getHost())) + .body("headers.x-forwarded-host", containsString(dgwConf.getHost())); + } + + @ParameterizedTest + @MethodSource("authenticationRequestSpecifications") + void fromUntrustedProxy_throughGW_xForwardHeadersProvided_untrustedXForwardHeadersNotForwarded(RequestSpecification requestSpecs) { + requestSpecs + .header("x-forwarded-proto", "http") + .header("x-forwarded-prefix", "/untrusted-proxy") + .header("x-forwarded-port", "666") + .header("x-forwarded-for", "6.6.6.6") + .header("x-forwarded-host", "9.9.9.9") + .when() + .get(dgwUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body("headers.x-forwarded-proto", is("https")) + .body("headers.x-forwarded-prefix", emptyOrNullString()) + .body("headers.x-forwarded-port", is(String.valueOf(dgwConf.getPort()))) + .body("headers.x-forwarded-for", emptyOrNullString()) + .body("headers.x-forwarded-host", not(containsString("9.9.9.9"))) + .body("headers.x-forwarded-host", containsString(dgwConf.getHost())); + } +} From a33fb30dd85bb34a1d7a8484bb0895cee0714ce0 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 25 Jul 2025 00:45:33 +0000 Subject: [PATCH 029/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.2.24'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8ccf7c5c3d..b7971608d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.24-SNAPSHOT +version=3.2.24 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 83b26863c466a54b13dd2c4255bb5be2f53d6197 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 25 Jul 2025 00:45:35 +0000 Subject: [PATCH 030/152] [Gradle Release plugin] Create new version: 'v3.2.25-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b7971608d2..26d0144d8e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.24 +version=3.2.25-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 6fa849c7ca4b78934dcd54fc895b2482c8361736 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 25 Jul 2025 00:45:36 +0000 Subject: [PATCH 031/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index bc73564653..c025b36394 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,4 +8,4 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.2.24-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.2.25-SNAPSHOT From eaad4b96e3d4954ae7b815a0496aebcd2d584b93 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 25 Jul 2025 14:58:51 +0200 Subject: [PATCH 032/152] fix: Use Gateway's address space suffix in modulith (#4237) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- apiml-package/src/main/resources/bin/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 831637f765..e329101a5c 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -314,7 +314,7 @@ if [ -n "${ZWE_java_home}" ]; then JAVA_BIN_DIR=${ZWE_java_home}/bin/ fi -APIML_CODE=AL +APIML_CODE=AG _BPXK_AUTOCVT=OFF _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Xms${ZWE_configs_heap_init:-${ZWE_components_gateway_heap_init:-32}}m -Xmx${ZWE_configs_heap_max:-${ZWE_components_gateway_heap_max:-512}}m \ From f48ceca2a48e2c76f2f7ba2ef1df889a47fcd090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= <58428711+pj892031@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:39:56 +0200 Subject: [PATCH 033/152] refactor: Including catalog as a part of Modulith module apiml (#4189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Pablo Carle Signed-off-by: ac892247 Co-authored-by: Pablo Carle Co-authored-by: Pablo Carle Co-authored-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 54 +- .gitignore | 2 + api-catalog-services/README.md | 82 --- api-catalog-services/build.gradle | 4 +- .../apicatalog/ApiCatalogApplication.java | 3 +- .../listeners => }/AppReadyListener.java | 9 +- .../ApiTransformationConfig.java | 26 +- .../apiml/apicatalog/config/BeanConfig.java | 13 +- .../apicatalog/config/CachingConfig.java | 37 - .../config/SecurityConfiguration.java | 385 ++++++++++ .../SwaggerConfiguration.java | 5 +- .../config/TomcatConfiguration.java | 34 - .../apiml/apicatalog/config/WebConfig.java | 77 +- .../controllers/api/ApiCatalogController.java | 251 ------- ...cController.java => ApiDocController.java} | 118 +++- .../controllers/api/ImageController.java | 27 +- .../controllers/api/MockController.java | 45 -- .../controllers/api/OidcController.java | 61 ++ .../controllers/api/ServicesController.java | 241 +++++++ .../api/StaticAPIRefreshController.java | 62 ++ .../api}/StaticDefinitionController.java | 53 +- .../controllers/api/TokenController.java | 114 +++ .../ApiCatalogControllerExceptionHandler.java | 28 +- ...talogApiDocControllerExceptionHandler.java | 40 +- .../CustomErrorStatusHandlingBean.java | 8 +- .../handlers/DefaultExceptionHandler.java | 55 ++ .../handlers/NotFoundErrorController.java | 5 +- ...cAPIRefreshControllerExceptionHandler.java | 24 +- ...cDefinitionControllerExceptionHandler.java | 22 +- .../ApiDiffNotAvailableException.java | 2 +- .../ApiDocNotFoundException.java | 7 +- .../ApiDocTransformationException.java | 2 +- .../ApiVersionNotFoundException.java | 6 +- ...=> ContainerStatusRetrievalException.java} | 7 +- .../ServiceNotFoundException.java | 2 +- .../health/ApiCatalogHealthIndicator.java | 5 +- .../EurekaServiceInstanceRequest.java | 25 - .../instance/InstanceInitializeService.java | 157 ----- .../instance/InstanceRefreshService.java | 242 ------- .../instance/InstanceRetrievalService.java | 279 -------- .../cached => }/model/ApiDocInfo.java | 11 +- .../apicatalog/model/SemanticVersion.java | 2 + .../{security => oidc}/OidcUtils.java | 7 +- .../ApiCatalogLogoutSuccessHandler.java | 57 +- .../security/SecurityConfiguration.java | 305 -------- .../services/cached/CachedApiDocService.java | 236 ------- .../cached/CachedProductFamilyService.java | 484 ------------- .../cached/CachedServicesService.java | 74 -- .../services/cached/model/ApiDocCacheKey.java | 21 - .../status/APIServiceStatusService.java | 183 ----- .../status/OpenApiCompareProducer.java | 22 - .../model/ContainerStatusChangeEvent.java | 42 -- .../status/event/model/STATUS_EVENT_TYPE.java | 17 - .../status/event/model/StatusChangeEvent.java | 31 - .../listeners/GatewayLookupEventListener.java | 54 -- .../services/status/model/APIServiceInfo.java | 19 - .../status/model/APIServiceInstances.java | 20 - .../standalone/ApiDocTransformForMock.java | 47 -- .../apicatalog/standalone/ExampleService.java | 227 ------ .../StandaloneAPIDocRetrievalService.java | 52 -- .../standalone/StandaloneInitializer.java | 52 -- .../standalone/StandaloneLoaderService.java | 162 ----- .../standalone/StandaloneSecurityConfig.java | 50 -- .../DiscoveryConfigProperties.java | 2 +- .../staticapi/StaticAPIRefreshController.java | 35 - .../staticapi/StaticAPIService.java | 1 - .../staticapi/StaticDefinitionGenerator.java | 4 +- .../swagger/ApiDocRetrievalServiceLocal.java | 135 ++++ .../swagger/ApiDocRetrievalServiceRest.java | 105 +++ .../ApiDocService.java} | 505 ++++++------- .../apicatalog/swagger/ContainerService.java | 370 ++++++++++ .../swagger/SubstituteSwaggerGenerator.java | 14 +- .../swagger/TransformApiDocService.java | 7 +- .../swagger/api/AbstractApiDocService.java | 19 +- .../swagger/api/ApiDocV2Service.java | 33 +- .../swagger/api/ApiDocV3Service.java | 34 +- ...itional-spring-configuration-metadata.json | 15 - .../src/main/resources/application.yml | 31 +- ...talogControllerContainerRetrievalTest.java | 50 -- ...inerRetrievalTestContextConfiguration.java | 48 -- .../api/ApiCatalogControllerTests.java | 345 --------- .../ApiDocControllerApiDocNotFoundTest.java | 62 ++ .../ApiDocControllerServiceNotFoundTest.java | 59 ++ .../controllers/api/ApiDocControllerTest.java | 136 ++++ ...logApiDocControllerApiDocNotFoundTest.java | 47 -- ...piDocNotFoundTestContextConfiguration.java | 52 -- ...ogApiDocControllerServiceNotFoundTest.java | 50 -- ...rviceNotFoundTestContextConfiguration.java | 48 -- .../api/CatalogApiDocControllerTest.java | 92 --- .../controllers/api/ImageControllerTest.java | 73 +- .../controllers/api/MockControllerTest.java | 89 --- .../controllers/api/OidcControllerTest.java | 91 +++ ...vicesControllerContainerRetrievalTest.java | 62 ++ .../api/ServicesControllerTests.java | 372 ++++++++++ .../api/StaticAPIRefreshControllerTest.java | 97 +++ .../api/StaticDefinitionControllerTest.java | 190 +++++ .../controllers/api/TokenControllerTest.java | 177 +++++ .../ApiDiffNotAvailableExceptionTest.java | 7 +- .../functional/ApiCatalogFunctionalTest.java | 1 + .../ApiCatalogProtectedEndpointTest.java | 2 + .../functional/AttlsConfigTest.java | 36 +- .../InstanceInitializeServiceTest.java | 231 ------ .../instance/InstanceRefreshServiceTest.java | 215 ------ .../InstanceRetrievalServiceTest.java | 316 --------- .../InstanceServicesContextConfiguration.java | 21 - .../apicatalog/model/SemanticVersionTest.java | 2 +- .../ApiCatalogLogoutSuccessHandlerTest.java | 54 +- .../apicatalog/services/ApiDocKeyTest.java | 38 - .../cached/CachedApiDocServiceTest.java | 266 ------- .../CachedProductFamilyServiceTest.java | 562 --------------- .../cached/CachedServicesServiceTest.java | 70 -- .../status/ApiServiceStatusServiceTest.java | 209 ------ .../status/LocalApiDocServiceTest.java | 504 ------------- .../GatewayLookupEventListenerTest.java | 31 - .../ApiDocTransformForMockTest.java | 39 - .../standalone/ExampleServiceTest.java | 189 ----- .../StandaloneAPIDocRetrievalServiceTest.java | 74 -- .../standalone/StandaloneInitializerTest.java | 92 --- .../StandaloneLoaderServiceTest.java | 188 ----- .../StaticAPIRefreshControllerTest.java | 93 --- .../staticapi/StaticAPIServiceTest.java | 1 - .../StaticApiContextConfiguration.java | 55 -- .../StaticDefinitionControllerTest.java | 149 ---- .../ApiDocRetrievalServiceLocalTest.java | 138 ++++ .../apicatalog/swagger/ApiDocServiceTest.java | 580 +++++++++++++++ .../ApiDocTransformationExceptionTest.java | 1 + .../swagger/ContainerServiceTest.java | 276 ++++++++ .../swagger/SecuritySchemeSerializerTest.java | 2 +- .../SubstituteSwaggerGeneratorTest.java | 11 +- .../swagger/TransformApiDocServiceTest.java | 10 +- .../api/AbstractApiDocServiceTest.java | 10 +- .../swagger/api/ApiDocV2ServiceTest.java | 62 +- .../swagger/api/ApiDocV3ServiceTest.java | 63 +- .../api/ApiTransformationConfigTest.java | 43 +- .../api/{dummy => }/DummyApiDocService.java | 15 +- .../apicatalog/util/ApplicationsWrapper.java | 29 - .../util/ContainerServiceMockUtil.java | 133 ---- .../util/ContainerServiceState.java | 27 - .../apicatalog/util/ServicesBuilder.java | 54 +- .../src/test/resources/application.yml | 30 +- .../test/resources/localhost_truststore.jks | Bin 1109 -> 0 bytes ...ervice1-instance1_zowe v1.0.0_default.json | 3 - .../invalid-apiDocName/apiDocs/service2.json | 1 - .../standalone/invalid-app/apps/service2.json | 3 - ...ervice1-instance1_zowe v1.0.0_default.json | 143 ---- ...ervice1-instance2_zowe v1.0.0_default.json | 143 ---- .../apiDocs/service2_zowe v1.0.0_default.json | 143 ---- .../apiDocs/service2_zowe v2.0.0.json | 143 ---- .../standalone/services/apps/service1.json | 85 --- .../standalone/services/apps/service2.json | 51 -- .../frontend/cypress.modulith.config.js | 2 +- .../cypress/e2e/dashboard/dashboard.cy.js | 2 +- .../cypress/e2e/detail-page/detail-page.cy.js | 4 +- .../multiple-gateway-services.cy.js | 4 +- .../detail-page/service-version-compare.cy.js | 2 +- .../detail-page/service-version-switch.cy.js | 2 +- .../swagger-try-out-and-code-snippets.cy.js | 2 +- .../cypress/e2e/graphql/graphql-apiml.cy.js | 4 +- .../cypress/e2e/login/login-bad.cy.js | 2 +- .../frontend/cypress/e2e/login/login-ok.cy.js | 2 +- .../mocked-backend/assets/containers.json | 2 +- api-catalog-ui/frontend/package.json | 2 +- .../src/components/Swagger/SwaggerUIApiml.jsx | 2 +- .../frontend/src/error-messages.json | 4 + .../frontend/src/services/user.service.jsx | 2 +- .../routing/transform/TransformService.java | 22 +- .../transform/TransformServiceTest.java | 2 +- apiml-package/src/main/resources/bin/start.sh | 10 +- apiml-security-common/build.gradle | 1 + .../error/AbstractExceptionHandler.java | 2 +- .../common/error/AuthExceptionHandler.java | 114 ++- .../error/ResourceAccessExceptionHandler.java | 6 +- apiml/.gitignore | 9 + apiml/build.gradle | 1 + ...achingServiceEurekaInstanceConfigBean.java | 17 +- .../CatalogEurekaInstanceConfigBean.java | 31 + .../GatewayEurekaInstanceConfigBean.java | 1 + .../org/zowe/apiml/GatewaySecurityApi.java | 76 ++ .../java/org/zowe/apiml/ModulithConfig.java | 39 +- .../controller/ApimlExceptionHandler.java | 2 +- .../ReactiveAuthenticationController.java | 13 +- .../org/zowe/apiml/filter/LogoutHandler.java | 7 + .../java/org/zowe/apiml/util/HttpUtils.java | 14 +- apiml/src/main/resources/application.yml | 40 +- .../zowe/apiml/GatewaySecurityApiTest.java | 180 +++++ .../zowe/apiml/filter/LogoutHandlerTest.java | 35 +- .../org/zowe/apiml/util/HttpUtilsTest.java | 14 + common-service-core/build.gradle | 2 + .../org/zowe/apiml/security/HttpsFactory.java | 11 +- .../java/org/zowe/apiml/util/EurekaUtils.java | 54 +- .../org/zowe/apiml/util/EurekaUtilsTest.java | 71 +- .../api-catalog-services-standalone.yml | 41 -- config/docker/api-defs/staticclient.yml | 2 +- ...ce1-instance1_org.zowe v1.0.0_default.json | 667 ------------------ ...ce1-instance2_org.zowe v1.0.0_default.json | 667 ------------------ .../service2_org.zowe v1.0.0_default.json | 667 ------------------ .../apiDocs/service2_org.zowe v2.0.0.json | 667 ------------------ ...ce3_instance1_org.zowe v1.0.0_default.json | 0 .../apps/service1.json | 83 --- .../apps/service2.json | 50 -- .../apps/service3.json | 0 config/local/api-catalog-service.yml | 4 + .../local/api-catalog-services-standalone.yml | 41 -- config/local/api-defs-http/staticclient.yml | 2 +- config/local/api-defs/staticclient.yml | 2 +- ...ce1-instance1_org.zowe v1.0.0_default.json | 667 ------------------ ...ce1-instance2_org.zowe v1.0.0_default.json | 667 ------------------ .../service2_org.zowe v1.0.0_default.json | 667 ------------------ .../apiDocs/service2_org.zowe v2.0.0.json | 667 ------------------ .../apps/service1.json | 85 --- .../apps/service2.json | 51 -- gateway-service/build.gradle | 5 +- .../apiml/gateway/config/SwaggerConfig.java | 2 +- .../GatewayHomepageController.java | 10 +- .../filters/PageRedirectionFilterFactory.java | 183 +++-- .../gateway/filters/X509FilterFactory.java | 4 +- .../apiml/gateway/service/RouteLocator.java | 13 +- .../acceptance/ForwardedProxyHeadersTest.java | 4 +- .../caching/LoadBalancerCacheTest.java | 9 +- ...bidEncodedCharactersFilterFactoryTest.java | 20 +- .../filters/InMemoryRateLimiterTest.java | 83 ++- .../gateway/filters/KeyResolverTest.java | 29 +- .../PageRedirectionFilterFactoryTest.java | 31 +- .../filters/X509FilterFactoryTest.java | 45 +- .../gateway/service/RouteLocatorTest.java | 21 +- .../x509/ClientCertFilterFactoryTest.java | 11 +- .../ApiCatalogAuthenticationTest.java | 65 +- .../ApiCatalogEndpointIntegrationTest.java | 14 +- .../ApiCatalogLoginIntegrationTest.java | 178 +++-- .../apicatalog/ApiCatalogStandaloneTest.java | 127 ---- .../ha/ApiCatalogMultipleInstancesTest.java | 17 + .../impl/ApiMediationLayerStartupChecker.java | 44 +- .../apiml/util/http/HttpRequestUtils.java | 8 +- .../util/requests/ApiCatalogRequests.java | 12 +- ...nment-configuration-docker-modulith-ha.yml | 4 +- ...ironment-configuration-docker-modulith.yml | 10 +- .../environment-configuration-docker.yml | 6 - .../resources/environment-configuration.yml | 6 - .../client/login/GatewayLoginProvider.java | 9 +- .../client/service/GatewaySecurity.java | 38 + .../service/GatewaySecurityService.java | 19 +- .../client/token/GatewayTokenProvider.java | 15 +- .../custom/CustomErrorStatusHandlingBean.java | 4 + 243 files changed, 6060 insertions(+), 15383 deletions(-) delete mode 100644 api-catalog-services/README.md rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{services/status/listeners => }/AppReadyListener.java (80%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{swagger/api => config}/ApiTransformationConfig.java (71%) delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{swagger => config}/SwaggerConfiguration.java (89%) delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/TomcatConfiguration.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/{CatalogApiDocController.java => ApiDocController.java} (60%) delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/MockController.java create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/OidcController.java create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ServicesController.java create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{staticapi => controllers/api}/StaticDefinitionController.java (51%) create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/TokenController.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/{custom => }/CustomErrorStatusHandlingBean.java (80%) create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/DefaultExceptionHandler.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{staticapi => controllers/handlers}/StaticAPIRefreshControllerExceptionHandler.java (70%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{staticapi => controllers/handlers}/StaticDefinitionControllerExceptionHandler.java (73%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{services/status/model => exceptions}/ApiDiffNotAvailableException.java (92%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{services/status/model => exceptions}/ApiDocNotFoundException.java (86%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{swagger => exceptions}/ApiDocTransformationException.java (91%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{services/status/model => exceptions}/ApiVersionNotFoundException.java (84%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/{ContainerStatusRetrievalThrowable.java => ContainerStatusRetrievalException.java} (66%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{services/status/model => exceptions}/ServiceNotFoundException.java (90%) delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/EurekaServiceInstanceRequest.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{services/cached => }/model/ApiDocInfo.java (88%) rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{security => oidc}/OidcUtils.java (84%) delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/model/ApiDocCacheKey.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIServiceStatusService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/OpenApiCompareProducer.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/ContainerStatusChangeEvent.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/STATUS_EVENT_TYPE.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/StatusChangeEvent.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListener.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInfo.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInstances.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMock.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ExampleService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializer.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderService.java delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneSecurityConfig.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{discovery => staticapi}/DiscoveryConfigProperties.java (95%) delete mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshController.java create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/{services/status/APIDocRetrievalService.java => swagger/ApiDocService.java} (53%) create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java delete mode 100644 api-catalog-services/src/main/resources/META-INF/additional-spring-configuration-metadata.json delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTestContextConfiguration.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerApiDocNotFoundTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerServiceNotFoundTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTestContextConfiguration.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTestContextConfiguration.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/MockControllerTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/OidcControllerTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerContainerRetrievalTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerTests.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticDefinitionControllerTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/TokenControllerTest.java rename api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/{services/status/model => exceptions}/ApiDiffNotAvailableExceptionTest.java (87%) delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceServicesContextConfiguration.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/ApiDocKeyTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/ApiServiceStatusServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListenerTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMockTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ExampleServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializerTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderServiceTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshControllerTest.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticApiContextConfiguration.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionControllerTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocalTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocServiceTest.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ContainerServiceTest.java rename api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/{dummy => }/DummyApiDocService.java (69%) delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApplicationsWrapper.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java delete mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceState.java delete mode 100644 api-catalog-services/src/test/resources/localhost_truststore.jks delete mode 100644 api-catalog-services/src/test/resources/standalone/invalid-apiDoc/apiDocs/service1-instance1_zowe v1.0.0_default.json delete mode 100644 api-catalog-services/src/test/resources/standalone/invalid-apiDocName/apiDocs/service2.json delete mode 100644 api-catalog-services/src/test/resources/standalone/invalid-app/apps/service2.json delete mode 100644 api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance1_zowe v1.0.0_default.json delete mode 100644 api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance2_zowe v1.0.0_default.json delete mode 100644 api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v1.0.0_default.json delete mode 100644 api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v2.0.0.json delete mode 100644 api-catalog-services/src/test/resources/standalone/services/apps/service1.json delete mode 100644 api-catalog-services/src/test/resources/standalone/services/apps/service2.json create mode 100644 apiml/src/main/java/org/zowe/apiml/CatalogEurekaInstanceConfigBean.java create mode 100644 apiml/src/main/java/org/zowe/apiml/GatewaySecurityApi.java create mode 100644 apiml/src/test/java/org/zowe/apiml/GatewaySecurityApiTest.java delete mode 100644 config/docker/api-catalog-services-standalone.yml delete mode 100644 config/docker/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json delete mode 100644 config/docker/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json delete mode 100644 config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json delete mode 100644 config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json delete mode 100644 config/docker/catalog-standalone-defs/apiDocs/service3_instance1_org.zowe v1.0.0_default.json delete mode 100644 config/docker/catalog-standalone-defs/apps/service1.json delete mode 100644 config/docker/catalog-standalone-defs/apps/service2.json delete mode 100644 config/docker/catalog-standalone-defs/apps/service3.json delete mode 100644 config/local/api-catalog-services-standalone.yml delete mode 100644 config/local/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json delete mode 100644 config/local/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json delete mode 100644 config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json delete mode 100644 config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json delete mode 100644 config/local/catalog-standalone-defs/apps/service1.json delete mode 100644 config/local/catalog-standalone-defs/apps/service2.json delete mode 100644 integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogStandaloneTest.java create mode 100644 security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurity.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5c44ebd78d..62fb547316 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -41,6 +41,8 @@ jobs: services: apiml: image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs env: APIML_SECURITY_AUTH_JWT_CUSTOMAUTHHEADER: customJwtHeader APIML_SECURITY_AUTH_PASSTICKET_CUSTOMUSERHEADER: customUserHeader @@ -50,14 +52,6 @@ jobs: SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: api-catalog-services - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka - APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -230,23 +224,6 @@ jobs: APIML_SERVICE_HOSTNAME: apiml-2 logbackService: ZWEAGW2 APIML_HEALTH_PROTECTED: false - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: api-catalog-services - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka - APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 - api-catalog-services-2: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: api-catalog-services-2 - APIML_HEALTH_PROTECTED: false - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml-2:10011/eureka,https://apiml:10011/eureka - APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -1203,13 +1180,6 @@ jobs: timeout-minutes: 15 services: - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka - APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -1398,10 +1368,6 @@ jobs: timeout-minutes: 15 services: - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -1848,7 +1814,7 @@ jobs: - name: Run startup check timeout-minutes: 4 run: > - ./gradlew runStartUpCheck --info --scan -Denvironment.config=-ha -Ddiscoverableclient.instances=1 + ./gradlew runStartUpCheck --info --scan -Denvironment.config=-ha -Ddiscoverableclient.instances=1 -Dgateway.instances=1 - name: Show status when APIML is not ready yet if: failure() shell: bash @@ -1894,14 +1860,6 @@ jobs: timeout-minutes: 30 services: - api-catalog-services: - image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} - volumes: - - /api-defs:/api-defs - env: - APIML_SERVICE_HOSTNAME: api-catalog-services - APIML_SERVICE_DISCOVERYSERVICEURLS: https://gateway-service:10011/eureka - APIML_SERVICE_GATEWAYHOSTNAME: https://gateway-service:10010 discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: @@ -1913,6 +1871,8 @@ jobs: gateway-service: # To avoid updating Oauth provider admin settings image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs env: APIML_DISCOVERY_ALLPEERSURLS: https://gateway-service:10011/eureka APIML_SERVICE_HOSTNAME: gateway-service @@ -1927,8 +1887,11 @@ jobs: APIML_SECURITY_OIDC_COOKIE_SAMESITE: None APIML_HEALTH_PROTECTED: false logbackService: ZWEAGW1 + APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 gateway-service-2: image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs env: APIML_DISCOVERY_ALLPEERSURLS: https://gateway-service-2:10011/eureka APIML_SERVICE_HOSTNAME: gateway-service-2 @@ -1941,6 +1904,7 @@ jobs: APIML_SECURITY_X509_ENABLED: true APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service-2:10010/gateway/certificates + APIML_SERVICE_GATEWAYHOSTNAME: https://apiml:10010 steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 39e7a90d6f..12454af93d 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,5 @@ data/* index/* *.lock +index.* +index-count diff --git a/api-catalog-services/README.md b/api-catalog-services/README.md deleted file mode 100644 index af8ffa2b98..0000000000 --- a/api-catalog-services/README.md +++ /dev/null @@ -1,82 +0,0 @@ - -# API Catalog Services - -- [Standalone mode](#standalone-mode) - - [Configuration](#configuration) - - [Service directory structure](#service-directory-structure) - - [Apps subdirectory](#apps-subdirectory) - - [ApiDocs subdirectory](#apidocs-subdirectory) - -## Standalone mode - -Standalone mode allows displaying, without the need for authentication, services -that are stored on the disk. Standalone API Catalog does not connect to any -other service. - -It requires building the UI module differently. For more information please follow -[the documentation about UI](../api-catalog-ui/frontend/README.md). - -### Configuration - -- `apiml.catalog.standalone.enabled` - specifies whether to enable the standalone mode - Default: false -- `apiml.catalog.standalone.servicesDirectory` - specifies a directory where service definitions are stored - Default: services - -### Service directory structure - -The service directory contains definitions of services that are visible in -the Catalog. - -It consists of the following subdirectories: - - apps - - apiDocs - -#### Apps subdirectory - -`apps` subdirectory contains files in JSON format that describes services. All -JSON files inside the directory are processed. The file name does not have any -specific format. - -The file contains the Eureka Instance descriptor of an API ML conformant -service. It is serialized `com.netflix.discovery.shared.Applications` object. -JSON `application` object contains a list of one or multiple services. - -You can use endpoints in the following link to obtain the file content: - - - -Example: -[service1.json](config/local/catalog-standalone-defs/apps/service1.json) - -#### ApiDocs subdirectory - -`apiDocs` subdirectory contains API documentation in JSON format for services. -All API ML supported formats are available, such as Swagger or openAPI. All JSON -files are processed. - -The file contains supported API documentation for one service and version. - -The file name has the following structure: - -`{serviceId}_{version}_default.json` - -or - -`{serviceId}_{version}.json` - -The `default` suffix is used for the API version that is displayed in -the Catalog as the default one. Make sure that it corresponds to the default -version specified in `apiInfo` section in the service metadata provided in `app` -subdirectory. Each service MUST have exactly one API documentation file with -a default suffix. - -Note: -The service ID and version cannot contain an underscore character. Otherwise, the definition will be not loaded. -You can find more information about apiInfo at - - -Example: -[service2_org.zowe v1.0.0_default.json](config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json) diff --git a/api-catalog-services/build.gradle b/api-catalog-services/build.gradle index 1cd5ecae23..7e817dcc11 100644 --- a/api-catalog-services/build.gradle +++ b/api-catalog-services/build.gradle @@ -69,8 +69,9 @@ dependencies { implementation libs.spring.boot.starter.actuator implementation libs.spring.boot.starter.security implementation libs.spring.boot.starter.web + implementation libs.spring.boot.starter.webflux implementation libs.spring.cloud.starter.eureka.client - implementation libs.spring.doc + implementation libs.spring.doc.webflux.ui implementation libs.spring.retry implementation libs.apache.velocity @@ -89,6 +90,7 @@ dependencies { testImplementation libs.spring.boot.starter.test testImplementation libs.spring.mock.mvc testImplementation(testFixtures(project(":apiml-common"))) + testImplementation libs.reactor.test compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java index 919987a301..1a7b52d364 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java @@ -28,7 +28,8 @@ "org.zowe.apiml.product.compatibility", "org.zowe.apiml.product.security", "org.zowe.apiml.product.web", - "org.zowe.apiml.product.gateway" + "org.zowe.apiml.product.gateway", + "org.zowe.apiml.filter" }) @EnableScheduling @EnableRetry diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/listeners/AppReadyListener.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java similarity index 80% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/listeners/AppReadyListener.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java index 4a30573aa6..f44ad75a3b 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/listeners/AppReadyListener.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java @@ -8,9 +8,8 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.status.listeners; +package org.zowe.apiml.apicatalog; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -20,11 +19,6 @@ * This class fires on ApplicationReadyEvent event during Spring context initialization */ @Component -@ConditionalOnProperty( - value = "apiml.catalog.standalone.enabled", - havingValue = "false", - matchIfMissing = true -) public class AppReadyListener { /** @@ -38,4 +32,5 @@ public void onApplicationEvent(ApplicationReadyEvent event) { new ServiceStartupEventHandler().onServiceStartup("API Catalog Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiTransformationConfig.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/ApiTransformationConfig.java similarity index 71% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiTransformationConfig.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/ApiTransformationConfig.java index 02525deda4..3fcf5b3152 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiTransformationConfig.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/ApiTransformationConfig.java @@ -8,22 +8,26 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.swagger.api; +package org.zowe.apiml.apicatalog.config; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import jakarta.validation.UnexpectedTypeException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.zowe.apiml.apicatalog.swagger.api.AbstractApiDocService; +import org.zowe.apiml.apicatalog.swagger.api.ApiDocV2Service; +import org.zowe.apiml.apicatalog.swagger.api.ApiDocV3Service; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.gateway.GatewayClient; -import jakarta.validation.UnexpectedTypeException; - import java.io.IOException; import java.util.function.Function; @@ -33,6 +37,7 @@ @SuppressWarnings("squid:S1452") public class ApiTransformationConfig { + private final ApplicationInfo applicationInfo; private final GatewayClient gatewayClient; @Bean @@ -43,22 +48,29 @@ public class ApiTransformationConfig { @Bean @Scope(value = "prototype") public AbstractApiDocService abstractApiDocService(String content) { - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()) + JsonFactory jsonFactory; + if (String.valueOf(content).trim().startsWith("{")) { + jsonFactory = new JsonFactory(); + } else { + jsonFactory = new YAMLFactory(); + } + + ObjectMapper mapper = new ObjectMapper(jsonFactory) .setSerializationInclusion(JsonInclude.Include.NON_NULL); try { ObjectNode objectNode = mapper.readValue(content, ObjectNode.class); JsonNode openApiNode = objectNode.get("openapi"); if (openApiNode != null) { - return new ApiDocV3Service(gatewayClient); + return new ApiDocV3Service(applicationInfo, gatewayClient); } else { JsonNode swaggerNode = objectNode.get("swagger"); if (swaggerNode != null) { - return new ApiDocV2Service(gatewayClient); + return new ApiDocV2Service(applicationInfo, gatewayClient); } } } catch (IOException e) { log.debug("Could not convert response body to a Swagger/OpenAPI object.", e); - throw new UnexpectedTypeException("Response is not a Swagger or OpenAPI type object."); + throw new UnexpectedTypeException("Response is not a Swagger or OpenAPI type object.", e); } return null; diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/BeanConfig.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/BeanConfig.java index 5fb9dda53c..b2f8dd94db 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/BeanConfig.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/BeanConfig.java @@ -10,9 +10,12 @@ package org.zowe.apiml.apicatalog.config; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.gateway.GatewayClient; @@ -21,11 +24,12 @@ /** * General configuration of the API Catalog. */ -@Configuration +@Configuration("catalogBeanConfig") public class BeanConfig { @Bean @Primary + @ConditionalOnMissingBean(name = "modulithConfig") public MessageService messageServiceCatalog() { MessageService messageService = YamlMessageServiceInstance.getInstance(); messageService.loadMessages("/security-client-log-messages.yml"); @@ -37,8 +41,15 @@ public MessageService messageServiceCatalog() { } @Bean + @Lazy public TransformService transformService(GatewayClient gatewayClient) { return new TransformService(gatewayClient); } + @Bean + @ConditionalOnMissingBean + public ApplicationInfo applicationInfo() { + return ApplicationInfo.builder().isModulith(false).build(); + } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java deleted file mode 100644 index 3dbc473d8c..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/CachingConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.config; - -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.concurrent.ConcurrentMapCache; -import org.springframework.cache.support.SimpleCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -import java.util.Arrays; - -@Configuration -@EnableCaching -public class CachingConfig { - - @Bean - @Primary - public CacheManager cacheManager() { - final SimpleCacheManager cacheManager = new SimpleCacheManager(); - cacheManager.setCaches(Arrays.asList( - new ConcurrentMapCache("api-doc") - )); - return cacheManager; - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java new file mode 100644 index 0000000000..77b2d8a02a --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java @@ -0,0 +1,385 @@ +/* + * 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.apicatalog.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.server.WebFilter; +import org.zowe.apiml.apicatalog.security.ApiCatalogLogoutSuccessHandler; +import org.zowe.apiml.config.ApplicationInfo; +import org.zowe.apiml.constants.ApimlConstants; +import org.zowe.apiml.message.api.ApiMessageView; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.security.client.EnableApimlAuth; +import org.zowe.apiml.security.client.service.GatewaySecurity; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.config.SafSecurityConfigurationProperties; +import org.zowe.apiml.security.common.login.LoginFilter; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.security.common.util.X509Util; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; +import static org.zowe.apiml.constants.ApimlConstants.HEADER_OIDC_TOKEN; +import static org.zowe.apiml.security.common.token.TokenAuthentication.createAuthenticated; + +/** + * Main configuration class of Spring web security for Api Catalog + * binds authentication managers + * configures ignores for static content + * adds endpoints and secures them + * adds security filters + */ +@Slf4j +@Configuration +@EnableWebFluxSecurity +@RequiredArgsConstructor +@EnableApimlAuth +@EnableReactiveMethodSecurity +@EnableConfigurationProperties(SafSecurityConfigurationProperties.class) +public class SecurityConfiguration { + + private static final String APIDOC_ROUTES = "/apidoc/**"; + private static final String STATIC_REFRESH_ROUTE = "/static-api/refresh"; + private static final String APPLICATION_HEALTH = "/application/health"; + + private final ApplicationInfo applicationInfo; + private final GatewaySecurity gatewaySecurity; + private final AuthConfigurationProperties authConfigurationProperties; + private final MessageService messageService; + private final ObjectMapper objectMapper; + + @Value("${apiml.security.ssl.verifySslCertificatesOfServices:true}") + private boolean verifySslCertificatesOfServices; + + @Value("${apiml.security.ssl.nonStrictVerifySslCertificatesOfServices:false}") + private boolean nonStrictVerifySslCertificatesOfServices; + + private WebFilter basicAuthenticationFilter; + private WebFilter tokenAuthenticationFilter; + private WebFilter oidcAuthenticationFilter; + + @PostConstruct + void initFilters() { + basicAuthenticationFilter = basicAuthenticationFilter(gatewaySecurity); + tokenAuthenticationFilter = tokenAuthenticationFilter(gatewaySecurity, authConfigurationProperties, messageService, objectMapper); + oidcAuthenticationFilter = oidcAuthenticationFilter(gatewaySecurity); + } + + private String[] getFullUrls(String...baseUrl) { + String prefix = applicationInfo.isModulith() ? "/apicatalog/api/v1" : "/apicatalog"; + for (int i = 0; i < baseUrl.length; i++) { + baseUrl[i] = prefix + baseUrl[i]; + } + return baseUrl; + } + + @Bean + @Order(1) + @ConditionalOnMissingBean(name = "modulithConfig") + public SecurityWebFilterChain loginSecurityWebFilterChain(ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint) { + return baseConfiguration(http, serverAuthenticationEntryPoint) + .securityMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, getFullUrls(authConfigurationProperties.getServiceLoginEndpoint()))) + .authorizeExchange(exchange -> exchange + .pathMatchers(HttpMethod.POST, getFullUrls(authConfigurationProperties.getServiceLoginEndpoint())).permitAll() + ) + .build(); + } + + @Bean + @Order(2) + @ConditionalOnMissingBean(name = "modulithConfig") + public SecurityWebFilterChain logoutSecurityWebFilterChain(ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint) { + return baseConfiguration(http, serverAuthenticationEntryPoint) + .securityMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, getFullUrls(authConfigurationProperties.getServiceLogoutEndpoint()))) + .logout(logout -> logout + .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, getFullUrls(authConfigurationProperties.getServiceLogoutEndpoint()))) + .logoutSuccessHandler(new ApiCatalogLogoutSuccessHandler(authConfigurationProperties)) + ) + .build(); + } + + /** + * Filter chain for protecting /apidoc/** endpoints with MF credentials for client certificate. + */ + @Bean + @Order(3) + public SecurityWebFilterChain basicAuthOrTokenOrCertApiDocFilterChain( + ServerHttpSecurity http, + ServerAuthenticationEntryPoint serverAuthenticationEntryPoint + ) { + baseConfiguration( + http.securityMatcher(ServerWebExchangeMatchers.pathMatchers(getFullUrls(APIDOC_ROUTES, STATIC_REFRESH_ROUTE))), + serverAuthenticationEntryPoint, + basicAuthenticationFilter, tokenAuthenticationFilter, oidcAuthenticationFilter + ) + .authorizeExchange(exchange -> exchange.anyExchange().authenticated()); + + if (verifySslCertificatesOfServices || !nonStrictVerifySslCertificatesOfServices) { + http.x509(x509 -> x509 + .principalExtractor(X509Util.x509PrincipalExtractor()) + .authenticationManager(X509Util.x509ReactiveAuthenticationManager()) + ); + } + + return http.build(); + } + + @Bean + @Order(4) + public SecurityWebFilterChain healthEndpointSecurityWebFilterChain( + ServerHttpSecurity http, + @Value("${apiml.health.protected:true}") boolean isHealthEndpointProtected, + ServerAuthenticationEntryPoint serverAuthenticationEntryPoint + ) { + http = baseConfiguration( + http.securityMatcher(ServerWebExchangeMatchers.pathMatchers(getFullUrls(APPLICATION_HEALTH))), + serverAuthenticationEntryPoint, + basicAuthenticationFilter, tokenAuthenticationFilter, oidcAuthenticationFilter + ); + + if (isHealthEndpointProtected) { + http.authorizeExchange(exchange -> exchange + .pathMatchers(getFullUrls(APPLICATION_HEALTH)).authenticated()); + } else { + http.authorizeExchange(exchange -> exchange + .pathMatchers(getFullUrls(APPLICATION_HEALTH)).permitAll()); + } + + return http.build(); + } + + @Bean + @Order(5) + public SecurityWebFilterChain basicAuthOrTokenAllEndpointsFilterChain( + ServerHttpSecurity http, + ServerAuthenticationEntryPoint serverAuthenticationEntryPoint + ) { + return baseConfiguration(http.securityMatcher(ServerWebExchangeMatchers.pathMatchers( + getFullUrls("/static-api/**", "/containers", "/containers/**", "/application/**", "/services/**", APIDOC_ROUTES))), + serverAuthenticationEntryPoint, + basicAuthenticationFilter, tokenAuthenticationFilter, oidcAuthenticationFilter + ) + .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) + .build(); + } + + /** + * Default filter chain to protect all routes with MF credentials. + */ + @Bean + @Order(6) + public SecurityWebFilterChain webSecurityCustomizer( + ServerHttpSecurity http, + ServerAuthenticationEntryPoint serverAuthenticationEntryPoint + ) { + return baseConfiguration(http, serverAuthenticationEntryPoint) + .securityMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, + ArrayUtils.addAll(getFullUrls( + "", + "/", + "/static/**", + "/favicon.ico", + "/v3/api-docs", + "/index.html", + "/application/info", + "/oidc/provider" + ), "/"))) + .authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec.anyExchange().permitAll()) + .build(); + } + + private ServerHttpSecurity baseConfiguration( + ServerHttpSecurity http, + ServerAuthenticationEntryPoint serverAuthenticationEntryPoint, + WebFilter...webFiltersAuthorization + ) { + var antMatcher = new AntPathMatcher(); + + http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) + .formLogin(ServerHttpSecurity.FormLoginSpec::disable) + .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) + + .headers(httpSecurityHeadersConfigurer -> + httpSecurityHeadersConfigurer.hsts(ServerHttpSecurity.HeaderSpec.HstsSpec::disable) + .frameOptions(ServerHttpSecurity.HeaderSpec.FrameOptionsSpec::disable)) + + .exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec + .authenticationEntryPoint((exchange, exception) -> { + String requestedPath = exchange.getRequest().getPath().toString(); + log.debug("Unauthorized access to '{}' endpoint", requestedPath); + + if (Stream.of(getFullUrls( + "/application/**", + APIDOC_ROUTES, + STATIC_REFRESH_ROUTE + )).anyMatch(pattern -> antMatcher.match(pattern, requestedPath)) + ) { + exchange.getResponse().getHeaders().add(HttpHeaders.WWW_AUTHENTICATE, ApimlConstants.BASIC_AUTHENTICATION_PREFIX); + } + + return serverAuthenticationEntryPoint.commence(exchange, exception); + }) + ); + + Stream.of(webFiltersAuthorization).forEach(webFilter -> http.addFilterBefore(webFilter, SecurityWebFiltersOrder.AUTHENTICATION)); + + return http; + } + + WebFilter basicAuthenticationFilter( + GatewaySecurity gatewaySecurity + ) { + return (exchange, chain) -> chain.filter(exchange) + .contextWrite(context -> { + var authorizationHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + return LoginFilter.getCredentialFromAuthorizationHeader(Optional.ofNullable(authorizationHeader)).map(login -> { + try { + return gatewaySecurity.login(login.getUsername(), login.getPassword(), null).map(token -> + ReactiveSecurityContextHolder.withAuthentication( + new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword(), Collections.emptyList()) + ) + ).orElse(context); + } catch (Exception e) { + log.debug("Cannot verify basic auth", e); + return context; + } + }).orElse(context); + }); + } + + WebFilter tokenAuthenticationFilter( + GatewaySecurity gatewaySecurity, + AuthConfigurationProperties authConfigurationProperties, + MessageService messageService, + ObjectMapper mapper + ) { + AuthConfigurationProperties.CookieProperties cp = authConfigurationProperties.getCookieProperties(); + + return (exchange, chain) -> chain.filter(exchange) + .contextWrite(context -> + Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)) + .filter(header -> header.startsWith("Bearer ")) + .map(header -> header.substring("Bearer ".length())) + .map(String::trim) + .or(() -> Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cp.getCookieName())) + .map(HttpCookie::getValue) + ) + .map(token -> { + try { + return Map.entry(token, gatewaySecurity.query(token)); + } catch (TokenNotValidException e) { + throw e; + } catch (Exception e) { + log.debug("Cannot query token: {}", token, e); + return null; + } + }) + .map(pair -> ReactiveSecurityContextHolder.withAuthentication( + createAuthenticated(pair.getValue().getUserId(), pair.getKey(), TokenAuthentication.Type.JWT) + )) + .orElse(context) + ) + // TODO: only to mitigate breaking change, it should be removed and handled by a universal 401 message + .onErrorResume(TokenNotValidException.class, context -> { + try { + ApiMessageView message = messageService.createMessage("org.zowe.apiml.common.unauthorized").mapToView(); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(mapper.writeValueAsBytes(message)); + exchange.getResponse().setRawStatusCode(SC_UNAUTHORIZED); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + return exchange.getResponse().writeWith(Mono.just(buffer)); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot serialize the error message about unauthorized access", e); + } + }); + } + + WebFilter oidcAuthenticationFilter( + GatewaySecurity gatewaySecurity + ) { + return (exchange, chain) -> chain.filter(exchange) + .contextWrite(context -> + Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(HEADER_OIDC_TOKEN)) + .map(token -> { + try { + return Map.entry(token, gatewaySecurity.verifyOidc(token)); + } catch (Exception e) { + log.debug("Cannot verify OIDC token: {}", token, e); + return null; + } + }) + .map(pair -> ReactiveSecurityContextHolder.withAuthentication( + createAuthenticated(pair.getValue().getUserId(), pair.getKey(), TokenAuthentication.Type.OIDC) + )) + .orElse(context) + ); + } + + @Bean + public ReactiveAuthenticationManager reactiveAuthenticationManager() { + return Mono::just; + } + + @Bean + public ServerAuthenticationEntryPoint serverAuthenticationEntryPoint( + MessageService messageService, + ObjectMapper mapper + ) { + return (exchange, authenticationException) -> { + try { + ApiMessageView message = messageService.createMessage("org.zowe.apiml.security.login.invalidCredentials", exchange.getRequest().getPath().toString()).mapToView(); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(mapper.writeValueAsBytes(message)); + exchange.getResponse().setRawStatusCode(SC_UNAUTHORIZED); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + return exchange.getResponse().writeWith(Mono.just(buffer)); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot serialize the error message about invalid credentials", e); + } + }; + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/SwaggerConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SwaggerConfiguration.java similarity index 89% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/SwaggerConfiguration.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SwaggerConfiguration.java index ea4fa925fc..6f42c1d8cb 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/SwaggerConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SwaggerConfiguration.java @@ -8,17 +8,19 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.swagger; +package org.zowe.apiml.apicatalog.config; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration +@ConditionalOnMissingBean(name = "modulithConfig") public class SwaggerConfiguration { @Value("${apiml.service.apiDoc.title}") @@ -43,4 +45,5 @@ public OpenAPI openApi() { .addSecuritySchemes("CookieAuth", new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER).name("apimlAuthenticationToken")) ); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/TomcatConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/TomcatConfiguration.java deleted file mode 100644 index f61b16fca9..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/TomcatConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.config; - -import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -/** - * Configuration of Tomcat for the API Gateway. - */ -@Configuration -public class TomcatConfiguration { - - @Bean - public ServletWebServerFactory servletContainer(List connectorCustomizers) { - TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); - tomcat.setProtocol(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - tomcat.addConnectorCustomizers(connectorCustomizers.toArray(new TomcatConnectorCustomizer[0])); - return tomcat; - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/WebConfig.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/WebConfig.java index feb5d36f2b..9b4c61b529 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/WebConfig.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/WebConfig.java @@ -10,36 +10,81 @@ package org.zowe.apiml.apicatalog.config; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.http.CacheControl; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.reactive.config.ResourceHandlerRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.zowe.apiml.config.ApplicationInfo; +import reactor.core.publisher.Mono; +import java.net.URI; import java.time.Duration; -@Configuration +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.POST; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +@Configuration("catalogWebConfig") @ComponentScan("org.zowe.apiml.product.web") -public class WebConfig implements WebMvcConfigurer { +@RequiredArgsConstructor +@SuppressWarnings("squid:S1192") // using same literals increase the readability +public class WebConfig implements WebFluxConfigurer { + + private final ApplicationInfo applicationInfo; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { + String prefix = applicationInfo.isModulith() ? "/apicatalog/ui/v1" : "/apicatalog"; + registry - .addResourceHandler("/index.html") - .setCacheControl(CacheControl - .noStore() - .cachePrivate() - .mustRevalidate()) - .addResourceLocations("/static/", "classpath:/static/"); + .addResourceHandler(prefix + "/*") + .setCacheControl(CacheControl + .noStore() + .cachePrivate() + .mustRevalidate()) + .addResourceLocations("/static/", "classpath:/static/"); registry - .addResourceHandler("/static/**") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365L))) - .addResourceLocations("classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/", "classpath:/static/static/"); + .addResourceHandler(prefix + "/static/**") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365L))) + .addResourceLocations("classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/", "classpath:/static/static/"); registry - .addResourceHandler("/resources/**") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365L))) - .addResourceLocations("/resources/", "/resources/static/", "/resources/templates/"); + .addResourceHandler(prefix + "/resources/**") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365L))) + .addResourceLocations("/resources/", "/resources/static/", "/resources/templates/"); } + + private Mono redirect(String path) { + return ServerResponse.permanentRedirect(URI.create(path)).build(); + } + + @Bean + @ConditionalOnMissingBean(name = "modulithConfig") + public RouterFunction redirectRouteMicroservice() { + return route(GET("/"), req -> redirect("/apicatalog")) + .and(route(GET("/apicatalog"), req -> redirect("/apicatalog/"))) + .and(route(GET("/apicatalog/"), req -> redirect("/apicatalog/index.html"))); + } + + @Bean + @ConditionalOnBean(name = "modulithConfig") + public RouterFunction redirectRouteModulith() { + return route(GET("/apicatalog/api/v1"), req -> redirect("/apicatalog/api/v1/")) + .and(route(GET("/apicatalog/api/v1/"), req -> redirect("/apicatalog/api/v1/index.html"))) + .and(route(GET("/apicatalog/ui/v1"), req -> redirect("/apicatalog/ui/v1/"))) + .and(route(GET("/apicatalog/ui/v1/"), req -> redirect("/apicatalog/ui/v1/index.html"))) + + .and(route(POST("/apicatalog/api/v1/auth/login"), req -> redirect("/gateway/api/v1/auth/login"))) + .and(route(POST("/apicatalog/api/v1/auth/logout"), req -> redirect("/gateway/api/v1/auth/logout"))) + .and(route(GET("/apicatalog/api/v1/auth/query"), req -> redirect("/gateway/api/v1/auth/query"))); + } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java deleted file mode 100644 index 6333b49116..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalThrowable; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.model.APIService; -import org.zowe.apiml.apicatalog.security.OidcUtils; -import org.zowe.apiml.apicatalog.services.cached.CachedApiDocService; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.StreamSupport; - -/** - * Main API for handling requests from the API Catalog UI, routed through the gateway - */ -@Slf4j -@RestController -@RequestMapping("/") -@Tag(name = "API Catalog") -public class ApiCatalogController { - - private final CachedProductFamilyService cachedProductFamilyService; - private final CachedApiDocService cachedApiDocService; - - @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); - - private AtomicReference> oidcProviderCache = new AtomicReference<>(); - - /** - * Create the controller and autowire in the repository services - * - * @param cachedProductFamilyService cached service for containers - * @param cachedApiDocService Cached state opf containers and services - */ - @Autowired - public ApiCatalogController(CachedProductFamilyService cachedProductFamilyService, - CachedApiDocService cachedApiDocService) { - this.cachedProductFamilyService = cachedProductFamilyService; - this.cachedApiDocService = cachedApiDocService; - } - - @GetMapping(value = "/oidc/provider", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getOidcProvider() { - if (oidcProviderCache.get() == null) { - oidcProviderCache.set(OidcUtils.getOidcProvider()); - } - - return new ResponseEntity<>(oidcProviderCache.get(), oidcProviderCache.get().isEmpty() ? HttpStatus.NO_CONTENT : HttpStatus.OK); - } - - - /** - * Get all containers - * - * @return a list of all containers - */ - @GetMapping(value = "/containers", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = "Lists catalog dashboard tiles", - description = "Returns a list of tiles including status and tile description", - security = { - @SecurityRequirement(name = "BasicAuthorization"), @SecurityRequirement(name = "CookieAuth") - } - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "OK"), - @ApiResponse(responseCode = "204", description = "No service available"), - @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "URI not found"), - @ApiResponse(responseCode = "500", description = "An unexpected condition occurred") - }) - public ResponseEntity> getAllAPIContainers() throws ContainerStatusRetrievalThrowable { - try { - Iterable allContainers = cachedProductFamilyService.getAllContainers(); - List apiContainers = toList(allContainers); - if (apiContainers == null || apiContainers.isEmpty()) { - return new ResponseEntity<>(apiContainers, HttpStatus.NO_CONTENT); - } else { - // for each container, check the status of all it's services so it's overall status can be set here - apiContainers.forEach(cachedProductFamilyService::calculateContainerServiceValues); - return new ResponseEntity<>(apiContainers, HttpStatus.OK); - } - } catch (Exception e) { - apimlLog.log("org.zowe.apiml.apicatalog.containerCouldNotBeRetrieved", e.getMessage()); - throw new ContainerStatusRetrievalThrowable(e); - } - } - - /** - * Get all containers (and included services) - * - * @return a containers by id - */ - @GetMapping(value = "/containers/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = "Retrieves a specific dashboard tile information", - description = "Returns information for a specific tile {id} including status and tile description", - security = { - @SecurityRequirement(name = "BasicAuthorization"), @SecurityRequirement(name = "CookieAuth") - } - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "OK"), - @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "URI not found"), - @ApiResponse(responseCode = "500", description = "An unexpected condition occurred") - }) - public ResponseEntity> getAPIContainerById(@PathVariable(value = "id") String id) throws ContainerStatusRetrievalThrowable { - try { - List apiContainers = new ArrayList<>(); - APIContainer containerById = cachedProductFamilyService.getContainerById(id); - if (containerById != null) { - apiContainers.add(containerById); - } - if (!apiContainers.isEmpty()) { - apiContainers.forEach(apiContainer -> { - // For this single container, check the status of all it's services so it's overall status can be set here - cachedProductFamilyService.calculateContainerServiceValues(apiContainer); - // add API Doc to the services to improve UI performance - setApiDocToService(apiContainer); - }); - } - return new ResponseEntity<>(apiContainers, HttpStatus.OK); - } catch (Exception e) { - apimlLog.log("org.zowe.apiml.apicatalog.containerCouldNotBeRetrieved", e.getMessage()); - throw new ContainerStatusRetrievalThrowable(e); - } - } - - /** - * Get a specific service by id - * - * @return a service by id - */ - @GetMapping(value = "/services/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = "Retrieves a specific service information", - description = "Returns information for a specific service {id} including status and service description", - security = { - @SecurityRequirement(name = "BasicAuthorization"), @SecurityRequirement(name = "CookieAuth") - } - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "OK"), - @ApiResponse(responseCode = "204", description = "No service available"), - @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "URI not found"), - @ApiResponse(responseCode = "500", description = "An unexpected condition occurred") - }) - public ResponseEntity getAPIServicesById(@PathVariable(value = "id") String id) throws ContainerStatusRetrievalThrowable { - try { - - var services = cachedProductFamilyService.getServices(); - if (services == null) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - var service = services.get(id); - if (service == null) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - log.debug("Getting service by id {}", id); - String apiDoc = cachedApiDocService.getDefaultApiDocForService(id); - log.debug("Getting service: {} with status {}", service.getServiceId(), service.getStatus()); - - if (apiDoc != null) { - log.debug("API doc was retrieved"); - service.setApiDoc(apiDoc); - List apiVersions = cachedApiDocService.getApiVersionsForService(id); - service.setApiVersions(apiVersions); - log.debug("Got API versions: {}", apiVersions != null ? apiVersions.size() : 0); - String defaultApiVersion = cachedApiDocService.getDefaultApiVersionForService(id); - log.debug("Default API version: {}", defaultApiVersion); - service.setDefaultApiVersion(defaultApiVersion); - } else { - log.debug("No API doc was retrieved for service with id {}", id); - } - return new ResponseEntity<>(service, HttpStatus.OK); - } catch (Exception e) { - apimlLog.log("org.zowe.apiml.apicatalog.serviceCouldNotBeRetrieved", e.getMessage()); - throw new ContainerStatusRetrievalThrowable(e); - } - } - - private void setApiDocToService(APIContainer apiContainer) { - apiContainer.getServices().forEach(apiService -> { - // try the get the Api Doc for this service, if it fails for any reason then do not change the existing value - // it may or may not be null - String serviceId = apiService.getServiceId(); - try { - String apiDoc = cachedApiDocService.getDefaultApiDocForService(serviceId); - if (apiDoc != null) { - apiService.setApiDoc(apiDoc); - } - - List apiVersions = cachedApiDocService.getApiVersionsForService(serviceId); - apiService.setApiVersions(apiVersions); - - String defaultApiVersion = cachedApiDocService.getDefaultApiVersionForService(serviceId); - apiService.setDefaultApiVersion(defaultApiVersion); - } catch (Exception e) { - log.debug("An error occurred when trying to fetch ApiDoc for service: {}, processing can continue but this service will not be able to display any Api Documentation.\nError:", serviceId, e); - apiService.setApiDocErrorMessage("Failed to fetch API documentation: " + e.getMessage()); - } - }); - } - - /** - * Convert an iterable to a list - * - * @param iterable the collection to convert - * @param the type of the collection - * @return a list - */ - private List toList(final Iterable iterable) { - if (iterable == null) { - return Collections.emptyList(); - } - return StreamSupport.stream(iterable.spliterator(), false) - .toList(); - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocController.java similarity index 60% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocController.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocController.java index fa373e5691..07cd39f1e1 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocController.java @@ -16,35 +16,36 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.openapitools.openapidiff.core.OpenApiCompare; +import org.openapitools.openapidiff.core.model.ChangedOpenApi; +import org.openapitools.openapidiff.core.output.HtmlRender; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.zowe.apiml.apicatalog.services.status.APIServiceStatusService; +import org.springframework.web.bind.annotation.*; +import org.zowe.apiml.apicatalog.exceptions.ApiDiffNotAvailableException; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; + +import static org.apache.hc.core5.http.HttpStatus.SC_OK; /** * Main API for handling requests from the API Catalog UI, routed through the gateway */ -@RestController -@RequestMapping("/apidoc") +@Slf4j @Tag(name = "API Documentation") -public class CatalogApiDocController { - - private final APIServiceStatusService apiServiceStatusService; - - /** - * Create the controller and autowire in the repository services - * - * @param apiServiceStatusService repo service for registered services - */ - @Autowired - public CatalogApiDocController(APIServiceStatusService apiServiceStatusService) { - this.apiServiceStatusService = apiServiceStatusService; - } +@RequiredArgsConstructor +public class ApiDocController { + private final ApiDocService apiDocService; /** * Retrieve the api-doc info for this service @@ -67,12 +68,18 @@ public CatalogApiDocController(APIServiceStatusService apiServiceStatusService) @ApiResponse(responseCode = "404", description = "URI not found"), @ApiResponse(responseCode = "500", description = "An unexpected condition occurred"), }) - public ResponseEntity getApiDocInfo( + @ResponseBody + public Mono> getApiDocInfo( @Parameter(name = "serviceId", description = "The unique identifier of the registered service", required = true, example = "apicatalog") @PathVariable(value = "serviceId") String serviceId, @Parameter(name = "apiId", description = "The API ID and version, separated by a space, of the API documentation", required = true, example = "zowe.apiml.apicatalog v1.0.0") @PathVariable(value = "apiId") String apiId) { - return this.apiServiceStatusService.getServiceCachedApiDocInfo(serviceId, apiId); + return apiDocService.retrieveApiDoc(serviceId, apiId) + .map(apiDoc -> ResponseEntity + .status(SC_OK) + .contentType(MediaType.APPLICATION_JSON) + .body(apiDoc) + ); } /** @@ -94,10 +101,16 @@ public ResponseEntity getApiDocInfo( @ApiResponse(responseCode = "404", description = "URI not found"), @ApiResponse(responseCode = "500", description = "An unexpected condition occurred"), }) - public ResponseEntity getDefaultApiDocInfo( + @ResponseBody + public Mono> getDefaultApiDocInfo( @Parameter(name = "serviceId", description = "The unique identifier of the registered service", required = true, example = "apicatalog") @PathVariable(value = "serviceId") String serviceId) { - return this.apiServiceStatusService.getServiceCachedDefaultApiDocInfo(serviceId); + return apiDocService.retrieveDefaultApiDoc(serviceId) + .map(apiDoc -> ResponseEntity + .status(SC_OK) + .contentType(MediaType.APPLICATION_JSON) + .body(apiDoc) + ); } @GetMapping(value = "/{serviceId}/{apiId1}/{apiId2}", produces = MediaType.TEXT_HTML_VALUE) @@ -113,13 +126,64 @@ public ResponseEntity getDefaultApiDocInfo( @ApiResponse(responseCode = "404", description = "URI not found"), @ApiResponse(responseCode = "500", description = "An unexpected condition occurred") }) - public ResponseEntity getApiDiff( + @ResponseBody + public Mono> getApiDiff( @Parameter(name = "serviceId", description = "The unique identifier of the registered service", required = true, example = "apicatalog") @PathVariable(value = "serviceId") String serviceId, @Parameter(name = "apiId1", description = "The API ID and version, separated by a space, of the API documentation", required = true, example = "zowe.apiml.apicatalog v1.0.0") @PathVariable(value = "apiId1") String apiId1, @Parameter(name = "apiId2", description = "The API ID and version, separated by a space, of the API documentation", required = true, example = "zowe.apiml.apicatalog v2.0.0") @PathVariable(value = "apiId2") String apiId2) { - return this.apiServiceStatusService.getApiDiffInfo(serviceId, apiId1, apiId2); + + return Mono.zip( + apiDocService.retrieveApiDoc(serviceId, apiId1), + apiDocService.retrieveApiDoc(serviceId, apiId2) + ).flatMap(tuple -> Mono.fromCallable(() -> { + ChangedOpenApi diff = OpenApiCompare.fromContents(tuple.getT1(), tuple.getT2()); + HtmlRender render = new HtmlRender(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + render.render(diff, new OutputStreamWriter(baos)); + String result = new String(baos.toByteArray(), StandardCharsets.UTF_8); + + // Remove external stylesheet + result = result.replace("", ""); + return ResponseEntity + .ok() + .contentType(MediaType.TEXT_HTML) + .body(result); + })) + .onErrorMap(e -> { + if (e instanceof ApiDocNotFoundException) { + return e; + } + return new ApiDiffNotAvailableException( + String.format("Error retrieving API diff for '%s' with versions '%s' and '%s'", serviceId, apiId1, apiId2), + e + ); + }); } + +} + +@RestController +@RequestMapping("/apicatalog/api/v1/apidoc") +@ConditionalOnBean(name = "modulithConfig") +class ApiDocControllerModulith extends ApiDocController { + + public ApiDocControllerModulith(ApiDocService apiDocService) { + super(apiDocService); + } + +} + +@RestController +@RequestMapping("/apicatalog/apidoc") +@ConditionalOnMissingBean(name = "modulithConfig") +class ApiDocControllerMicroservice extends ApiDocController { + + public ApiDocControllerMicroservice(ApiDocService apiDocService) { + super(apiDocService); + } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ImageController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ImageController.java index fa687352d6..1f5f15612a 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ImageController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ImageController.java @@ -11,6 +11,8 @@ package org.zowe.apiml.apicatalog.controllers.api; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -19,11 +21,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; import java.io.File; -@RestController -@RequestMapping("/") public class ImageController { @Value("${apiml.catalog.customStyle.logo:}") @@ -51,17 +52,31 @@ private MediaType getMediaType(String fileName) { @GetMapping(value = "/custom-logo") @ResponseBody - public ResponseEntity downloadImage() { + public Mono> downloadImage() { File imageFile = new File(image); if (!imageFile.exists()) { - return ResponseEntity.notFound().build(); + return Mono.just(ResponseEntity.notFound().build()); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(getMediaType(image)); - return ResponseEntity.ok() + return Mono.fromSupplier(() -> ResponseEntity.ok() .headers(headers) - .body(new FileSystemResource(imageFile)); + .body(new FileSystemResource(imageFile))); } } + +@RestController +@RequestMapping("/apicatalog/api/v1/") +@ConditionalOnBean(name = "modulithConfig") +class ImageControllerModulith extends ImageController { + +} + +@RestController +@RequestMapping("/apicatalog") +@ConditionalOnMissingBean(name = "modulithConfig") +class ImageControllerMicroservice extends ImageController { + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/MockController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/MockController.java deleted file mode 100644 index 2e9bd2357f..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/MockController.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.zowe.apiml.apicatalog.standalone.ExampleService; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * This controller simulates the responses of services that are not available in the standalone mode. - */ -@Controller -@RequestMapping("/mock") -@SuppressWarnings("squid:S3752") // this controller cannot be more accurate in definition, it should handle all requests -@Tag(name = "API Catalog") -@RequiredArgsConstructor -@ConditionalOnProperty(value = "apiml.catalog.standalone.enabled", havingValue = "true") -public class MockController { - - private final ExampleService exampleService; - - @RequestMapping("/**") - public void mockEndpoint(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException { - String path = httpServletRequest.getRequestURI(); - int index = path.indexOf("/mock"); - if (index >= 0) path = path.substring(index + "/mock".length()); - exampleService.replyExample(httpServletResponse, httpServletRequest.getMethod(), path); - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/OidcController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/OidcController.java new file mode 100644 index 0000000000..bd01b7904b --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/OidcController.java @@ -0,0 +1,61 @@ +/* + * 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.apicatalog.controllers.api; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zowe.apiml.apicatalog.oidc.OidcUtils; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Endpoints related to the OIDC integration + */ +@Slf4j +@Tag(name = "OIDC integration") +public class OidcController { + + private AtomicReference> oidcProviderCache = new AtomicReference<>(); + + @GetMapping(value = "/provider", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono>> getOidcProvider() { + if (oidcProviderCache.get() == null) { + oidcProviderCache.set(OidcUtils.getOidcProvider()); + } + + return Mono.just(new ResponseEntity<>(oidcProviderCache.get(), oidcProviderCache.get().isEmpty() ? HttpStatus.NO_CONTENT : HttpStatus.OK)); + } + +} + +@RestController +@RequestMapping({"/apicatalog/oidc", "/apicatalog/api/v1/oidc"}) +@ConditionalOnBean(name = "modulithConfig") +class OidcControllerModulith extends OidcController { + +} + +@RestController +@RequestMapping({"/apicatalog/oidc", "/apicatalog/api/v1/oidc"}) +@ConditionalOnMissingBean(name = "modulithConfig") +class OidcControllerMicroservice extends OidcController { + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ServicesController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ServicesController.java new file mode 100644 index 0000000000..5ee212630a --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ServicesController.java @@ -0,0 +1,241 @@ +/* + * 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.apicatalog.controllers.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalException; +import org.zowe.apiml.apicatalog.model.APIContainer; +import org.zowe.apiml.apicatalog.model.APIService; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; +import org.zowe.apiml.apicatalog.swagger.ContainerService; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.List; +import java.util.stream.StreamSupport; + +/** + * Main API for handling requests from the API Catalog UI, routed through the gateway + */ +@Slf4j +@Tag(name = "API Catalog") +@RequiredArgsConstructor +public class ServicesController { + + private final ContainerService containerService; + private final ApiDocService apiDocService; + + @InjectApimlLogger + private final ApimlLogger apimlLog = ApimlLogger.empty(); + + /** + * Get all containers + * + * @return a list of all containers + */ + @GetMapping(value = "/containers", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Lists catalog dashboard tiles", + description = "Returns a list of tiles including status and tile description", + security = { + @SecurityRequirement(name = "BasicAuthorization"), @SecurityRequirement(name = "CookieAuth") + } + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "204", description = "No service available"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "URI not found"), + @ApiResponse(responseCode = "500", description = "An unexpected condition occurred") + }) + @ResponseBody + public Mono>> getAllAPIContainers() throws ContainerStatusRetrievalException { + try { + Iterable allContainers = containerService.getAllContainers(); + List apiContainers = toList(allContainers); + if (apiContainers.isEmpty()) { + // TODO: replace with 404 + return Mono.just(ResponseEntity.status(HttpStatus.NO_CONTENT).build()); + } + return Mono.just(ResponseEntity.ok(apiContainers)); + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.apicatalog.containerCouldNotBeRetrieved", e.getMessage()); + throw new ContainerStatusRetrievalException(e); + } + } + + /** + * Get all containers (and included services) + * + * @return a containers by id + */ + @GetMapping(value = "/containers/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Retrieves a specific dashboard tile information", + description = "Returns information for a specific tile {id} including status and tile description", + security = { + @SecurityRequirement(name = "BasicAuthorization"), @SecurityRequirement(name = "CookieAuth") + } + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "URI not found"), + @ApiResponse(responseCode = "500", description = "An unexpected condition occurred") + }) + @ResponseBody + public Mono>> getAPIContainerById(@PathVariable(value = "id") String id) throws ContainerStatusRetrievalException { + APIContainer containerById; + try { + containerById = containerService.getContainerById(id); + if (containerById == null) { + return Mono.just(new ResponseEntity<>(Collections.emptyList(), HttpStatus.NOT_FOUND)); + } + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.apicatalog.containerCouldNotBeRetrieved", e.getMessage()); + throw new ContainerStatusRetrievalException(e); + } + + return Flux.fromIterable(containerById.getServices()) + .map(s -> { + try { + s.setApiVersions(apiDocService.retrieveApiVersions(s.getServiceId())); + s.setDefaultApiVersion(apiDocService.retrieveDefaultApiVersion(s.getServiceId())); + } catch (Exception e) { + log.debug("An error occurred when trying to fetch ApiDoc for service: {}, processing can continue but this service will not be able to display any Api Documentation.\nError:", s.getServiceId(), e); + s.setApiDocErrorMessage("Failed to fetch API documentation: " + e.getMessage()); + } + return s; + }) + .log() + .flatMap(s -> + Mono.zip(Mono.just(s), apiDocService.retrieveDefaultApiDoc(s.getServiceId()) + .onErrorResume(Exception.class, e -> { + log.debug("An error occurred when trying to fetch ApiDoc for service: {}, processing can continue but this service will not be able to display any Api Documentation.\nError:", s.getServiceId(), e); + s.setApiDocErrorMessage("Failed to fetch API documentation: " + e.getMessage()); + return Mono.empty(); + }) + ) + ) + .map(x -> { + x.getT1().setApiDoc(x.getT2()); + return x; + }) + .then(Mono.just(containerById)) + .map(Collections::singletonList) + .map(c -> new ResponseEntity<>(c, HttpStatus.OK)); + } + + /** + * Get a specific service by id + * + * @return a service by id + */ + @GetMapping(value = "/services/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Retrieves a specific service information", + description = "Returns information for a specific service {id} including status and service description", + security = { + @SecurityRequirement(name = "BasicAuthorization"), @SecurityRequirement(name = "CookieAuth") + } + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "204", description = "No service available"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "URI not found"), + @ApiResponse(responseCode = "500", description = "An unexpected condition occurred") + }) + @ResponseBody + public Mono> getAPIServicesById(@PathVariable(value = "id") String id) { + + var service = containerService.getService(id); + if (service == null) { + return Mono.just(new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + log.debug("Getting service api doc by id {}", id); + return apiDocService.retrieveDefaultApiDoc(id) + .onErrorResume(e -> { + log.debug("Cannot download api doc", e); + return Mono.empty(); + }) + .map(apiDoc -> { + log.debug("API doc was retrieved"); + service.setApiDoc(apiDoc); + List apiVersions = apiDocService.retrieveApiVersions(id); + service.setApiVersions(apiVersions); + log.debug("Got API versions: {}", apiVersions != null ? apiVersions.size() : 0); + String defaultApiVersion = apiDocService.retrieveDefaultApiVersion(id); + log.debug("Default API version: {}", defaultApiVersion); + service.setDefaultApiVersion(defaultApiVersion); + return service; + }) + .switchIfEmpty(Mono.just(service)) + .map(s -> new ResponseEntity<>(s, HttpStatus.OK)) + .onErrorMap(Exception.class, e -> { + apimlLog.log("org.zowe.apiml.apicatalog.serviceCouldNotBeRetrieved", e.getMessage()); + return new ContainerStatusRetrievalException(e); + }); + } + + /** + * Convert an iterable to a list + * + * @param iterable the collection to convert + * @param the type of the collection + * @return a list + */ + private List toList(final Iterable iterable) { + if (iterable == null) { + return Collections.emptyList(); + } + return StreamSupport.stream(iterable.spliterator(), false) + .toList(); + } + +} + +@RestController +@RequestMapping("/apicatalog/api/v1/") +@ConditionalOnBean(name = "modulithConfig") +class ServicesControllerModulith extends ServicesController { + + public ServicesControllerModulith(ContainerService containerService, ApiDocService apiDocService) { + super(containerService, apiDocService); + } + +} + +@RestController +@RequestMapping("/apicatalog/") +@ConditionalOnMissingBean(name = "modulithConfig") +class ServicesControllerMicroservice extends ServicesController { + + public ServicesControllerMicroservice(ContainerService containerService, ApiDocService apiDocService) { + super(containerService, apiDocService); + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java new file mode 100644 index 0000000000..830f10046e --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java @@ -0,0 +1,62 @@ +/* + * 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.apicatalog.controllers.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zowe.apiml.apicatalog.staticapi.StaticAPIService; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@RequiredArgsConstructor +public class StaticAPIRefreshController { + + private final StaticAPIService staticAPIService; + + @PostMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono> refreshStaticApis() { + return Mono.fromCallable(staticAPIService::refresh) + .map(staticAPIResponse -> ResponseEntity + .status(staticAPIResponse.getStatusCode()) + .body(staticAPIResponse.getBody()) + ) + .subscribeOn(Schedulers.boundedElastic()); + } + +} + +@RestController +@RequestMapping("/apicatalog/api/v1/static-api") +@ConditionalOnBean(name = "modulithConfig") +class StaticAPIRefreshControllerModulith extends StaticAPIRefreshController { + + public StaticAPIRefreshControllerModulith(StaticAPIService staticAPIService) { + super(staticAPIService); + } + +} + +@RestController +@RequestMapping("/apicatalog/static-api") +@ConditionalOnMissingBean(name = "modulithConfig") +class StaticAPIRefreshControllerMicroservice extends StaticAPIRefreshController { + + public StaticAPIRefreshControllerMicroservice(StaticAPIService staticAPIService) { + super(staticAPIService); + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticDefinitionController.java similarity index 51% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionController.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticDefinitionController.java index 5f14c450de..b1cebd842c 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticDefinitionController.java @@ -8,13 +8,18 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.staticapi; +package org.zowe.apiml.apicatalog.controllers.api; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.zowe.apiml.apicatalog.staticapi.StaticAPIResponse; +import org.zowe.apiml.apicatalog.staticapi.StaticDefinitionGenerator; +import reactor.core.publisher.Mono; import java.io.IOException; @@ -22,11 +27,10 @@ * Controller to handle the request issued from the UI to generate * a static definition file from the Wizard interface */ -@RestController -@RequestMapping("/static-api") @RequiredArgsConstructor @PreAuthorize("@safMethodSecurityExpressionRoot.hasSafServiceResourceAccess('SERVICES', 'READ',#root)") public class StaticDefinitionController { + private final StaticDefinitionGenerator staticDefinitionGenerator; /** @@ -36,11 +40,12 @@ public class StaticDefinitionController { * @return the response entity */ @PostMapping(value = "/generate", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity generateStaticDef(@RequestBody String payload, @RequestHeader(value = "Service-Id") String serviceId) throws IOException { + @ResponseBody + public Mono> generateStaticDef(@RequestBody String payload, @RequestHeader(value = "Service-Id") String serviceId) throws IOException { StaticAPIResponse staticAPIResponse = staticDefinitionGenerator.generateFile(payload, serviceId); - return ResponseEntity + return Mono.just(ResponseEntity .status(staticAPIResponse.getStatusCode()) - .body(staticAPIResponse.getBody()); + .body(staticAPIResponse.getBody())); } /** @@ -50,17 +55,41 @@ public ResponseEntity generateStaticDef(@RequestBody String payload, @Re * @return the response entity */ @PostMapping(value = "/override", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity overrideStaticDef(@RequestBody String payload, @RequestHeader(value = "Service-Id") String serviceId) throws IOException { + @ResponseBody + public Mono> overrideStaticDef(@RequestBody String payload, @RequestHeader(value = "Service-Id") String serviceId) throws IOException { StaticAPIResponse staticAPIResponse = staticDefinitionGenerator.overrideFile(payload, serviceId); - return ResponseEntity + return Mono.just(ResponseEntity .status(staticAPIResponse.getStatusCode()) - .body(staticAPIResponse.getBody()); + .body(staticAPIResponse.getBody())); } - @DeleteMapping(value = "/delete", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity deleteStaticDef(@RequestHeader(value = "Service-Id") String serviceId) throws IOException { + @ResponseBody + public Mono> deleteStaticDef(@RequestHeader(value = "Service-Id") String serviceId) throws IOException { StaticAPIResponse staticAPIResponse = staticDefinitionGenerator.deleteFile(serviceId); - return ResponseEntity.status(staticAPIResponse.getStatusCode()).body(staticAPIResponse.getBody()); + return Mono.just(ResponseEntity.status(staticAPIResponse.getStatusCode()).body(staticAPIResponse.getBody())); + } + +} + +@RestController +@RequestMapping("/apicatalog/api/v1/static-api") +@ConditionalOnBean(name = "modulithConfig") +class StaticDefinitionControllerModulith extends StaticDefinitionController { + + public StaticDefinitionControllerModulith(StaticDefinitionGenerator staticDefinitionGenerator) { + super(staticDefinitionGenerator); + } + +} + +@RestController +@RequestMapping("/apicatalog/static-api") +@ConditionalOnMissingBean(name = "modulithConfig") +class StaticDefinitionControllerMicroservice extends StaticDefinitionController { + + public StaticDefinitionControllerMicroservice(StaticDefinitionGenerator staticDefinitionGenerator) { + super(staticDefinitionGenerator); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/TokenController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/TokenController.java new file mode 100644 index 0000000000..07457681b2 --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/TokenController.java @@ -0,0 +1,114 @@ +/* + * 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.apicatalog.controllers.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.apache.http.HttpHeaders; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpCookie; +import org.springframework.http.ResponseCookie; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.security.client.service.GatewaySecurity; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.login.LoginFilter; +import org.zowe.apiml.security.common.login.LoginRequest; +import org.zowe.apiml.security.common.token.QueryResponse; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST; +import static org.apache.hc.core5.http.HttpStatus.SC_NO_CONTENT; + +@RestController +@RequestMapping("/apicatalog/auth") +@ConditionalOnMissingBean(name = "modulithConfig") +@RequiredArgsConstructor +public class TokenController { + + private final ObjectMapper mapper; + private final GatewaySecurity gatewaySecurity; + private final AuthConfigurationProperties authConfigurationProperties; + + private AuthConfigurationProperties.CookieProperties cp; + private int cookieMaxAge = -1; + + @PostConstruct + void initConstants() { + cp = authConfigurationProperties.getCookieProperties(); + if (cp.getCookieMaxAge() != null) { + cookieMaxAge = cp.getCookieMaxAge(); + } + } + + @PostMapping(value = "/login") + public Mono login( + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String requestHeader, + ServerWebExchange exchange + ) { + return DataBufferUtils.join(exchange.getRequest().getBody()) + .handle((buffer, sink) -> { + try (InputStream is = buffer.asInputStream()) { + sink.next(mapper.readValue(is, LoginRequest.class)); + } catch (IOException e) { + sink.error(new AuthenticationCredentialsNotFoundException("Login object has wrong format.", e)); + } finally { + DataBufferUtils.release(buffer); + } + }) + .switchIfEmpty(Mono.fromSupplier(() -> LoginFilter + .getCredentialFromAuthorizationHeader(Optional.ofNullable(requestHeader)) + .orElse( null) + )) + .switchIfEmpty(Mono.error(() -> WebClientResponseException.create(SC_BAD_REQUEST, "bad request", exchange.getRequest().getHeaders(), new byte[0], StandardCharsets.UTF_8))) + .flatMap(login -> + gatewaySecurity.login(login.getUsername(), login.getPassword(), null).map(token -> { + exchange.getResponse().addCookie(ResponseCookie.from(cp.getCookieName(), token) + .path(cp.getCookiePath()) + .sameSite(cp.getCookieSameSite().getValue()) + .maxAge(cookieMaxAge) + .httpOnly(true) + .secure(cp.isCookieSecure()) + .build() + ); + exchange.getResponse().setRawStatusCode(SC_NO_CONTENT); + return Mono.empty(); + }).orElse(Mono.error(() -> new InsufficientAuthenticationException("No credentials provided."))) + ); + } + + @GetMapping("/query") + public Mono login( + ServerWebExchange exchange + ) { + return Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(org.springframework.http.HttpHeaders.AUTHORIZATION)) + .filter(header -> header.startsWith("Bearer ")) + .map(header -> header.substring("Bearer ".length())) + .map(String::trim) + .or(() -> Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cp.getCookieName())) + .map(HttpCookie::getValue) + ) + .map(gatewaySecurity::query) + .map(Mono::just) + .orElse(Mono.error(() -> new InsufficientAuthenticationException("No credentials provided."))); + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/ApiCatalogControllerExceptionHandler.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/ApiCatalogControllerExceptionHandler.java index 3167ff76af..ddccf64f14 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/ApiCatalogControllerExceptionHandler.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/ApiCatalogControllerExceptionHandler.java @@ -10,23 +10,29 @@ package org.zowe.apiml.apicatalog.controllers.handlers; -import org.zowe.apiml.apicatalog.controllers.api.ApiCatalogController; -import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalThrowable; -import org.zowe.apiml.message.api.ApiMessageView; -import org.zowe.apiml.message.core.Message; -import org.zowe.apiml.message.core.MessageService; import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.zowe.apiml.apicatalog.controllers.api.ServicesController; +import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalException; +import org.zowe.apiml.message.api.ApiMessageView; +import org.zowe.apiml.message.core.Message; +import org.zowe.apiml.message.core.MessageService; +import reactor.core.publisher.Mono; + +import static org.springframework.http.MediaType.APPLICATION_JSON; /** * This class creates responses for exceptional behavior of the ApiCatalogController */ -@ControllerAdvice(assignableTypes = {ApiCatalogController.class}) +@Order(0) +@ControllerAdvice(assignableTypes = {ServicesController.class}) @RequiredArgsConstructor public class ApiCatalogControllerExceptionHandler { + private final MessageService messageService; /** @@ -35,11 +41,13 @@ public class ApiCatalogControllerExceptionHandler { * @param exception ContainerStatusRetrievalThrowable * @return 500 and the message 'Could not retrieve container statuses, {optional text}' */ - @ExceptionHandler(ContainerStatusRetrievalThrowable.class) - public ResponseEntity handleServiceNotFoundException(ContainerStatusRetrievalThrowable exception) { + @ExceptionHandler(ContainerStatusRetrievalException.class) + public Mono> handleServiceNotFoundException(ContainerStatusRetrievalException exception) { Message message = messageService.createMessage("org.zowe.apiml.apicatalog.containerStatusRetrievalException", exception.getMessage()); - return ResponseEntity + return Mono.just(ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(message.mapToView()); + .contentType(APPLICATION_JSON) + .body(message.mapToView())); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/CatalogApiDocControllerExceptionHandler.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/CatalogApiDocControllerExceptionHandler.java index 76fab5bc27..4c9a97334f 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/CatalogApiDocControllerExceptionHandler.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/CatalogApiDocControllerExceptionHandler.java @@ -10,54 +10,62 @@ package org.zowe.apiml.apicatalog.controllers.handlers; -import org.zowe.apiml.apicatalog.controllers.api.CatalogApiDocController; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; -import org.zowe.apiml.apicatalog.services.status.model.ServiceNotFoundException; -import org.zowe.apiml.message.api.ApiMessageView; -import org.zowe.apiml.message.core.Message; -import org.zowe.apiml.message.core.MessageService; import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.zowe.apiml.apicatalog.controllers.api.ApiDocController; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.exceptions.ServiceNotFoundException; +import org.zowe.apiml.message.api.ApiMessageView; +import org.zowe.apiml.message.core.Message; +import org.zowe.apiml.message.core.MessageService; +import reactor.core.publisher.Mono; + +import static org.springframework.http.MediaType.APPLICATION_JSON; /** * This class creates responses for exceptional behavior of the CatalogApiDocController */ -@ControllerAdvice(assignableTypes = {CatalogApiDocController.class}) +@Order(0) +@ControllerAdvice(assignableTypes = {ApiDocController.class}) @RequiredArgsConstructor public class CatalogApiDocControllerExceptionHandler { + private final MessageService messageService; /** * Could not retrieve the API Documentation * * @param exception InvalidFormatException - * @return 500 and the message 'TBD' + * @return 404 and the message 'API Documentation not retrieved...' */ @ExceptionHandler(ApiDocNotFoundException.class) - public ResponseEntity handleApiDocNotFoundException(ApiDocNotFoundException exception) { + public Mono> handleApiDocNotFoundException(ApiDocNotFoundException exception) { Message message = messageService.createMessage("org.zowe.apiml.apicatalog.apiDocNotFound", exception.getMessage()); - return ResponseEntity + return Mono.just(ResponseEntity .status(HttpStatus.NOT_FOUND) - .body(message.mapToView()); + .contentType(APPLICATION_JSON) + .body(message.mapToView())); } /** * Could not retrieve the API Documentation as the Gateway was not available * * @param exception InvalidFormatException - * @return 404 and the message 'TBD' + * @return 404 and the message 'Service not located...' */ @ExceptionHandler(ServiceNotFoundException.class) - public ResponseEntity handleServiceNotFoundException(ServiceNotFoundException exception) { - + public Mono> handleServiceNotFoundException(ServiceNotFoundException exception) { Message message = messageService.createMessage("org.zowe.apiml.apicatalog.serviceNotFound", exception.getMessage()); - return ResponseEntity + return Mono.just(ResponseEntity .status(HttpStatus.NOT_FOUND) - .body(message.mapToView()); + .contentType(APPLICATION_JSON) + .body(message.mapToView())); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/custom/CustomErrorStatusHandlingBean.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/CustomErrorStatusHandlingBean.java similarity index 80% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/custom/CustomErrorStatusHandlingBean.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/CustomErrorStatusHandlingBean.java index 9f5460d9e1..e88d02cdda 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/custom/CustomErrorStatusHandlingBean.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/CustomErrorStatusHandlingBean.java @@ -8,19 +8,23 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.controllers.handlers.custom; +package org.zowe.apiml.apicatalog.controllers.handlers; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -@Component +@Component("catalogCustomErrorStatusHandlingBean") +@ConditionalOnMissingBean(name = "modulithConfig") public class CustomErrorStatusHandlingBean implements WebServerFactoryCustomizer { + @Override public void customize(ConfigurableServletWebServerFactory factory) { factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/not_found")); factory.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/internal_error")); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/DefaultExceptionHandler.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/DefaultExceptionHandler.java new file mode 100644 index 0000000000..a0feaeac24 --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/DefaultExceptionHandler.java @@ -0,0 +1,55 @@ +/* + * 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.apicatalog.controllers.handlers; + +import jakarta.servlet.ServletException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.message.api.ApiMessageView; +import org.zowe.apiml.security.common.error.AuthExceptionHandler; +import reactor.core.publisher.Mono; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +@Slf4j +@ControllerAdvice +@RequiredArgsConstructor +public class DefaultExceptionHandler { + + private final AuthExceptionHandler authExceptionHandler; + + @ExceptionHandler(Exception.class) + public Mono> handleException(ServerWebExchange exchange, Exception exception) { + AtomicReference> responseJson = new AtomicReference<>(); + BiConsumer consumer = (message, status) -> + responseJson.set(ResponseEntity + .status(status) + .contentType(MediaType.APPLICATION_JSON) + .body(message) + ); + + try { + authExceptionHandler.handleException(exchange.getRequest().getPath().value(), consumer, exchange.getResponse().getHeaders()::add, exception); + return Mono.just(responseJson.get()); + } catch (ServletException e) { + log.error("Cannot handle exception: {}", exception, e); + return Mono.error(() -> new RuntimeException(e)); + } + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java index 830f5e331b..11f79fff58 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/NotFoundErrorController.java @@ -10,15 +10,14 @@ package org.zowe.apiml.apicatalog.controllers.handlers; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.RequestDispatcher; -import jakarta.servlet.http.HttpServletRequest; - /** * Handles errors in REST API processing. diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshControllerExceptionHandler.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/StaticAPIRefreshControllerExceptionHandler.java similarity index 70% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshControllerExceptionHandler.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/StaticAPIRefreshControllerExceptionHandler.java index 5c8c8e1d16..491b201880 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshControllerExceptionHandler.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/StaticAPIRefreshControllerExceptionHandler.java @@ -8,23 +8,29 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.staticapi; +package org.zowe.apiml.apicatalog.controllers.handlers; import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.client.RestClientException; -import org.zowe.apiml.apicatalog.services.status.model.ServiceNotFoundException; +import org.zowe.apiml.apicatalog.controllers.api.StaticAPIRefreshController; +import org.zowe.apiml.apicatalog.exceptions.ServiceNotFoundException; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.Message; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.product.constants.CoreService; +import reactor.core.publisher.Mono; + +import static org.springframework.http.MediaType.APPLICATION_JSON; /** * This class creates responses for exceptional behavior of the StaticAPIRefreshController */ +@Order(0) @ControllerAdvice(assignableTypes = {StaticAPIRefreshController.class}) @RequiredArgsConstructor public class StaticAPIRefreshControllerExceptionHandler { @@ -37,12 +43,13 @@ public class StaticAPIRefreshControllerExceptionHandler { * @return 503 status code */ @ExceptionHandler(ServiceNotFoundException.class) - public ResponseEntity handleServiceNotFoundException(ServiceNotFoundException exception) { + public Mono> handleServiceNotFoundException(ServiceNotFoundException exception) { Message message = messageService.createMessage("org.zowe.apiml.apicatalog.serviceNotFound", CoreService.DISCOVERY.getServiceId()); - return ResponseEntity + return Mono.just(ResponseEntity .status(HttpStatus.SERVICE_UNAVAILABLE) - .body(message.mapToView()); + .contentType(APPLICATION_JSON) + .body(message.mapToView())); } /** @@ -52,12 +59,13 @@ public ResponseEntity handleServiceNotFoundException(ServiceNotF * @return 500 status code if there is any exception with refresh api */ @ExceptionHandler(RestClientException.class) - public ResponseEntity handleServiceNotFoundException(RestClientException exception) { + public Mono> handleServiceNotFoundException(RestClientException exception) { Message message = messageService.createMessage("org.zowe.apiml.apicatalog.StaticApiRefreshFailed", exception); - return ResponseEntity + return Mono.just(ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(message.mapToView()); + .contentType(APPLICATION_JSON) + .body(message.mapToView())); } } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionControllerExceptionHandler.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/StaticDefinitionControllerExceptionHandler.java similarity index 73% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionControllerExceptionHandler.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/StaticDefinitionControllerExceptionHandler.java index 71851e74c2..3dc9666013 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionControllerExceptionHandler.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/handlers/StaticDefinitionControllerExceptionHandler.java @@ -8,25 +8,31 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.staticapi; +package org.zowe.apiml.apicatalog.controllers.handlers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.zowe.apiml.apicatalog.controllers.api.StaticDefinitionController; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.Message; import org.zowe.apiml.message.core.MessageService; +import reactor.core.publisher.Mono; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; +import static org.springframework.http.MediaType.APPLICATION_JSON; + /** * This class creates responses for exceptional behavior of the StaticDefinitionController */ @Slf4j +@Order(0) @ControllerAdvice(assignableTypes = {StaticDefinitionController.class}) @RequiredArgsConstructor public class StaticDefinitionControllerExceptionHandler { @@ -39,14 +45,15 @@ public class StaticDefinitionControllerExceptionHandler { * @return 500 status code */ @ExceptionHandler(IOException.class) - public ResponseEntity handleIOException(IOException exception) { + public Mono> handleIOException(IOException exception) { log.error("Cannot write the static definition file because: {}", exception.getMessage()); Message message = messageService.createMessage("org.zowe.apiml.apicatalog.StaticDefinitionGenerationFailed", exception); - return ResponseEntity + return Mono.just(ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(message.mapToView()); + .contentType(APPLICATION_JSON) + .body(message.mapToView())); } /** @@ -56,12 +63,13 @@ public ResponseEntity handleIOException(IOException exception) { * @return 409 status code */ @ExceptionHandler(FileAlreadyExistsException.class) - public ResponseEntity handleFileAlreadyExistsException(FileAlreadyExistsException exception) { + public Mono> handleFileAlreadyExistsException(FileAlreadyExistsException exception) { Message message = messageService.createMessage("org.zowe.apiml.apicatalog.StaticDefinitionGenerationFailed", exception); - return ResponseEntity + return Mono.just(ResponseEntity .status(HttpStatus.CONFLICT) - .body(message.mapToView()); + .contentType(APPLICATION_JSON) + .body(message.mapToView())); } } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiDiffNotAvailableException.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDiffNotAvailableException.java similarity index 92% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiDiffNotAvailableException.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDiffNotAvailableException.java index 8b0de789ec..758e46cf1d 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiDiffNotAvailableException.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDiffNotAvailableException.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.status.model; +package org.zowe.apiml.apicatalog.exceptions; /** * Exception thrown when API diff is not available diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiDocNotFoundException.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDocNotFoundException.java similarity index 86% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiDocNotFoundException.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDocNotFoundException.java index bcf4ba20e2..e61fe7a5d3 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiDocNotFoundException.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDocNotFoundException.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.status.model; +package org.zowe.apiml.apicatalog.exceptions; /** * Exception thrown when API Doc is not accessible @@ -21,8 +21,8 @@ public ApiDocNotFoundException(String message, Throwable cause) { super(message, cause); } - public ApiDocNotFoundException(String s) { - super(s); + public ApiDocNotFoundException(String message) { + this(message, null); } /** @@ -33,4 +33,5 @@ public ApiDocNotFoundException(String s) { public synchronized Throwable fillInStackTrace() { return this; } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocTransformationException.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDocTransformationException.java similarity index 91% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocTransformationException.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDocTransformationException.java index e3b35c5e6a..f443bc48f2 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocTransformationException.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiDocTransformationException.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.swagger; +package org.zowe.apiml.apicatalog.exceptions; public class ApiDocTransformationException extends RuntimeException { diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiVersionNotFoundException.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiVersionNotFoundException.java similarity index 84% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiVersionNotFoundException.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiVersionNotFoundException.java index 37451c7bcb..e786df4671 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ApiVersionNotFoundException.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ApiVersionNotFoundException.java @@ -8,9 +8,12 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.status.model; +package org.zowe.apiml.apicatalog.exceptions; public class ApiVersionNotFoundException extends RuntimeException { + + private static final long serialVersionUID = 1187349522463958685L; + public ApiVersionNotFoundException(String s) { super(s); } @@ -22,4 +25,5 @@ public ApiVersionNotFoundException(String s) { public synchronized Throwable fillInStackTrace() { return this; } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ContainerStatusRetrievalThrowable.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ContainerStatusRetrievalException.java similarity index 66% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ContainerStatusRetrievalThrowable.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ContainerStatusRetrievalException.java index 7a1e203428..85676ee3c5 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ContainerStatusRetrievalThrowable.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ContainerStatusRetrievalException.java @@ -10,9 +10,12 @@ package org.zowe.apiml.apicatalog.exceptions; -public class ContainerStatusRetrievalThrowable extends Throwable { +public class ContainerStatusRetrievalException extends Exception { - public ContainerStatusRetrievalThrowable(Throwable e) { + private static final long serialVersionUID = 2060505088907324466L; + + public ContainerStatusRetrievalException(Throwable e) { super(e); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ServiceNotFoundException.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ServiceNotFoundException.java similarity index 90% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ServiceNotFoundException.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ServiceNotFoundException.java index 2d1a0d1a87..2f52d643ab 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/ServiceNotFoundException.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/exceptions/ServiceNotFoundException.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.status.model; +package org.zowe.apiml.apicatalog.exceptions; /** * Exception thrown when a Service is not accessible diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/health/ApiCatalogHealthIndicator.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/health/ApiCatalogHealthIndicator.java index 365a16a107..78d799306b 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/health/ApiCatalogHealthIndicator.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/health/ApiCatalogHealthIndicator.java @@ -10,19 +10,21 @@ package org.zowe.apiml.apicatalog.health; -import org.zowe.apiml.product.constants.CoreService; import lombok.RequiredArgsConstructor; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Component; +import org.zowe.apiml.product.constants.CoreService; /** * Api Catalog health information (/application/health) */ @Component @RequiredArgsConstructor +@ConditionalOnMissingBean(name = "modulithConfig") public class ApiCatalogHealthIndicator extends AbstractHealthIndicator { private final DiscoveryClient discoveryClient; @@ -38,4 +40,5 @@ protected void doHealthCheck(Health.Builder builder) { .status(healthStatus) .withDetail(gatewayServiceId, healthStatus.getCode()); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/EurekaServiceInstanceRequest.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/EurekaServiceInstanceRequest.java deleted file mode 100644 index 900bfc7671..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/EurekaServiceInstanceRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class EurekaServiceInstanceRequest { - - private String serviceId; - private String eurekaRequestUrl; - private String username; - private String password; - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeService.java deleted file mode 100644 index 357669503b..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeService.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.retry.RetryException; -import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Recover; -import org.springframework.retry.annotation.Retryable; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.gateway.GatewayNotAvailableException; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; -import org.zowe.apiml.product.registry.CannotRegisterServiceException; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_ID; - -/** - * Initialize the API catalog with the running instances. - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class InstanceInitializeService { - - private final CachedProductFamilyService cachedProductFamilyService; - private final CachedServicesService cachedServicesService; - private final InstanceRetrievalService instanceRetrievalService; - private final InstanceRefreshService instanceRefreshService; - - @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); - - /** - * Initialise the API Catalog with all current running instances - * The API Catalog itself must be UP before checking all other instances - * If the catalog is not up, or if the fetch fails, then wait for a defined period and retry up to a max of 5 times - * - * @throws CannotRegisterServiceException if the fetch fails or the catalog is not registered with the discovery - */ - @Retryable( - retryFor = {RetryException.class}, - noRetryFor = CannotRegisterServiceException.class, - maxAttempts = 5, - backoff = @Backoff(delayExpression = "#{${apiml.service-registry.serviceFetchDelayInMillis}}")) - public void retrieveAndRegisterAllInstancesWithCatalog() throws CannotRegisterServiceException { - log.info("Initialising API Catalog with Discovery services."); - try { - String serviceId = CoreService.API_CATALOG.getServiceId(); - InstanceInfo apiCatalogInstance = instanceRetrievalService.getInstanceInfo(serviceId); - if (apiCatalogInstance == null) { - String msg = "API Catalog Instance not retrieved from Discovery service"; - log.debug(msg); - throw new RetryException(msg); - } else { - log.info("API Catalog instance found, retrieving all services."); - getAllInstances(apiCatalogInstance); - instanceRefreshService.start(); - } - } catch (InstanceInitializationException | GatewayNotAvailableException e) { - throw new RetryException(e.getMessage()); - } catch (Exception e) { - String msg = "An unexpected exception occurred when trying to retrieve API Catalog instance from Discovery service"; - apimlLog.log("org.zowe.apiml.apicatalog.initializeAborted", e.getMessage()); - throw new CannotRegisterServiceException(msg, e); - } - } - - @Recover - public void recover(RetryException e) { - apimlLog.log("org.zowe.apiml.apicatalog.initializeFailed"); - } - - /** - * Query the discovery service for all running instances - */ - private void updateCacheWithAllInstances() { - Applications discoveryApplications = instanceRetrievalService.getAllInstancesFromDiscovery(false); - - // Only include services which have a instances - List listApplication = discoveryApplications.getRegisteredApplications() - .stream() - .filter(application -> !application.getInstances().isEmpty()) - .toList(); - - // Return an empty string if no services are found after filtering - if (listApplication.isEmpty()) { - log.info("No services found"); - return; - } - - log.debug("Found: " + listApplication.size() + " services on startup."); - String s = listApplication.stream() - .map(Application::getName).collect(Collectors.joining(", ")); - log.debug("Discovered Services: {}", s); - - // create containers for services - listApplication.forEach(this::createContainers); - listApplication.forEach(this::createServices); - // populate the cache - Collection containers = cachedProductFamilyService.getAllContainers(); - log.debug("Cache contains: " + containers.size() + " tiles."); - } - - - public void createContainers(Application application) { - cachedServicesService.updateService(application.getName(), application); - application.getInstances().forEach(instanceInfo -> { - String productFamilyId = instanceInfo.getMetadata().get(CATALOG_ID); - if (productFamilyId != null) { - log.debug("Initialising product family (creating tile for) : " + productFamilyId); - cachedProductFamilyService.saveContainerFromInstance(productFamilyId, instanceInfo); - } - - }); - } - - public void createServices(Application application) { - cachedServicesService.updateService(application.getName(), application); - application.getInstances().forEach(instanceInfo -> { - log.debug("Initialising service: {}", instanceInfo.getInstanceId()); - cachedProductFamilyService.addService(instanceInfo); - }); - } - - private void getAllInstances(InstanceInfo apiCatalogInstance) { - String productFamilyId = apiCatalogInstance.getMetadata().get(CATALOG_ID); - if (productFamilyId != null) { - log.debug("Initialising product family (creating tile for) : " + productFamilyId); - cachedProductFamilyService.saveContainerFromInstance(productFamilyId, apiCatalogInstance); - } - - updateCacheWithAllInstances(); - log.info("API Catalog initialised with running services.."); - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshService.java deleted file mode 100644 index 3631dea2d5..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshService.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; - -import java.util.HashSet; -import java.util.Set; - -import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_ID; - -/** - * Refresh the cache with the latest state of the discovery service - * Use deltas to get latest changes from Eureka - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class InstanceRefreshService { - - // until versioning is implemented, only v1 API docs are supported - private final CachedProductFamilyService cachedProductFamilyService; - private final CachedServicesService cachedServicesService; - private final InstanceRetrievalService instanceRetrievalService; - private boolean isStarted = false; - - @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); - - /** - * Starts refreshing the API Catalog cache - */ - public void start() { - this.isStarted = true; - log.info("InstanceRefreshService started"); - } - - /** - * Periodically refresh the container/service caches - * Depends on the GatewayClient: no refreshes happen when it's not initialized - */ - @Scheduled( - initialDelayString = "${apiml.service-registry.cacheRefreshInitialDelayInMillis}", - fixedDelayString = "${apiml.service-registry.cacheRefreshRetryDelayInMillis}") - public void refreshCacheFromDiscovery() { - if (!isStarted) { - log.debug("InstanceRefreshService is stopped. Skip refresh."); - return; - } - - log.debug("Refreshing API Catalog with the latest state of discovery service"); - - this.compareServices(); - } - - /** - * @return a list of changed services - */ - @SuppressWarnings("deprecation") - private Set compareServices() { - // Updated containers - Set containersUpdated = new HashSet<>(); - Applications cachedServices = cachedServicesService.getAllCachedServices(); - Applications deltaFromDiscovery = instanceRetrievalService.getAllInstancesFromDiscovery(true); - - if (deltaFromDiscovery != null && !deltaFromDiscovery.getRegisteredApplications().isEmpty()) { - // use the version to check if this delta has changed, it is deprecated and should be replaced as soon as a - // newer identifier is provided by Netflix - // if getVersion is removed then the process will be slightly more inefficient but will not need to change - if (cachedServicesService.getVersionDelta() != deltaFromDiscovery.getVersion()) { - containersUpdated = processServiceInstances(cachedServices, deltaFromDiscovery); - } - cachedServicesService.setVersionDelta(deltaFromDiscovery.getVersion()); - } - return containersUpdated; - } - - /** - * Check each delta instance and consider it for processing - * - * @param cachedServices the collection of cached services - * @param deltaFromDiscovery changed instances - */ - private Set processServiceInstances(Applications cachedServices, Applications deltaFromDiscovery) { - Set containersUpdated = new HashSet<>(); - Set updatedServices = updateDelta(deltaFromDiscovery); - updatedServices.forEach(instance -> { - try { - // check if this instance should be processed/updated - processServiceInstance(containersUpdated, cachedServices, deltaFromDiscovery, instance); - } catch (Exception e) { - log.debug("could not update cache for service: " + instance + ", processing will continue.", e); - } - }); - return containersUpdated; - } - - /** - * Get this instance service details and check if it should be processed - * - * @param containersUpdated containers, which were updated - * @param cachedServices existing services - * @param deltaFromDiscovery changed service instances - * @param instance this instance - */ - private void processServiceInstance(Set containersUpdated, Applications cachedServices, - Applications deltaFromDiscovery, InstanceInfo instance) { - Application application = null; - // Get the application which this instance belongs to - if (cachedServices != null && cachedServices.getRegisteredApplications() != null) { - application = cachedServices.getRegisteredApplications().stream() - .filter(service -> service.getName().equalsIgnoreCase(instance.getAppName())).findFirst().orElse(null); - } - // if its new then it will only be in the delta - if (application == null || application.getInstances().isEmpty()) { - application = deltaFromDiscovery.getRegisteredApplications().stream() - .filter(service -> service.getName().equalsIgnoreCase(instance.getAppName())).findFirst().orElse(null); - } - - // there's no chance which this case is not called. It's just double check - if (application == null || application.getInstances().isEmpty()) { - log.debug("Instance {} couldn't get details from cache and delta", instance.getAppName()); - return; - } - - processInstance(containersUpdated, instance, application); - } - - /** - * Go ahead and retrieve this instances API doc and update the cache - * - * @param containersUpdated containers, which were updated - * @param instance the instance - * @param application the service - */ - private void processInstance(Set containersUpdated, InstanceInfo instance, Application application) { - application.addInstance(instance); - - if (!InstanceInfo.InstanceStatus.DOWN.equals(instance.getStatus())) { - // update any containers which contain this service - updateContainer(containersUpdated, instance); - updateService(instance); - } - if (InstanceInfo.ActionType.DELETED.equals(instance.getActionType())) { - // remove instance which isn't available anymore - cachedProductFamilyService.removeInstance(instance.getMetadata().get(CATALOG_ID), instance); - cachedProductFamilyService.removeInstanceFromServices(instance); - return; - } - - // Update the service cache - updateService(instance.getAppName(), application); - } - - private void updateService(String serviceId, Application application) { - if (application == null) { - log.debug("Could not find Application object for serviceId: " + serviceId + " cache not updated with " + - "current values."); - } else { - cachedServicesService.updateService(serviceId, application); - log.debug("Updated service cache for service: " + serviceId); - } - } - - /** - * Update the container - * - * @param containersUpdated what containers were updated - * @param instanceInfo the instance - */ - private void updateContainer(Set containersUpdated, InstanceInfo instanceInfo) { - String productFamilyId = instanceInfo.getMetadata().get(CATALOG_ID); - if (productFamilyId == null) { - log.debug("Cannot create a tile without a parent id, the metadata for service '{}' must contain an entry for '{}'", - instanceInfo.getAppName(), CATALOG_ID); - } else { - APIContainer container = cachedProductFamilyService.saveContainerFromInstance(productFamilyId, instanceInfo); - log.debug("Created/Updated tile and updated cache for container: " + container.getId() + " @ " + container.getLastUpdatedTimestamp().getTime()); - containersUpdated.add(productFamilyId); - } - } - - /** - * Update the container - * - * @param instanceInfo the instance - */ - private void updateService(InstanceInfo instanceInfo) { - cachedProductFamilyService.addService(instanceInfo); - log.debug("Created/Updated service and updated cache: {}", instanceInfo.getInstanceId()); - } - - /** - * Compare cached instances against eureka delta to send back a change-list - * - * @param delta retrieved from Eureka - * @return changed instances - */ - private Set updateDelta(Applications delta) { - int deltaCount = 0; - Set updatedInstances = new HashSet<>(); - for (Application app : delta.getRegisteredApplications()) { - for (InstanceInfo instance : app.getInstances()) { - ++deltaCount; - if (InstanceInfo.ActionType.ADDED.equals(instance.getActionType())) { - log.debug("Added instance {} to the list of changed instances ", instance.getId()); - updatedInstances.add(instance); - } else if (InstanceInfo.ActionType.MODIFIED.equals(instance.getActionType())) { - log.debug("Modified instance {} added to the list of changed instances ", instance.getId()); - updatedInstances.add(instance); - } else if (InstanceInfo.ActionType.DELETED.equals(instance.getActionType())) { - log.debug("Deleted instance {} added to the list of changed instances ", instance.getId()); - instance.setStatus(InstanceInfo.InstanceStatus.DOWN); - updatedInstances.add(instance); - } - } - } - - log.debug("The total number of changed instances fetched by the delta processor : {}", deltaCount); - return updatedInstances; - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java deleted file mode 100644 index f555306de6..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalService.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.converters.jackson.EurekaJsonJacksonCodec; -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import jakarta.validation.constraints.NotBlank; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.apache.hc.core5.http.message.BasicHeader; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; -import org.zowe.apiml.constants.EurekaMetadataDefinition; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; -import org.zowe.apiml.product.registry.ApplicationWrapper; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; - -import static org.zowe.apiml.product.constants.CoreService.GATEWAY; - -/** - * Service for instance retrieval from Eureka - */ -@Slf4j -@Service -public class InstanceRetrievalService { - - private final DiscoveryConfigProperties discoveryConfigProperties; - private final CloseableHttpClient httpClient; - - private static final String APPS_ENDPOINT = "apps/"; - private static final String DELTA_ENDPOINT = "delta"; - private static final String UNKNOWN = "unknown"; - - @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); - - private ObjectMapper mapper = new ObjectMapper(); - - @Autowired - public InstanceRetrievalService(DiscoveryConfigProperties discoveryConfigProperties, - CloseableHttpClient httpClient) { - this.discoveryConfigProperties = discoveryConfigProperties; - this.httpClient = httpClient; - } - - private InstanceInfo getInstanceInfo(String serviceId, AtomicBoolean instanceFound, Predicate selector) { - List eurekaServiceInstanceRequests = constructServiceInfoQueryRequest(serviceId, false); - // iterate over list of discovery services, return at first success - for (EurekaServiceInstanceRequest eurekaServiceInstanceRequest : eurekaServiceInstanceRequests) { - // call Eureka REST endpoint to fetch single or all Instances - try { - String responseBody = queryDiscoveryForInstances(eurekaServiceInstanceRequest); - if (responseBody != null) { - instanceFound.set(true); - return extractSingleInstanceFromApplication(serviceId, responseBody, selector); - } - } catch (Exception e) { - log.debug("Error obtaining instance information from {}, error message: {}", - eurekaServiceInstanceRequest.getEurekaRequestUrl(), e.getMessage()); - } - } - return null; - } - - /** - * Retrieves {@link InstanceInfo} of particular service - * - * @param serviceId the service to search for - * @return service instance - */ - public InstanceInfo getInstanceInfo(@NotBlank(message = "Service Id must be supplied") String serviceId) { - if (serviceId.equalsIgnoreCase(UNKNOWN)) { - return null; - } - - // identification if there was no instance or any error happened during fetching - AtomicBoolean instanceFound = new AtomicBoolean(false); - InstanceInfo instanceInfo = getInstanceInfo(serviceId, instanceFound, - ii -> EurekaMetadataDefinition.RegistrationType.of(ii.getMetadata()).isPrimary() - ); - if (instanceInfo == null) { - // maybe the input is apimlId, try to find the matching Gateway (multi-tenancy use case) - instanceInfo = getInstanceInfo(GATEWAY.getServiceId(), instanceFound, - ii -> EurekaMetadataDefinition.RegistrationType.of(ii.getMetadata()).isAdditional() - ); - } - - if (!instanceFound.get()) { - String msg = "An error occurred when trying to get instance info for: " + serviceId; - throw new InstanceInitializationException(msg); - } - - return instanceInfo; - } - - /** - * Retrieve instances from the discovery service - * - * @param delta filter the registry information to the just updated infos - * @return the Applications object that wraps all the registry information - */ - public Applications getAllInstancesFromDiscovery(boolean delta) { - - List requestInfoList = constructServiceInfoQueryRequest(null, delta); - for (EurekaServiceInstanceRequest requestInfo : requestInfoList) { - try { - String responseBody = queryDiscoveryForInstances(requestInfo); - return extractApplications(responseBody); - } catch (Exception e) { - log.debug("Not able to contact discovery service: {}", requestInfo.getEurekaRequestUrl(), e); - } - } - // call Eureka REST endpoint to fetch single or all Instances - return null; - } - - /** - * Parse information from the response and extract the Applications object which contains all the registry information returned by eureka server - * - * @param responseBody the http response body - * @return Applications object that wraps all the registry information - */ - private Applications extractApplications(String responseBody) { - Applications applications = null; - ObjectMapper mapper = new EurekaJsonJacksonCodec().getObjectMapper(Applications.class); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - try { - applications = mapper.readValue(responseBody, Applications.class); - } catch (IOException e) { - apimlLog.log("org.zowe.apiml.apicatalog.serviceRetrievalParsingFailed", e.getMessage()); - } - - return applications; - } - - /** - * Query Discovery - * - * @param eurekaServiceInstanceRequest information used to query the discovery service - * @return ResponseEntity query response - */ - String queryDiscoveryForInstances(EurekaServiceInstanceRequest eurekaServiceInstanceRequest) throws IOException { - HttpGet httpGet = new HttpGet(eurekaServiceInstanceRequest.getEurekaRequestUrl()); - for (Header header : createRequestHeader(eurekaServiceInstanceRequest)) { - httpGet.setHeader(header); - } - - return httpClient.execute(httpGet, response -> { - final int statusCode = response.getCode(); - final HttpEntity responseEntity = response.getEntity(); - - String responseBody = ""; - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); - } - - if (HttpStatus.valueOf(statusCode).is2xxSuccessful()) { - return responseBody; - } - - apimlLog.log("org.zowe.apiml.apicatalog.serviceRetrievalRequestFailed", - eurekaServiceInstanceRequest.getServiceId(), - eurekaServiceInstanceRequest.getEurekaRequestUrl(), - statusCode, - response.getReasonPhrase() != null ? response.getReasonPhrase() : responseBody - ); - - return null; - }); - } - - /** - * @param serviceId the service to search for - * @param responseBody the fetch attempt response body - * @return service instance - */ - InstanceInfo extractSingleInstanceFromApplication(String serviceId, String responseBody, Predicate selector) { - ApplicationWrapper application = null; - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - try { - application = mapper.readValue(responseBody, ApplicationWrapper.class); - } catch (IOException e) { - log.debug("Could not extract service: {} info from discovery --{}", serviceId, e.getMessage(), e); - } - - return Optional.ofNullable(application) - .map(ApplicationWrapper::getApplication) - .map(Application::getInstances) - .orElse(Collections.emptyList()) - .stream().filter(selector) - .findFirst() - .orElse(null); - } - - /** - * Construct a tuple used to query the discovery service - * - * @param serviceId optional service id - * @return request information - */ - private List constructServiceInfoQueryRequest(String serviceId, boolean getDelta) { - String[] discoveryServiceUrls = discoveryConfigProperties.getLocations(); - List eurekaServiceInstanceRequests = new ArrayList<>(discoveryServiceUrls.length); - for (String discoveryUrl : discoveryServiceUrls) { - String discoveryServiceLocatorUrl = discoveryUrl.endsWith("/") ? discoveryUrl + APPS_ENDPOINT : discoveryUrl + "/" + APPS_ENDPOINT; - if (getDelta) { - discoveryServiceLocatorUrl += DELTA_ENDPOINT; - } else { - if (serviceId != null) { - discoveryServiceLocatorUrl += serviceId.toLowerCase(); - } - } - - String eurekaUsername = discoveryConfigProperties.getEurekaUserName(); - String eurekaUserPassword = discoveryConfigProperties.getEurekaUserPassword(); - - log.debug("Querying instance information of the service {} from the URL {} with the user {} and password {}", - serviceId, discoveryServiceLocatorUrl, eurekaUsername, - StringUtils.isEmpty(eurekaUserPassword) ? "NO PASSWORD" : "*******"); - - EurekaServiceInstanceRequest eurekaServiceInstanceRequest = EurekaServiceInstanceRequest.builder() - .serviceId(serviceId) - .eurekaRequestUrl(discoveryServiceLocatorUrl) - .username(eurekaUsername) - .password(eurekaUserPassword) - .build(); - eurekaServiceInstanceRequests.add(eurekaServiceInstanceRequest); - } - - return eurekaServiceInstanceRequests; - } - - /** - * Create HTTP headers - * - * @return HTTP Headers - */ - private List
createRequestHeader(EurekaServiceInstanceRequest eurekaServiceInstanceRequest) { - List
headers = new ArrayList<>(); - if (eurekaServiceInstanceRequest != null && eurekaServiceInstanceRequest.getUsername() != null && eurekaServiceInstanceRequest.getPassword() != null) { - String basicToken = "Basic " + Base64.getEncoder().encodeToString((eurekaServiceInstanceRequest.getUsername() + ":" - + eurekaServiceInstanceRequest.getPassword()).getBytes()); - headers.add(new BasicHeader(HttpHeaders.AUTHORIZATION, basicToken)); - } - headers.add(new BasicHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); - headers.add(new BasicHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)); - return headers; - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/model/ApiDocInfo.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/ApiDocInfo.java similarity index 88% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/model/ApiDocInfo.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/ApiDocInfo.java index 462eac0d84..e92ad876e0 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/model/ApiDocInfo.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/ApiDocInfo.java @@ -8,13 +8,14 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.cached.model; +package org.zowe.apiml.apicatalog.model; -import org.zowe.apiml.config.ApiInfo; -import org.zowe.apiml.product.routing.RoutedServices; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.ToString; +import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.product.routing.RoutedServices; /** * Contains all necessary information to create API Documentation @@ -23,8 +24,12 @@ @Data @AllArgsConstructor @ToString +@Builder public class ApiDocInfo { + ApiInfo apiInfo; String apiDocContent; RoutedServices routes; + boolean local; + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/SemanticVersion.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/SemanticVersion.java index ae05f4692c..50fcdb8a0a 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/SemanticVersion.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/SemanticVersion.java @@ -15,6 +15,7 @@ @EqualsAndHashCode(callSuper = false) public class SemanticVersion implements Comparable { + @NonNull private final int[] numbers; @@ -38,4 +39,5 @@ public int compareTo(@NonNull SemanticVersion another) { } return 0; } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/OidcUtils.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/oidc/OidcUtils.java similarity index 84% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/OidcUtils.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/oidc/OidcUtils.java index 3c34fbf496..919aeab149 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/OidcUtils.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/oidc/OidcUtils.java @@ -8,12 +8,12 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.security; +package org.zowe.apiml.apicatalog.oidc; import lombok.experimental.UtilityClass; -import org.apache.commons.lang3.StringUtils; import java.util.List; +import java.util.Objects; @UtilityClass public class OidcUtils { @@ -22,7 +22,8 @@ public class OidcUtils { public List getOidcProvider() { return System.getenv().keySet().stream() - .filter(key -> StringUtils.startsWith(key, PREFIX)) + .filter(Objects::nonNull) + .filter(key -> key.startsWith(PREFIX)) .map(key -> key.substring(PREFIX.length())) .map(key -> key.split("_")) .filter(parts -> parts.length > 2) diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandler.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandler.java index 130f47b59b..2283bada66 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandler.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandler.java @@ -10,52 +10,47 @@ package org.zowe.apiml.apicatalog.security; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import org.springframework.web.server.WebSession; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import reactor.core.publisher.Mono; /** * Handles logout success by removing cookie and clearing security context */ @RequiredArgsConstructor -public class ApiCatalogLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { +public class ApiCatalogLogoutSuccessHandler implements ServerLogoutSuccessHandler { private final AuthConfigurationProperties authConfigurationProperties; /** * Clears cookie, session, context and sets response code * - * @param httpServletRequest Http request - * @param httpServletResponse Http response + * @param exchange Request exchange * @param authentication Valid authentication */ @Override - public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, - Authentication authentication) { - HttpSession session = httpServletRequest.getSession(false); - if (session != null) { - session.invalidate(); - } - httpServletResponse.setStatus(HttpServletResponse.SC_OK); - - // Set the cookie to null and expired - Cookie tokenCookie = new Cookie(authConfigurationProperties.getCookieProperties().getCookieName(), null); - tokenCookie.setPath(authConfigurationProperties.getCookieProperties().getCookiePath()); - tokenCookie.setSecure(true); - tokenCookie.setHttpOnly(true); - tokenCookie.setMaxAge(0); - httpServletResponse.addCookie(tokenCookie); - - SecurityContext context = SecurityContextHolder.getContext(); - context.setAuthentication(null); - SecurityContextHolder.clearContext(); + public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) { + return exchange.getExchange().getSession() + .flatMap(WebSession::invalidate) + .then(Mono.defer(() -> { + var response = exchange.getExchange().getResponse(); + response.addCookie(ResponseCookie.from(authConfigurationProperties.getCookieProperties().getCookieName()) + .path(authConfigurationProperties.getCookieProperties().getCookiePath()) + .secure(true) + .httpOnly(true) + .maxAge(0L) + .build() + ); + response.setStatusCode(HttpStatusCode.valueOf(HttpServletResponse.SC_OK)); + return Mono.empty(); + })); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java deleted file mode 100644 index 5fa7838b50..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.security; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.zowe.apiml.filter.AttlsFilter; -import org.zowe.apiml.filter.SecureConnectionFilter; -import org.zowe.apiml.security.client.EnableApimlAuth; -import org.zowe.apiml.security.client.login.GatewayLoginProvider; -import org.zowe.apiml.security.client.token.GatewayTokenProvider; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.config.CertificateAuthenticationProvider; -import org.zowe.apiml.security.common.config.HandlerInitializer; -import org.zowe.apiml.security.common.config.SafSecurityConfigurationProperties; -import org.zowe.apiml.security.common.content.BasicContentFilter; -import org.zowe.apiml.security.common.content.BearerContentFilter; -import org.zowe.apiml.security.common.content.CookieContentFilter; -import org.zowe.apiml.security.common.content.OidcContentFilter; -import org.zowe.apiml.security.common.filter.CategorizeCertsFilter; -import org.zowe.apiml.security.common.login.LoginFilter; -import org.zowe.apiml.security.common.login.ShouldBeAlreadyAuthenticatedFilter; -import org.zowe.apiml.security.common.verify.CertificateValidator; - -import java.util.Collections; -import java.util.Set; - -/** - * Main configuration class of Spring web security for Api Catalog - * binds authentication managers - * configures ignores for static content - * adds endpoints and secures them - * adds security filters - */ -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -@EnableApimlAuth -@EnableMethodSecurity -@EnableConfigurationProperties(SafSecurityConfigurationProperties.class) -@ConditionalOnProperty(value = "apiml.catalog.standalone.enabled", havingValue = "false", matchIfMissing = true) -public class SecurityConfiguration { - private static final String APIDOC_ROUTES = "/apidoc/**"; - private static final String STATIC_REFRESH_ROUTE = "/static-api/refresh"; - - private final ObjectMapper securityObjectMapper; - private final AuthConfigurationProperties authConfigurationProperties; - private final HandlerInitializer handlerInitializer; - private final GatewayLoginProvider gatewayLoginProvider; - private final GatewayTokenProvider gatewayTokenProvider; - private final CertificateValidator certificateValidator; - @Qualifier("publicKeyCertificatesBase64") - private final Set publicKeyCertificatesBase64; - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; - @Value("${apiml.health.protected:true}") - private boolean isHealthEndpointProtected; - /** - * Filter chain for protecting /apidoc/** endpoints with MF credentials for client certificate. - */ - @Configuration - @Order(1) - public class FilterChainBasicAuthOrTokenOrCertForApiDoc { - - @Value("${apiml.security.ssl.verifySslCertificatesOfServices:true}") - private boolean verifySslCertificatesOfServices; - - @Value("${apiml.security.ssl.nonStrictVerifySslCertificatesOfServices:false}") - private boolean nonStrictVerifySslCertificatesOfServices; - - @Bean - public SecurityFilterChain basicAuthOrTokenOrCertApiDocFilterChain(HttpSecurity http) throws Exception { - mainframeCredentialsConfiguration( - baseConfiguration(http.securityMatchers(matchers -> matchers.requestMatchers(APIDOC_ROUTES, STATIC_REFRESH_ROUTE))) - ) - .authorizeHttpRequests(requests -> requests - .anyRequest() - .authenticated()) - .authenticationProvider(gatewayLoginProvider) - .authenticationProvider(gatewayTokenProvider) - .authenticationProvider(new CertificateAuthenticationProvider()); - - if (verifySslCertificatesOfServices || !nonStrictVerifySslCertificatesOfServices) { - if (isAttlsEnabled) { - http.x509(x509 -> x509 - .userDetailsService(x509UserDetailsService())) - .addFilterBefore(reversedCategorizeCertFilter(), X509AuthenticationFilter.class) - .addFilterBefore(new AttlsFilter(), X509AuthenticationFilter.class) - .addFilterBefore(new SecureConnectionFilter(), AttlsFilter.class); - } else { - http.x509(x509 -> x509 - .userDetailsService(x509UserDetailsService())); - } - } - - return http.build(); - } - - private UserDetailsService x509UserDetailsService() { - return username -> new User(username, "", Collections.emptyList()); - } - - private CategorizeCertsFilter reversedCategorizeCertFilter() { - CategorizeCertsFilter out = new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator); - out.setCertificateForClientAuth(crt -> out.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); - out.setApimlCertificate(crt -> !out.getPublicKeyCertificatesBase64().contains(CategorizeCertsFilter.base64EncodePublicKey(crt))); - return out; - } - } - - /** - * Default filter chain to protect all routes with MF credentials. - */ - @Configuration - @Order(2) - public class FilterChainBasicAuthOrTokenAllEndpoints { - - @Bean - public WebSecurityCustomizer webSecurityCustomizer() { - String[] noSecurityAntMatchers = { - "/", - "/static/**", - "/favicon.ico", - "/v3/api-docs", - "/index.html", - "/application/info", - "/oidc/provider" - }; - return web -> web.ignoring().requestMatchers(noSecurityAntMatchers); - } - - @Bean - public SecurityFilterChain basicAuthOrTokenAllEndpointsFilterChain(HttpSecurity http) throws Exception { - - if (isHealthEndpointProtected) { - http.authorizeHttpRequests(requests -> requests - .requestMatchers("/application/health").authenticated()); - } else { - http.authorizeHttpRequests(requests -> requests - .requestMatchers("/application/health").permitAll()); - } - - mainframeCredentialsConfiguration(baseConfiguration(http.securityMatchers(matchers -> matchers.requestMatchers("/static-api/**","/containers/**","/application/**","/services/**",APIDOC_ROUTES)))) - .authorizeHttpRequests(requests -> requests - .anyRequest().authenticated() - ) - .authenticationProvider(gatewayLoginProvider) - .authenticationProvider(gatewayTokenProvider); - if (isAttlsEnabled) { - http.addFilterBefore(new SecureConnectionFilter(), UsernamePasswordAuthenticationFilter.class); - } - return http.build(); - } - } - - private HttpSecurity baseConfiguration(HttpSecurity http) throws Exception { - http.csrf(AbstractHttpConfigurer::disable) // NOSONAR - - .headers(httpSecurityHeadersConfigurer -> - httpSecurityHeadersConfigurer.httpStrictTransportSecurity(HeadersConfigurer.HstsConfig::disable) - .frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) - - .exceptionHandling(handling -> handling - - .defaultAuthenticationEntryPointFor( - handlerInitializer.getBasicAuthUnauthorizedHandler(), new AntPathRequestMatcher("/application/**") - ) - .defaultAuthenticationEntryPointFor( - handlerInitializer.getBasicAuthUnauthorizedHandler(), new AntPathRequestMatcher(APIDOC_ROUTES) - ) - .defaultAuthenticationEntryPointFor( - handlerInitializer.getBasicAuthUnauthorizedHandler(), new AntPathRequestMatcher(STATIC_REFRESH_ROUTE) - ) - .defaultAuthenticationEntryPointFor( - handlerInitializer.getUnAuthorizedHandler(), new AntPathRequestMatcher("/**") - )) - .sessionManagement(management -> management - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - - return http; - } - - private HttpSecurity mainframeCredentialsConfiguration(HttpSecurity http) throws Exception { - http.securityMatchers(matcher -> matcher.requestMatchers(HttpMethod.POST, authConfigurationProperties.getServiceLoginEndpoint())) - // login endpoint - .authorizeHttpRequests(requests -> requests - .requestMatchers(HttpMethod.POST, authConfigurationProperties.getServiceLoginEndpoint()).permitAll()); - - http.securityMatchers(matcher -> matcher.requestMatchers(HttpMethod.POST, authConfigurationProperties.getServiceLogoutEndpoint())) - // logout endpoint - .logout(logout -> logout - .logoutUrl(authConfigurationProperties.getServiceLogoutEndpoint()) - .logoutSuccessHandler(logoutSuccessHandler()) - ).with(new CustomSecurityFilters(), c -> { }); - - return http; - } - - private class CustomSecurityFilters extends AbstractHttpConfigurer { - @Override - public void configure(HttpSecurity http) throws Exception { - AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); - - http.addFilterBefore(new ShouldBeAlreadyAuthenticatedFilter(authConfigurationProperties.getServiceLoginEndpoint(), handlerInitializer.getAuthenticationFailureHandler()), UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(loginFilter(authConfigurationProperties.getServiceLoginEndpoint(), authenticationManager), ShouldBeAlreadyAuthenticatedFilter.class) - .addFilterBefore(basicFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class) - .addFilterAfter(cookieFilter(authenticationManager), BasicContentFilter.class) - .addFilterAfter(bearerContentFilter(authenticationManager), CookieContentFilter.class) - .addFilterAfter(oidcFilter(authenticationManager), BearerContentFilter.class); - } - - private LoginFilter loginFilter(String loginEndpoint, AuthenticationManager authenticationManager) { - return new LoginFilter( - loginEndpoint, - handlerInitializer.getSuccessfulLoginHandler(), - handlerInitializer.getAuthenticationFailureHandler(), - securityObjectMapper, - authenticationManager, - handlerInitializer.getResourceAccessExceptionHandler() - ); - } - - /** - * Secures content with a basic authentication - */ - private BasicContentFilter basicFilter(AuthenticationManager authenticationManager) { - return new BasicContentFilter( - authenticationManager, - handlerInitializer.getAuthenticationFailureHandler(), - handlerInitializer.getResourceAccessExceptionHandler() - ); - } - - /** - * Secures content with a token stored in a cookie - */ - private CookieContentFilter cookieFilter(AuthenticationManager authenticationManager) { - return new CookieContentFilter( - authenticationManager, - handlerInitializer.getAuthenticationFailureHandler(), - handlerInitializer.getResourceAccessExceptionHandler(), - authConfigurationProperties); - } - - /** - * Secures content with a Bearer token - */ - private BearerContentFilter bearerContentFilter(AuthenticationManager authenticationManager) { - return new BearerContentFilter( - authenticationManager, - handlerInitializer.getAuthenticationFailureHandler(), - handlerInitializer.getResourceAccessExceptionHandler() - ); - } - - /** - * Secures content with a OIDC token - */ - private OidcContentFilter oidcFilter(AuthenticationManager authenticationManager) { - return new OidcContentFilter( - authenticationManager, - handlerInitializer.getAuthenticationFailureHandler(), - handlerInitializer.getResourceAccessExceptionHandler() - ); - } - - } - - @Bean - public LogoutSuccessHandler logoutSuccessHandler() { - return new ApiCatalogLogoutSuccessHandler(authConfigurationProperties); - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocService.java deleted file mode 100644 index 843d34f473..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocService.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.cached; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocCacheKey; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.services.status.APIDocRetrievalService; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; -import org.zowe.apiml.apicatalog.services.status.model.ApiVersionNotFoundException; -import org.zowe.apiml.apicatalog.swagger.TransformApiDocService; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; - -/** - * Caching service for API Doc Info - */ - -@Service -@Slf4j -public class CachedApiDocService { - public static final String DEFAULT_API_KEY = "default"; - - private static final Map serviceApiDocs = new HashMap<>(); - private static final Map> serviceApiVersions = new HashMap<>(); - private static final Map serviceApiDefaultVersions = new HashMap<>(); - - private final APIDocRetrievalService apiDocRetrievalService; - private final TransformApiDocService transformApiDocService; - - private static final UnaryOperator exceptionMessage = serviceId -> "No API Documentation was retrieved for the service " + serviceId + "."; - - public CachedApiDocService(APIDocRetrievalService apiDocRetrievalService, TransformApiDocService transformApiDocService) { - this.apiDocRetrievalService = apiDocRetrievalService; - this.transformApiDocService = transformApiDocService; - } - - /** - * Fetches API documentation for a given service and version. - * - * @param serviceId service identifier - * @param cacheKeySuffix version or default API key - * @param retrievalLogic supplier providing the API documentation retrieval logic - * @return the API documentation - */ - private String fetchApiDoc( - final String serviceId, - final String cacheKeySuffix, - final Supplier retrievalLogic - ) { - ApiDocCacheKey cacheKey = new ApiDocCacheKey(serviceId, cacheKeySuffix); - String errorMessage = ""; - Exception exception = null; - - // Try to fetch API doc from the retrieval logic - try { - ApiDocInfo apiDocInfo = retrievalLogic.get(); - if (apiDocInfo != null && apiDocInfo.getApiDocContent() != null) { - String apiDoc = transformApiDocService.transformApiDoc(serviceId, apiDocInfo); - CachedApiDocService.serviceApiDocs.put(cacheKey, apiDoc); - return apiDoc; - } else { - log.debug("No API Documentation found for the service {}", serviceId); - } - } catch (Exception e) { - log.debug("Exception updating API doc in cache for '{} {}'", serviceId, cacheKeySuffix, e); - errorMessage = e.getMessage(); - exception = e; - } - - // If no DS is available, try to use cached data - String apiDoc = CachedApiDocService.serviceApiDocs.get(cacheKey); - if (apiDoc != null) { - log.debug("Using cached API doc for service '{}'", serviceId); - return apiDoc; - } - - // Cannot obtain API doc, throw exception - log.error("No API doc available for '{} {}'", serviceId, cacheKeySuffix); - throw new ApiDocNotFoundException( - exceptionMessage.apply(serviceId) + " Root cause: " + errorMessage, exception - ); - } - - /** - * Update the api docs for this service - * - * @param serviceId service identifier - * @param apiVersion the version of the API - * @return api doc info for the requested service id - */ - public String getApiDocForService(final String serviceId, final String apiVersion) { - return fetchApiDoc( - serviceId, - apiVersion, - () -> apiDocRetrievalService.retrieveApiDoc(serviceId, apiVersion) - ); - } - - /** - * Update the api docs for this service - * This method should be executed if a new version of a service is discovered on renewal - * - * @param serviceId service identifier - * @param apiVersion the version of the API - * @param apiDoc API Doc info - */ - public void updateApiDocForService(final String serviceId, final String apiVersion, final String apiDoc) { - CachedApiDocService.serviceApiDocs.put(new ApiDocCacheKey(serviceId, apiVersion), apiDoc); - } - - /** - * Update the docs for the latest API version for this service - * - * @param serviceId service identifier - * @return api doc info for the latest API of the request service id - */ - public String getDefaultApiDocForService(final String serviceId) { - return fetchApiDoc( - serviceId, - DEFAULT_API_KEY, - () -> apiDocRetrievalService.retrieveDefaultApiDoc(serviceId) - ); - } - - /** - * Update the latest version api doc for this service. - * THis method should be executed if a new version of a service is discovered on renewal - * - * @param serviceId service identifier - * @param apiDoc API Doc info - */ - public void updateDefaultApiDocForService(final String serviceId, final String apiDoc) { - CachedApiDocService.serviceApiDocs.put(new ApiDocCacheKey(serviceId, DEFAULT_API_KEY), apiDoc); - } - - /** - * Update the api versions for this service - * - * @param serviceId service identifier - * @return List of API version strings for the requested service ID - */ - public List getApiVersionsForService(final String serviceId) { - // First try to fetch apiDoc from the DS - try { - List versions = apiDocRetrievalService.retrieveApiVersions(serviceId); - if (!versions.isEmpty()) { - CachedApiDocService.serviceApiVersions.put(serviceId, versions); - return versions; - } - } catch (Exception e) { - log.debug("Exception updating API versions in cache for {}", serviceId, e); - } - - // if no DS is available try to use cached data - List versions = CachedApiDocService.serviceApiVersions.get(serviceId); - if (versions != null) { - return versions; - } - - // Cannot obtain API doc, end with exception - log.error("No API versions available for service '{}'", serviceId); - throw new ApiVersionNotFoundException("No API versions were retrieved for the service " + serviceId + "."); - } - - /** - * Update the api versions for this service. - * This method should be executed if a new version of a service is discovered on renewal. - * - * @param serviceId service identifier - * @param apiVersions the API versions - */ - public void updateApiVersionsForService(final String serviceId, final List apiVersions) { - CachedApiDocService.serviceApiVersions.put(serviceId, apiVersions); - } - - /** - * Update the default API version for this service. - * - * @param serviceId service identifier - * @return default API version for given service id - */ - public String getDefaultApiVersionForService(final String serviceId) { - // First try to fetch apiDoc from the DS - try { - String version = apiDocRetrievalService.retrieveDefaultApiVersion(serviceId); - if (version != null) { - CachedApiDocService.serviceApiDefaultVersions.put(serviceId, version); - return version; - } - } catch (Exception e) { - log.error("No default API version available for service '{}'", serviceId, e); - } - - // if no DS is available try to use cached data - String version = CachedApiDocService.serviceApiDefaultVersions.get(serviceId); - if (version != null) { - return version; - } - - // cannot obtain apiDoc ends with exception - throw new ApiVersionNotFoundException("Error trying to find default API version"); - } - - /** - * Update the default api version for this service. - * This method should be executed if a new version of a service is discovered on renewal. - * - * @param serviceId service identifier - * @param apiVersion the default API version - */ - public void updateDefaultApiVersionForService(final String serviceId, final String apiVersion) { - CachedApiDocService.serviceApiDefaultVersions.put(serviceId, apiVersion); - } - - /** - * Reset the cache for this service - */ - public void resetCache() { - serviceApiDocs.clear(); - serviceApiVersions.clear(); - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java deleted file mode 100644 index 0c807fdba4..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java +++ /dev/null @@ -1,484 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.cached; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.model.APIService; -import org.zowe.apiml.apicatalog.model.CustomStyleConfig; -import org.zowe.apiml.auth.Authentication; -import org.zowe.apiml.auth.AuthenticationSchemes; -import org.zowe.apiml.config.ApiInfo; -import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; -import org.zowe.apiml.product.routing.RoutedServices; -import org.zowe.apiml.product.routing.ServiceType; -import org.zowe.apiml.product.routing.transform.TransformService; -import org.zowe.apiml.product.routing.transform.URLTransformationException; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; -import static org.zowe.apiml.product.constants.CoreService.GATEWAY; - -/** - * Caching service for eureka services - */ -@Slf4j -@Service -public class CachedProductFamilyService { - - private static final String DEFAULT_APIINFO_KEY = "default"; - - @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); - - private final Integer cacheRefreshUpdateThresholdInMillis; - - private final CachedServicesService cachedServicesService; - private final EurekaMetadataParser metadataParser = new EurekaMetadataParser(); - private final TransformService transformService; - - private final Map products = new ConcurrentHashMap<>(); - @Getter - private final Map services = new ConcurrentHashMap<>(); - - private final AuthenticationSchemes schemes = new AuthenticationSchemes(); - private final CustomStyleConfig customStyleConfig; - - @Value("${apiml.catalog.hide.serviceInfo:false}") - private boolean hideServiceInfo; - - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; - - public CachedProductFamilyService(CachedServicesService cachedServicesService, - TransformService transformService, - @Value("${apiml.service-registry.cacheRefreshUpdateThresholdInMillis}") Integer cacheRefreshUpdateThresholdInMillis, - CustomStyleConfig customStyleConfig) { - this.cachedServicesService = cachedServicesService; - this.transformService = transformService; - this.cacheRefreshUpdateThresholdInMillis = cacheRefreshUpdateThresholdInMillis; - this.customStyleConfig = customStyleConfig; - } - - /** - * Return all cached service instances - * - * @return instances - */ - public Collection getAllContainers() { - return products.values(); - } - - - /** - * return cached service instance by id - * - * @param id service identifier - * @return {@link APIContainer} - */ - public APIContainer getContainerById(String id) { - return products.get(id); - } - - /** - * Retrieve any containers which have had their details updated after the threshold figure - * If performance is slow then possibly cache the result and evict after 'n' seconds - * - * @return recently updated containers - */ - public List getRecentlyUpdatedContainers() { - return this.products.values().stream().filter( - container -> { - boolean isRecent = container.isRecentUpdated(cacheRefreshUpdateThresholdInMillis); - if (isRecent) { - log.debug("Container: " + container.getId() + " last updated: " - + container.getLastUpdatedTimestamp().getTime() + - " was updated recently"); - } - return isRecent; - }).toList(); - } - - /** - * Add service to container - * - * @param productFamilyId the service identifier - * @param instanceInfo InstanceInfo - */ - public void addServiceToContainer(final String productFamilyId, final InstanceInfo instanceInfo) { - APIContainer apiContainer = products.get(productFamilyId); - // fix - throw error if null - apiContainer.addService(createAPIServiceFromInstance(instanceInfo)); - products.put(productFamilyId, apiContainer); - } - - /** - * Save a containers details using a service's metadata - * - * @param productFamilyId the product family id of the container - * @param instanceInfo the service instance - */ - public APIContainer saveContainerFromInstance(String productFamilyId, InstanceInfo instanceInfo) { - APIContainer container = products.get(productFamilyId); - if (container == null) { - container = createNewContainerFromService(productFamilyId, instanceInfo); - } else { - Set apiServices = container.getServices(); - APIService service = createAPIServiceFromInstance(instanceInfo); - - // Verify whether already exists - if (apiServices.contains(service)) { - apiServices.stream() - .filter(existingService -> existingService.equals(service)) - .forEach(existingService -> { - if (!existingService.getInstances().contains(instanceInfo.getInstanceId())) { - existingService.getInstances().add(instanceInfo.getInstanceId()); - } - }); // If the instance is in list, do nothing otherwise - } else { - apiServices.add(service); - } - - container.setServices(apiServices); - //update container - String versionFromInstance = instanceInfo.getMetadata().get(CATALOG_VERSION); - String title = instanceInfo.getMetadata().get(CATALOG_TITLE); - String description = instanceInfo.getMetadata().get(CATALOG_DESCRIPTION); - - container.setVersion(versionFromInstance); - container.setTitle(title); - container.setDescription(description); - container.updateLastUpdatedTimestamp(); - - products.put(productFamilyId, container); - } - - return container; - } - - /** - * Remove Instance which isn't available anymore. Based on what service the instance belongs to: - * 1) it will remove the whole APIContainer (Tile) if there is no instance of any service remaining - * 2) Remove the service from the containe if there is no instance of service remaining - * 3) Remove instance from the service - * - * @param removedInstanceFamilyId the product family id of the container - * @param removedInstance the service instance - */ - public void removeInstance(String removedInstanceFamilyId, InstanceInfo removedInstance) { - APIContainer containerWithInstance = products.get(removedInstanceFamilyId); - // There is nothing to do. - if (containerWithInstance == null) { - log.info("Remove product with id: {} instance {}", removedInstanceFamilyId, removedInstance.getInstanceId()); - return; - } - - APIService toBeRemoved = createAPIServiceFromInstance(removedInstance); - - Set currentServices = containerWithInstance.getServices(); - AtomicBoolean removeFullService = new AtomicBoolean(false); - currentServices.stream() - .filter(existingService -> existingService.equals(toBeRemoved)) - .forEach(existingService -> { - if (existingService.getInstances().size() == 1) { - removeFullService.set(true); - } else { - // Only remove one of the instances - existingService.getInstances().remove(removedInstance.getInstanceId()); - } - }); - - // Remove at least the full service - if (removeFullService.get()) { - currentServices.remove(toBeRemoved); - - // Remove the whole container (tile) - if (currentServices.isEmpty()) { - products.remove(removedInstanceFamilyId); - } - } - } - - /** - * Remove Instance which isn't available anymore. Based on what service the instance belongs to: - * 1) it will remove the whole APIContainer (Tile) if there is no instance of any service remaining - * 2) Remove the service from the containe if there is no instance of service remaining - * 3) Remove instance from the service - * - * @param removedInstance the service instance - */ - public void removeInstanceFromServices(InstanceInfo removedInstance) { - var serviceId = removedInstance.getAppName(); - var currentService = services.get(serviceId); - // There is nothing to do. - if (currentService == null) { - log.info("Service with id: {} instance {} not found", serviceId, removedInstance.getInstanceId()); - return; - } - if (currentService.getInstances().size() == 1) { - services.remove(serviceId); - } else { - currentService.getInstances().remove(removedInstance.getInstanceId()); - services.put(serviceId, currentService); - } - - } - - /** - * Update the summary totals, sso and API IDs info for a container based on it's running services - * - * @param apiContainer calculate totals for this container - */ - public void calculateContainerServiceValues(APIContainer apiContainer) { - if (apiContainer.getServices() == null) { - apiContainer.setServices(new HashSet<>()); - } - - int servicesCount = apiContainer.getServices().size(); - int activeServicesCount = 0; - boolean isSso = servicesCount > 0; - for (APIService apiService : apiContainer.getServices()) { - if (update(apiService)) { - activeServicesCount++; - } - isSso &= apiService.isSsoAllInstances(); - } - - setStatus(apiContainer, servicesCount, activeServicesCount); - apiContainer.setSso(isSso); - apiContainer.setHideServiceInfo(hideServiceInfo); - - // set metadata to customize the UI - if (customStyleConfig != null) { - setCustomUiConfig(apiContainer); - } - - } - - /** - * Map the configuration to customize the Catalog UI to the container - * - * @param apiContainer - */ - private void setCustomUiConfig(APIContainer apiContainer) { - apiContainer.setCustomStyleConfig(customStyleConfig); - } - - /** - * Return the number of containers (used for checking if a new container was created) - * - * @return the number of containers - */ - public int getContainerCount() { - return products.size(); - } - - /** - * Try to transform the service homepage url and return it. If it fails, - * return the original homepage url - * - * @param instanceInfo the service instance - * @return the transformed homepage url - */ - private String getInstanceHomePageUrl(InstanceInfo instanceInfo) { - String instanceHomePage = instanceInfo.getHomePageUrl(); - - //Gateway homePage is used to hold DVIPA address and must not be modified - if (hasHomePage(instanceInfo) && !StringUtils.equalsIgnoreCase(GATEWAY.getServiceId(), instanceInfo.getAppName())) { - instanceHomePage = instanceHomePage.trim(); - RoutedServices routes = metadataParser.parseRoutes(instanceInfo.getMetadata()); - try { - instanceHomePage = transformService.transformURL( - ServiceType.UI, - instanceInfo.getVIPAddress(), - instanceHomePage, - routes, - isAttlsEnabled); - } catch (URLTransformationException | IllegalArgumentException e) { - apimlLog.log("org.zowe.apiml.apicatalog.homePageTransformFailed", instanceInfo.getAppName(), e.getMessage()); - } - } - - log.debug("Homepage URL for {} service is: {}", instanceInfo.getVIPAddress(), instanceHomePage); - return instanceHomePage; - } - - /** - * Get the base path for the service. - * - * @param instanceInfo the service instance - * @return the base URL - */ - private String getApiBasePath(InstanceInfo instanceInfo) { - if (hasHomePage(instanceInfo)) { - try { - String apiBasePath = instanceInfo.getMetadata().get("apiml.apiBasePath"); - if (apiBasePath != null) { - return apiBasePath; - } - - RoutedServices routes = metadataParser.parseRoutes(instanceInfo.getMetadata()); - return transformService.retrieveApiBasePath( - instanceInfo.getVIPAddress(), - instanceInfo.getHomePageUrl(), - routes); - } catch (URLTransformationException e) { - apimlLog.log("org.zowe.apiml.apicatalog.getApiBasePathFailed", instanceInfo.getAppName(), e.getMessage()); - } - } - return ""; - } - - private boolean hasHomePage(InstanceInfo instanceInfo) { - String instanceHomePage = instanceInfo.getHomePageUrl(); - return instanceHomePage != null - && !instanceHomePage.isEmpty(); - } - - /** - * Create a new container based on information in a new instance - * - * @param productFamilyId parent id - * @param instanceInfo instance - * @return a new container - */ - private APIContainer createNewContainerFromService(String productFamilyId, InstanceInfo instanceInfo) { - Map instanceInfoMetadata = instanceInfo.getMetadata(); - String title = instanceInfoMetadata.get(CATALOG_TITLE); - String description = instanceInfoMetadata.get(CATALOG_DESCRIPTION); - String version = instanceInfoMetadata.get(CATALOG_VERSION); - APIContainer container = new APIContainer(); - container.setStatus("UP"); - container.setId(productFamilyId); - container.setDescription(description); - container.setTitle(title); - container.setVersion(version); - log.debug("updated Container cache with product family: " + productFamilyId + ": " + title); - - // create API Service from instance and update container last changed date - container.addService(createAPIServiceFromInstance(instanceInfo)); - products.put(productFamilyId, container); - return container; - } - - public void addService(InstanceInfo instanceInfo) { - var serviceInfo = createAPIServiceFromInstance(instanceInfo); - services.put(serviceInfo.getServiceId(), serviceInfo); - } - - /** - * Create a APIService object using the instances metadata - * - * @param instanceInfo the service instance - * @return a APIService object - */ - APIService createAPIServiceFromInstance(InstanceInfo instanceInfo) { - boolean secureEnabled = instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE); - - String instanceHomePage = getInstanceHomePageUrl(instanceInfo); - String apiBasePath = getApiBasePath(instanceInfo); - Map apiInfoById = new HashMap<>(); - - try { - List apiInfoList = metadataParser.parseApiInfo(instanceInfo.getMetadata()); - apiInfoList.stream().filter(apiInfo -> apiInfo.getApiId() != null).forEach(apiInfo -> { - String id = (apiInfo.getMajorVersion() < 0) ? DEFAULT_APIINFO_KEY : apiInfo.getApiId() + " v" + apiInfo.getVersion(); - apiInfoById.put(id, apiInfo); - }); - if (!apiInfoById.containsKey(DEFAULT_APIINFO_KEY)) { - ApiInfo defaultApiInfo = apiInfoList.stream().filter(ApiInfo::isDefaultApi).findFirst().orElse(null); - apiInfoById.put(DEFAULT_APIINFO_KEY, defaultApiInfo); - } - } catch (Exception ex) { - log.info("createApiServiceFromInstance#incorrectVersions {}", ex.getMessage()); - } - - String serviceId = instanceInfo.getAppName(); - String title = instanceInfo.getMetadata().get(SERVICE_TITLE); - if (StringUtils.equalsIgnoreCase(GATEWAY.getServiceId(), serviceId)) { - if (RegistrationType.of(instanceInfo.getMetadata()).isAdditional()) { - // additional registration for GW means domain one, update serviceId and basePath with the ApimlId - String apimlId = instanceInfo.getMetadata().get(APIML_ID); - if (apimlId != null) { - serviceId = apimlId; - apiBasePath = String.join("/", "", serviceId.toLowerCase()); - title += " (" + apimlId + ")"; - } - } else { - apiBasePath = "/"; - } - } - - return new APIService.Builder(StringUtils.lowerCase(serviceId)) - .title(title) - .description(instanceInfo.getMetadata().get(SERVICE_DESCRIPTION)) - .tileDescription(instanceInfo.getMetadata().get(CATALOG_DESCRIPTION)) - .secured(secureEnabled) - .baseUrl(instanceInfo.getHomePageUrl()) - .homePageUrl(instanceHomePage) - .basePath(apiBasePath) - .sso(isSso(instanceInfo)) - .apis(apiInfoById) - .instanceId(instanceInfo.getInstanceId()) - .build(); - } - - private boolean isSso(InstanceInfo instanceInfo) { - Map eurekaMetadata = instanceInfo.getMetadata(); - return Authentication.builder() - .scheme(schemes.map(eurekaMetadata.get(AUTHENTICATION_SCHEME))) - .supportsSso(BooleanUtils.toBooleanObject(eurekaMetadata.get(AUTHENTICATION_SSO))) - .build() - .supportsSso(); - } - - private boolean update(APIService apiService) { - Application application = cachedServicesService.getService(apiService.getServiceId()); - // service has not cached yet, but count as alive - if (application == null) return true; - - List instancies = application.getInstances(); - boolean isUp = instancies.stream().anyMatch(i -> InstanceInfo.InstanceStatus.UP.equals(i.getStatus())); - boolean isSso = instancies.stream().allMatch(this::isSso); - - apiService.setStatus(isUp ? "UP" : "DOWN"); - apiService.setSsoAllInstances(isSso); - - return isUp; - } - - private void setStatus(APIContainer apiContainer, int servicesCount, int activeServicesCount) { - apiContainer.setTotalServices(servicesCount); - apiContainer.setActiveServices(activeServicesCount); - - if (activeServicesCount == 0) { - apiContainer.setStatus("DOWN"); - } else if (activeServicesCount == servicesCount) { - apiContainer.setStatus("UP"); - } else { - apiContainer.setStatus("WARNING"); - } - - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesService.java deleted file mode 100644 index fa2392f4a9..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesService.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.cached; - -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import lombok.NonNull; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -/** - * Container for eureka services - */ -@Service -public class CachedServicesService { - - private Map services = new HashMap<>(); - private long versionDelta; - - /** - * return all cached service instances - * @return instances - */ - public Applications getAllCachedServices() { - if (services.isEmpty()) { - return null; - } else { - return new Applications(null, 1L, new ArrayList<>(services.values())); - } - } - - /** - * return all cached service instances - * @param serviceId the service identifier - * @return instances for this service (might be empty instances collection) - */ - public Application getService(@NonNull final String serviceId) { - return services.get(serviceId.toLowerCase()); - } - - /** - * Update this service with the application object - * @param serviceId the service name (lowercase) - * @param application updated application with running instances - */ - public void updateService(@NonNull final String serviceId, final Application application) { - services.put(serviceId.toLowerCase(), application); - } - /** - * Clear the cache and remove all entries from the map - */ - public void clearAllServices() { - services.clear(); - } - - public long getVersionDelta() { - return versionDelta; - } - - public void setVersionDelta(long versionDelta) { - this.versionDelta = versionDelta; - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/model/ApiDocCacheKey.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/model/ApiDocCacheKey.java deleted file mode 100644 index a527f2374e..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/model/ApiDocCacheKey.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.cached.model; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class ApiDocCacheKey { - private String serviceId; - private String apiVersion; -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIServiceStatusService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIServiceStatusService.java deleted file mode 100644 index b0155526b6..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIServiceStatusService.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Applications; -import lombok.AllArgsConstructor; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.openapitools.openapidiff.core.model.ChangedOpenApi; -import org.openapitools.openapidiff.core.output.HtmlRender; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.services.cached.CachedApiDocService; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.apicatalog.services.status.event.model.ContainerStatusChangeEvent; -import org.zowe.apiml.apicatalog.services.status.event.model.STATUS_EVENT_TYPE; -import org.zowe.apiml.apicatalog.services.status.model.ApiDiffNotAvailableException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -@Slf4j -@Service -@AllArgsConstructor -public class APIServiceStatusService { - - private final CachedProductFamilyService cachedProductFamilyService; - private final CachedServicesService cachedServicesService; - private final CachedApiDocService cachedApiDocService; - private final OpenApiCompareProducer openApiCompareProducer; - - /** - * Return a cached snapshot of services and instances as a response - * - * @return Applications from cache - */ - public ResponseEntity getCachedApplicationStateResponse() { - return new ResponseEntity<>(cachedServicesService.getAllCachedServices(), createHeaders(), HttpStatus.OK); - } - - /** - * Return a cached snapshot of services and instances - * - * @return Applications from cache - */ - public Applications getCachedApplicationState() { - return cachedServicesService.getAllCachedServices(); - } - - /** - * Retrieve all containers and return them as events - * - * @return container status as events - */ - public List getContainersStateAsEvents() { - log.debug("Retrieving all containers statuses as events"); - List events = new ArrayList<>(); - Iterable allContainers = cachedProductFamilyService.getAllContainers(); - allContainers.forEach(container -> { - cachedProductFamilyService.calculateContainerServiceValues(container); - addContainerEvent(events, container); - }); - return events; - } - - /** - * Return the cached API docs for a service - * - * @param serviceId the unique service id - * @param apiVersion the version of the API - * @return a version of an API Doc - */ - public ResponseEntity getServiceCachedApiDocInfo(@NonNull String serviceId, String apiVersion) { - return new ResponseEntity<>(cachedApiDocService.getApiDocForService(serviceId, apiVersion), createHeaders(), HttpStatus.OK); - } - - /** - * Return the cached default API doc for a service - * - * @param serviceId the unique service id - * @return the default version of an API Doc - */ - public ResponseEntity getServiceCachedDefaultApiDocInfo(@NonNull String serviceId) { - return new ResponseEntity<>(cachedApiDocService.getDefaultApiDocForService(serviceId), createHeaders(), HttpStatus.OK); - } - - /** - * Return the diff of two api versions - * @param serviceId the unique service id - * @param apiVersion1 the old version of the api - * @param apiVersion2 the new version of the api - * @return response containing HTML document detailing changes between api doc versions - */ - public ResponseEntity getApiDiffInfo(@NonNull String serviceId, String apiVersion1, String apiVersion2) { - try { - String doc1 = cachedApiDocService.getApiDocForService(serviceId, apiVersion1); - String doc2 = cachedApiDocService.getApiDocForService(serviceId, apiVersion2); - ChangedOpenApi diff = openApiCompareProducer.fromContents(doc1, doc2); - HtmlRender render = new HtmlRender(); - String result = render.render(diff); - //Remove external stylesheet - result = result.replace("", ""); - return new ResponseEntity<>(result, createHeaders(), HttpStatus.OK); - } catch (Exception e) { - String errorMessage = String.format("Error retrieving API diff for '%s' with versions '%s' and '%s'", serviceId, apiVersion1, apiVersion2); - log.error(errorMessage, e); - throw new ApiDiffNotAvailableException(errorMessage); - } - } - - /** - * Retrieve all containers which were updated inside a given threshold value and return them as events - * - * @return recent container status as events - */ - public List getRecentlyUpdatedContainersAsEvents() { - List recentEvents = new ArrayList<>(); - Iterable allContainers = cachedProductFamilyService.getRecentlyUpdatedContainers(); - allContainers.forEach(container -> { - cachedProductFamilyService.calculateContainerServiceValues(container); - addContainerEvent(recentEvents, container); - }); - if (!recentEvents.isEmpty()) { - log.debug("Recent events found: " + recentEvents.size()); - } - return recentEvents; - } - - /** - * Create an event based on the status of the instance - * - * @param events the list of events to return - * @param container the instance - */ - private void addContainerEvent(List events, APIContainer container) { - STATUS_EVENT_TYPE eventType; - if (InstanceInfo.InstanceStatus.DOWN.name().equalsIgnoreCase(container.getStatus())) { - eventType = STATUS_EVENT_TYPE.CANCEL; - } else if (container.getCreatedTimestamp().equals(container.getLastUpdatedTimestamp())) { - eventType = STATUS_EVENT_TYPE.CREATED_CONTAINER; - } else { - eventType = STATUS_EVENT_TYPE.RENEW; - } - events.add(new ContainerStatusChangeEvent( - container.getId(), - container.getTitle(), - container.getStatus(), - container.getTotalServices(), - container.getActiveServices(), - container.getServices(), - eventType) - ); - } - -// ============================== HELPER METHODS - - - /** - * HTTP headers - * - * @return headers for requests - */ - private HttpHeaders createHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - return headers; - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/OpenApiCompareProducer.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/OpenApiCompareProducer.java deleted file mode 100644 index 200f97e73d..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/OpenApiCompareProducer.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status; - -import org.openapitools.openapidiff.core.OpenApiCompare; -import org.openapitools.openapidiff.core.model.ChangedOpenApi; -import org.springframework.stereotype.Service; - -@Service -public class OpenApiCompareProducer { - public ChangedOpenApi fromContents(String oldContent, String newContent) { - return OpenApiCompare.fromContents(oldContent, newContent); - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/ContainerStatusChangeEvent.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/ContainerStatusChangeEvent.java deleted file mode 100644 index 05e6b1ab2a..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/ContainerStatusChangeEvent.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status.event.model; - -import org.zowe.apiml.apicatalog.model.APIService; -import lombok.Data; - -import java.util.Set; - -@Data -public class ContainerStatusChangeEvent implements StatusChangeEvent { - private final String containerId; - private final String title; - private final String status; - private final int totalServices; - private final int activeServices; - private Set services; - private final STATUS_EVENT_TYPE statusEventType; - private final String timeStamp; - - public ContainerStatusChangeEvent(String containerId, String title, String status, - int totalServices, int activeServices, - Set services, - STATUS_EVENT_TYPE statusEventType) { - this.containerId = containerId; - this.title = title; - this.status = status; - this.services = services; - this.totalServices = totalServices; - this.activeServices = activeServices; - this.statusEventType = statusEventType; - timeStamp = setTimeStamp(); - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/STATUS_EVENT_TYPE.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/STATUS_EVENT_TYPE.java deleted file mode 100644 index 4593f7e73a..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/STATUS_EVENT_TYPE.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status.event.model; - -public enum STATUS_EVENT_TYPE { - CREATED_CONTAINER, - RENEW, - CANCEL -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/StatusChangeEvent.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/StatusChangeEvent.java deleted file mode 100644 index 6886f0f313..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/event/model/StatusChangeEvent.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status.event.model; - -import java.sql.Timestamp; -import java.text.DateFormat; -import java.time.Instant; -import java.util.TimeZone; - -public interface StatusChangeEvent { - - /** - * Create a String time stamp for this event - * @return a timestamp - */ - default String setTimeStamp() { - Instant now = Instant.now(); - Timestamp current = Timestamp.from(now); - DateFormat df = DateFormat.getDateTimeInstance(); - df.setTimeZone(TimeZone.getTimeZone("UTC")); - return df.format(current); - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListener.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListener.java deleted file mode 100644 index 0985f0de97..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListener.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status.listeners; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.event.EventListener; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.zowe.apiml.apicatalog.instance.InstanceInitializeService; -import org.zowe.apiml.product.gateway.GatewayLookupCompleteEvent; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * This class fires on GatewayLookupCompleteEvent event - * Initializes Catalog instances from Eureka - */ -@Slf4j -@Component -@ConditionalOnProperty( - value = "apiml.catalog.standalone.enabled", - havingValue = "false", - matchIfMissing = true) -@RequiredArgsConstructor -@Order(Ordered.HIGHEST_PRECEDENCE) -public class GatewayLookupEventListener { - - private final InstanceInitializeService instanceInitializeService; - private final AtomicBoolean hasRun = new AtomicBoolean(false); - - @EventListener(GatewayLookupCompleteEvent.class) - public void onApplicationEvent() { - if (!hasRun.get()) { - hasRun.set(true); - try { - instanceInitializeService.retrieveAndRegisterAllInstancesWithCatalog(); - } catch (Exception e) { - hasRun.set(false); - log.debug("Unexpected error occurred while initial retrieving of services: {}", e.getMessage()); - } - } - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInfo.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInfo.java deleted file mode 100644 index ec4cad83d0..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInfo.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status.model; - -import lombok.Data; - -@Data -public class APIServiceInfo { - private String instanceId; - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInstances.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInstances.java deleted file mode 100644 index cd6e9cd46f..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/model/APIServiceInstances.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status.model; - -import lombok.Data; - -import java.util.List; - -@Data -public class APIServiceInstances { - private List instances; -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMock.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMock.java deleted file mode 100644 index ec668752eb..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMock.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.zowe.apiml.product.instance.ServiceAddress; - -@Configuration -@ConditionalOnProperty(value = "apiml.catalog.standalone.enabled", havingValue = "true") -public class ApiDocTransformForMock { - - @Value("${server.hostname:localhost}") - private String hostname; - - @Value("${server.port}") - private String port; - - @Value("${service.schema:https}") - private String schema; - - @Value("${server.servlet.contextPath:/apicatalog}") - private String contextPath; - - @Bean - @Primary - public ServiceAddress gatewayConfigPropertiesForMock() { - String path = this.contextPath.endsWith("/") ? this.contextPath.substring(0, this.contextPath.lastIndexOf('/')) : this.contextPath; - - return ServiceAddress.builder() - .scheme(schema) - .hostname(String.format("%s:%s%s/mock", hostname, port, path)) - .build(); - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ExampleService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ExampleService.java deleted file mode 100644 index c8d2cbdb82..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/ExampleService.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import com.fasterxml.jackson.databind.module.SimpleModule; -import io.swagger.oas.inflector.examples.ExampleBuilder; -import io.swagger.oas.inflector.processors.JsonNodeExampleSerializer; -import io.swagger.parser.OpenAPIParser; -import io.swagger.v3.core.util.Json; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.Paths; -import io.swagger.v3.oas.models.media.Content; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.responses.ApiResponse; -import io.swagger.v3.parser.core.models.SwaggerParseResult; -import lombok.Builder; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; -import org.springframework.util.AntPathMatcher; - -import jakarta.servlet.http.HttpServletResponse; -import org.zowe.apiml.apicatalog.swagger.ApiDocTransformationException; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.*; - -/** - * This class is responsible for generating the examples for API Doc and can simulate the response for any endpoint. It - * is helpful for the standalone mode that mocks the loaded schemas. - * - * This class tries to generate a response for each endpoint in the API documentation. It focuses on the successful - * response with media-type JSON. If it is not possible to generate such content, it uses the default one (an empty - * JSON object). Examples are mapping by ANT matcher and it is possible to respond by this class (see method - * {@link ExampleService#replyExample(HttpServletResponse, String, String)}). - */ -@Slf4j -@Service -@ConditionalOnProperty(value = "apiml.catalog.standalone.enabled", havingValue = "true") -public class ExampleService { - - private static final Example DEFAULT_EXAMPLE = Example.builder() - .responseCode(200).content("{}").mediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE) - .build(); - - /** - * Collection of know responses. The key of the map represents method type. The list itself is all known - * examples of responses for the specific type. It is not possible to use map or a different structure, because - * the mapping uses Ant Patterns. Therefore, it is necessary to test mapping of each URL path. - */ - private static final Map> examples = Collections.synchronizedMap(new HashMap<>()); - - { - // init JSON library to generate example - SimpleModule simpleModule = new SimpleModule().addSerializer(new JsonNodeExampleSerializer()); - Json.mapper().registerModule(simpleModule); - } - - /** - * Construct class {@link ExampleService.Example} and put in {@link #examples} for the future using. - * @param method method of the request - * @param path ANT pattern to match URL path of request - * @param responseCode response code of example - * @param mediaType the media type of the example - * @param content response's content of example - */ - static void addExample(String method, String path, int responseCode, String mediaType, String content) { - Example example = Example.builder() - .method(method).path(path) - .responseCode(responseCode).mediaType(mediaType).content(content) - .build(); - - List byMethod = examples.computeIfAbsent(method, k -> Collections.synchronizedList(new ArrayList<>())); - byMethod.add(example); - - log.debug("Generated response example for request {}, {}, {}, {}:\n{}", method, path, responseCode, mediaType, content); - } - - /** - * Method returns first entry with response code 2xx if exists. Otherwise it return null (in case the operation - * does not contain any record) or the first entry. - * @param operation operation to analyze - * @return entry with successful response if exists, otherwise first entry or null if no one exists - */ - static Map.Entry getFirstApiResponses(Operation operation) { - Map.Entry first = null; - for (Map.Entry responseEntry : operation.getResponses().entrySet()) { - if (responseEntry.getKey().startsWith("2")) return responseEntry; - if (first == null) first = responseEntry; - } - return first; - } - - /** - * Method to translate response code in API doc to number. - * @param input the representation of response code in API doc - * @return translated response code or 0 if the input is unclear - */ - static int getResponseCode(String input) { - if (StringUtils.equals("default", input)) { - return 200; - } - if (StringUtils.isNumeric(input)) { - return Integer.parseInt(input); - } - return 0; - } - - private void generateExample(String serviceId, OpenAPI swagger, String method, Operation operation, String path) { - Map.Entry responseEntry = getFirstApiResponses(operation); - if (responseEntry == null) return; - Content content = responseEntry.getValue().getContent(); - if (content == null) return; - - for (Map.Entry mediaEntry : content.entrySet()) { - if (!mediaEntry.getKey().contains("json")) continue; - - Object example = mediaEntry.getValue().getExample(); - if (example == null) { - example = ExampleBuilder.fromSchema(mediaEntry.getValue().getSchema(), swagger.getComponents().getSchemas()); - } - - String uri = String.format("/%s%s", serviceId, path); - String exampleJson = Json.pretty(example); - - addExample(method, uri, getResponseCode(responseEntry.getKey()), mediaEntry.getKey(), exampleJson); - } - } - - /** - * It load, parse and analyze input file with API doc. The response is loaded examples in {@link #examples}. - * @param serviceId ID of service of API doc file - * @param apiDoc path of file with API doc to parse - */ - public void generateExamples(String serviceId, String apiDoc) { - - try { - SwaggerParseResult parseResult = new OpenAPIParser().readContents(apiDoc, null, null); - OpenAPI swagger = parseResult != null ? parseResult.getOpenAPI() : null; - Paths paths = swagger != null ? swagger.getPaths() : null; - if (paths != null) { - for (Map.Entry pathItemEntry : paths.entrySet()) { - for (Map.Entry operationEntry : pathItemEntry.getValue().readOperationsMap().entrySet()) { - generateExample(serviceId, swagger, operationEntry.getKey().name(), operationEntry.getValue(), pathItemEntry.getKey()); - } - } - } else { - throw new ApiDocTransformationException("Exception parsing null apiDoc " + apiDoc + - " paths is null: '" + paths + "'."); - } - } catch (Exception e) { - log.warn("Cannot generate example from API doc file {}", apiDoc, e); - } - } - /** - * To find a prepared example for specific endpoint defined by request method and URL path. If no example is found - * it returns the default one (empty JSON object). - * @param method HTTP method to find - * @param path URL path to find - * @return example generated by loaded API doc (see previous call of {@link #generateExamples(String, String)}) or - * the default one (empty JSON object) - */ - public Example getExample(String method, String path) { - List byMethod = examples.get(method); - if (byMethod == null) return DEFAULT_EXAMPLE; - - return byMethod.stream() - .filter(e -> e.isMatching(path)) - .findFirst() - .orElse(DEFAULT_EXAMPLE); - } - - /** - * It writes a response with an example for specified endpoint. - * @param httpServletResponse response to be handled - * @param method request method of the endpoint - * @param path URL path of the endpoint - * @throws IOException - */ - public void replyExample(HttpServletResponse httpServletResponse, String method, String path) throws IOException { - Example example = getExample(method, path); - httpServletResponse.setContentType(example.getMediaType()); - httpServletResponse.setStatus(example.getResponseCode()); - try (PrintWriter pw = httpServletResponse.getWriter()) { - pw.print(example.getContent()); - } - } - - /** - * Data object to define example - */ - @Builder - @Value - static class Example { - - private final AntPathMatcher pathMatcher = new AntPathMatcher(); - private final String path; - private final String method; - private final int responseCode; - private final String content; - private final String mediaType; - - /** - * Checking if the ant pattern of the example is matching with the requested path - * @param uri tested URI - * @return true if example is matching, otherwise false - */ - boolean isMatching(String uri) { - return pathMatcher.match(this.path, uri); - } - - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalService.java deleted file mode 100644 index 3a63453473..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalService.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import lombok.NonNull; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.services.status.APIDocRetrievalService; - -import java.util.Collections; -import java.util.List; - -@Service -@ConditionalOnProperty( - value = "apiml.catalog.standalone.enabled", - havingValue = "true") -public class StandaloneAPIDocRetrievalService extends APIDocRetrievalService { - - public StandaloneAPIDocRetrievalService() { - super(null, null, null); - } - - @Override - public ApiDocInfo retrieveApiDoc(@NonNull String serviceId, String apiVersion) { - return null; - } - - @Override - public ApiDocInfo retrieveDefaultApiDoc(@NonNull String serviceId) { - return null; - } - - @Override - public List retrieveApiVersions(@NonNull String serviceId) { - return Collections.emptyList(); - } - - @Override - public String retrieveDefaultApiVersion(@NonNull String serviceId) { - return null; - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializer.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializer.java deleted file mode 100644 index ff5a945f78..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializer.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.event.EventListener; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.zowe.apiml.product.service.ServiceStartupEventHandler; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Initializes Catalog instances from files - */ -@Component -@ConditionalOnProperty( - value = "apiml.catalog.standalone.enabled", - havingValue = "true") -@RequiredArgsConstructor -@Order(Ordered.HIGHEST_PRECEDENCE) -public class StandaloneInitializer { - - private final StandaloneLoaderService standaloneLoaderService; - private final AtomicBoolean hasRun = new AtomicBoolean(false); - - @EventListener(ApplicationReadyEvent.class) - public void onApplicationEvent(ApplicationReadyEvent event) { - if (isStandalone(event.getApplicationContext()) && hasRun.compareAndSet(false, true)) { - standaloneLoaderService.initializeCache(); - new ServiceStartupEventHandler().onServiceStartup("API Catalog Service Standalone", - ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); - } - } - - private boolean isStandalone(ConfigurableApplicationContext context) { - return Boolean.parseBoolean(context.getEnvironment().getProperty("apiml.catalog.standalone.enabled", "false")); - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderService.java deleted file mode 100644 index 81d841565e..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderService.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Applications; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.instance.InstanceInitializeService; -import org.zowe.apiml.apicatalog.services.cached.CachedApiDocService; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.swagger.api.AbstractApiDocService; -import org.zowe.apiml.config.ApiInfo; -import org.zowe.apiml.product.instance.ServiceAddress; -import org.zowe.apiml.product.routing.RoutedServices; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.List; -import java.util.function.Function; - -@Slf4j -@Service -@ConditionalOnProperty( - value = "apiml.catalog.standalone.enabled", - havingValue = "true") -@RequiredArgsConstructor -public class StandaloneLoaderService { - - @Value("${apiml.catalog.standalone.servicesDirectory:services}") - private String servicesDirectory; - - private final ObjectMapper objectMapper; - private final InstanceInitializeService instanceInitializeService; - private final CachedApiDocService cachedApiDocService; - private final StandaloneAPIDocRetrievalService standaloneAPIDocRetrievalService; - private final Function> beanApiDocFactory; - private final ServiceAddress gatewayConfigProperties; - private final ExampleService exampleService; - - public void initializeCache() { - loadApplicationCache(); - loadOpenAPICache(); - } - - private void loadApplicationCache() { - File[] appFiles = getFiles(servicesDirectory + "/apps"); - log.debug("Found {} files", appFiles.length); - if (appFiles.length == 0) { - log.error("No service definition files found."); - return; - } - - for (File file : appFiles) { - log.debug("Processing file {}", file.getName()); - createContainerFromFile(file); - } - } - - private void loadOpenAPICache() { - File[] openAPIFiles = getFiles(servicesDirectory + "/apiDocs"); - log.debug("Found {} API Doc files", openAPIFiles.length); - if (openAPIFiles.length == 0) { - log.error("No apiDocs files found or I/O error occured."); - return; - } - - for (File openAPIFile : openAPIFiles) { - log.debug("Processing {}", openAPIFile.getName()); - loadApiDocCache(openAPIFile); - } - } - - private void createContainerFromFile(File file) { - log.info("Initialising services from '{}' file.", file.getName()); - - try { - Applications apps = objectMapper.readValue(file, Applications.class); - apps.getRegisteredApplications().forEach(app -> { - instanceInitializeService.createContainers(app); - // Uses metadata from the first instance like {@link InstanceRetrievalService#getInstanceInfo} - loadApiDocVersionCache(app.getInstances().get(0)); - }); - } catch (IOException e) { - log.error("Unable to parse service definition '{}' because {}", file.getName(), e.getMessage()); - } - } - - private ApiDocInfo createApiDocInfo(String serviceId, String apiDoc) { - ApiInfo apiInfo = new ApiInfo(); - apiInfo.setGatewayUrl(String.format("%s://%s/%s/", gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname(), serviceId)); - RoutedServices routedServices = new RoutedServices(); - return new ApiDocInfo( - apiInfo, - apiDoc, - routedServices - ); - } - - private void loadApiDocCache(File file) { - try (InputStream inputStream = Files.newInputStream(file.toPath())) { - String[] name = FilenameUtils.removeExtension(file.getName()).split("_"); - if (name.length < 2 || name.length > 3 || (name.length == 3 && !"default".equals(name[2]))) { - log.warn("ApiDoc file has incorrect name format '{}'. The correct format is '{serviceId}_{version}(_default)'.", file.getName()); - return; - } - String serviceId = name[0]; - String apiVersion = name[1]; - - String apiDoc = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - ApiDocInfo apiDocInfo = createApiDocInfo(serviceId, apiDoc); - apiDoc = beanApiDocFactory.apply(apiDoc).transformApiDoc(serviceId, apiDocInfo); - - if (name.length > 2 && name[2].equals("default")) { - cachedApiDocService.updateApiDocForService(serviceId, CachedApiDocService.DEFAULT_API_KEY, apiDoc); - } - - cachedApiDocService.updateApiDocForService(serviceId, apiVersion, apiDoc); - - exampleService.generateExamples(serviceId, apiDoc); - } catch (IOException e) { - log.error("Cannot read '{}' because {}", file.getName(), e.getMessage()); - } - } - - private void loadApiDocVersionCache(InstanceInfo instanceInfo) { - List apiVersions = standaloneAPIDocRetrievalService.retrieveApiVersions(instanceInfo.getMetadata()); - cachedApiDocService.updateApiVersionsForService(instanceInfo.getAppName(), apiVersions); - - String defaultApiVersion = standaloneAPIDocRetrievalService.retrieveDefaultApiVersion(instanceInfo.getMetadata()); - cachedApiDocService.updateDefaultApiVersionForService(instanceInfo.getAppName(), defaultApiVersion); - } - - private File[] getFiles(String directory) { - File dir = new File(directory); - if (!dir.isDirectory()) { - log.error("Directory '{}' does not exists.", directory); - return new File[0]; - } - var files = dir.listFiles(f -> f.isFile() && f.getName().endsWith(".json")); - if (files == null) { - return new File[0]; - } else return files; - } -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneSecurityConfig.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneSecurityConfig.java deleted file mode 100644 index fd806cea35..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/standalone/StandaloneSecurityConfig.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - - -import jakarta.annotation.PostConstruct; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.web.SecurityFilterChain; -import org.zowe.apiml.product.constants.CoreService; - -@Configuration -@ConditionalOnProperty(value = "apiml.catalog.standalone.enabled", havingValue = "true") -@Slf4j -public class StandaloneSecurityConfig { - - @PostConstruct - void init() { - log.warn(CoreService.API_CATALOG.getServiceId() + " is running in standalone mode. Authentication is disabled. Do not use in production."); - } - - @Bean - @Order(Ordered.HIGHEST_PRECEDENCE) - public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { - return http - .csrf(CsrfConfigurer::disable) // NOSONAR - .headers(httpSecurityHeadersConfigurer -> - httpSecurityHeadersConfigurer.httpStrictTransportSecurity(HeadersConfigurer.HstsConfig::disable) - .frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) - - .authorizeHttpRequests(matcherRegistry -> matcherRegistry.anyRequest().permitAll()) - .build(); - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/discovery/DiscoveryConfigProperties.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/DiscoveryConfigProperties.java similarity index 95% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/discovery/DiscoveryConfigProperties.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/DiscoveryConfigProperties.java index 180992f4e2..80a63113e3 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/discovery/DiscoveryConfigProperties.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/DiscoveryConfigProperties.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.discovery; +package org.zowe.apiml.apicatalog.staticapi; import lombok.Data; import org.springframework.beans.factory.annotation.Value; diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshController.java deleted file mode 100644 index 5b6c3b24ba..0000000000 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshController.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.staticapi; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/static-api") -@RequiredArgsConstructor -public class StaticAPIRefreshController { - - private final StaticAPIService staticAPIService; - - @PostMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity refreshStaticApis() { - StaticAPIResponse staticAPIResponse = staticAPIService.refresh(); - return ResponseEntity - .status(staticAPIResponse.getStatusCode()) - .body(staticAPIResponse.getBody()); - } - -} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java index c890834c70..16dbe73e17 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; import java.io.IOException; import java.util.ArrayList; diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionGenerator.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionGenerator.java index 314d5e7787..8803384bed 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionGenerator.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionGenerator.java @@ -15,7 +15,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.io.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java new file mode 100644 index 0000000000..3a22b7cb13 --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java @@ -0,0 +1,135 @@ +/* + * 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.apicatalog.swagger; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.v3.oas.models.Paths; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springdoc.core.customizers.SpringDocCustomizers; +import org.springdoc.core.models.GroupedOpenApi; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.providers.SpringDocProviders; +import org.springdoc.core.service.AbstractRequestService; +import org.springdoc.core.service.GenericResponseService; +import org.springdoc.core.service.OpenAPIService; +import org.springdoc.core.service.OperationService; +import org.springdoc.webflux.api.OpenApiWebfluxResource; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; +import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.product.gateway.GatewayClient; +import reactor.core.publisher.Mono; + +import java.util.*; + +@Slf4j +@Service +public class ApiDocRetrievalServiceLocal { + + private static final String SLASH = "/"; + + private final Map apiDocResource = new HashMap<>(); + + public ApiDocRetrievalServiceLocal( + List groupedOpenApis, + ObjectFactory openAPIBuilderObjectFactory, + AbstractRequestService requestBuilder, + GenericResponseService responseBuilder, + OperationService operationParser, + SpringDocConfigProperties springDocConfigProperties, + SpringDocProviders springDocProviders, + GatewayClient gatewayClient + ) { + groupedOpenApis + .forEach(groupedOpenApi -> { + String group = groupedOpenApi.getGroup(); + + SpringDocConfigProperties.GroupConfig groupConfig = new SpringDocConfigProperties.GroupConfig(group, groupedOpenApi.getPathsToMatch(), groupedOpenApi.getPackagesToScan(), groupedOpenApi.getPackagesToExclude(), groupedOpenApi.getPathsToExclude(), groupedOpenApi.getProducesToMatch(), groupedOpenApi.getConsumesToMatch(), groupedOpenApi.getHeadersToMatch(), groupedOpenApi.getDisplayName()); + springDocConfigProperties.addGroupConfig(groupConfig); + + Set openApiCustomizers = new HashSet<>(groupedOpenApi.getOpenApiCustomizers()); + openApiCustomizers.add(normalizePathsCustomizer(groupedOpenApi.getPathsToMatch())); + + var openApiWebfluxResource = new OpenApiWebfluxResource(groupedOpenApi.getGroup(), + openAPIBuilderObjectFactory, + requestBuilder, + responseBuilder, + operationParser, + springDocConfigProperties, + springDocProviders, new SpringDocCustomizers(Optional.of(openApiCustomizers), Optional.of(groupedOpenApi.getOperationCustomizers()), + Optional.of(groupedOpenApi.getRouterOperationCustomizers()), Optional.of(groupedOpenApi.getOpenApiMethodFilters())) + ) { + @Override + protected String getServerUrl(ServerHttpRequest serverHttpRequest, String apiDocsUrl) { + var gw = gatewayClient.getGatewayConfigProperties(); + return String.format("%s://%s%s", gw.getScheme(), gw.getHostname(), apiDocsUrl); + } + }; + + apiDocResource.put(group, openApiWebfluxResource); + }); + } + + String getCommonBasePath(List paths) { + var commopnPath = StringUtils.getCommonPrefix( + paths.stream() + .map(path -> path.endsWith(SLASH) ? path : path + SLASH) + .map(path -> StringUtils.substringBefore(path, "*")) + .map(path -> StringUtils.substringBeforeLast(path, SLASH)) + .toArray(String[]::new) + ); + return commopnPath.endsWith(SLASH) ? commopnPath.substring(0, commopnPath.length() - 1) : commopnPath; + } + + OpenApiCustomizer normalizePathsCustomizer(List paths) { + String basePath = getCommonBasePath(paths); + + return openApi -> { + openApi.setServers(openApi.getServers().stream().map(server -> { + server.setUrl(UriComponentsBuilder.fromUriString(server.getUrl()).path(basePath).build().toUriString()); + return server; + }).toList()); + + var newPaths = new Paths(); + openApi.getPaths().entrySet().forEach(entry -> + newPaths.addPathItem(entry.getKey().substring(basePath.length()), entry.getValue()) + ); + openApi.setPaths(newPaths); + }; + } + + public boolean isSupported(String serviceId) { + return apiDocResource.containsKey(StringUtils.lowerCase(serviceId)); + } + + public Mono retrieveApiDoc(ServiceInstance serviceInstance, ApiInfo apiInfo) { + String serviceId = StringUtils.lowerCase(serviceInstance.getServiceId()); + + try { + return Optional.ofNullable(apiDocResource.get(serviceId)) + .orElseThrow(() -> new ApiDocNotFoundException("Cannot obtain API doc for service " + serviceId)) + .openapiJson(null, SLASH, Locale.getDefault()) + .map(String::new) + .map(content -> ApiDocInfo.builder().local(true).apiInfo(apiInfo).apiDocContent(content).build()); + } catch (JsonProcessingException jpe) { + log.debug("Cannot process API doc", jpe); + throw new ApiDocNotFoundException("Cannot obtain API doc for " + serviceId, jpe); + } + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java new file mode 100644 index 0000000000..481630c25d --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java @@ -0,0 +1,105 @@ +/* + * 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.apicatalog.swagger; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; +import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.function.UnaryOperator; + +/** + * Retrieves the API documentation for a registered service + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class ApiDocRetrievalServiceRest { + + private static final UnaryOperator exceptionMessage = serviceId -> "No API Documentation was retrieved for the service " + serviceId + "."; + + @Qualifier("secureHttpClientWithoutKeystore") + private final CloseableHttpClient secureHttpClientWithoutKeystore; + + @InjectApimlLogger + private ApimlLogger apimlLogger = ApimlLogger.empty(); + + public Mono retrieveApiDoc(ServiceInstance serviceInstance, ApiInfo apiInfo) { + String serviceId = StringUtils.lowerCase(serviceInstance.getServiceId()); + log.debug("Retrieving API doc for '{} {}'", serviceId, apiInfo.getVersion()); + + String apiDocUrl = apiInfo.getSwaggerUrl(); + + return getApiDocContentByUrl(serviceId, apiDocUrl) + .map(content -> ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(content).build()); + } + + /** + * Get ApiDoc content by Url + * + * @param serviceId the unique service id + * @param apiDocUrl the url of apidoc + * @return the information about ApiDoc content as application/json + * @throws ApiDocNotFoundException if the response is error + */ + private Mono getApiDocContentByUrl(@NonNull String serviceId, String apiDocUrl) { + HttpGet httpGet = new HttpGet(apiDocUrl); + httpGet.setHeader(org.apache.http.HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + + // TODO: refactor with reactive client + return Mono.defer(() -> { + try { + return Mono.just(secureHttpClientWithoutKeystore.execute(httpGet, response -> { + String responseBody = ""; + var responseEntity = response.getEntity(); + if (responseEntity != null) { + responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); + } + + if (HttpStatus.SC_OK == response.getCode()) { + return responseBody; + } else { + throw new ApiDocNotFoundException( + String.format("No API Documentation was retrieved due to %s server error: %d %s", serviceId, response.getCode(), responseBody) + ); + } + } + )); + } catch (IOException e) { + apimlLogger.log("org.zowe.apiml.apicatalog.apiDocHostCommunication", serviceId, e.getMessage()); + log.debug("Error retrieving api doc for '{}'", serviceId, e); + return Mono.error(new ApiDocNotFoundException( + exceptionMessage.apply(serviceId) + " Root cause: " + e.getMessage(), e + )); + } + }) + .subscribeOn(Schedulers.boundedElastic()); + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIDocRetrievalService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocService.java similarity index 53% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIDocRetrievalService.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocService.java index b26f46dcde..955118624a 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/status/APIDocRetrievalService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocService.java @@ -8,66 +8,142 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.status; +package org.zowe.apiml.apicatalog.swagger; -import com.netflix.appinfo.InstanceInfo; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.http.MediaType; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import org.zowe.apiml.apicatalog.instance.InstanceRetrievalService; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; -import org.zowe.apiml.apicatalog.services.status.model.ApiVersionNotFoundException; -import org.zowe.apiml.apicatalog.swagger.SubstituteSwaggerGenerator; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.exceptions.ApiVersionNotFoundException; +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.message.log.ApimlLogger; import org.zowe.apiml.product.gateway.GatewayClient; -import org.zowe.apiml.product.instance.InstanceInitializationException; import org.zowe.apiml.product.instance.ServiceAddress; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.RoutedServices; +import org.zowe.apiml.util.EurekaUtils; +import reactor.core.publisher.Mono; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; -/** - * Retrieves the API documentation for a registered service - */ @Service -@ConditionalOnProperty( - value = "apiml.catalog.standalone.enabled", - havingValue = "false", - matchIfMissing = true) -@RequiredArgsConstructor @Slf4j -public class APIDocRetrievalService { +@RequiredArgsConstructor +public class ApiDocService { - @Qualifier("secureHttpClientWithoutKeystore") - private final CloseableHttpClient secureHttpClientWithoutKeystore; + private static final EurekaMetadataParser metadataParser = new EurekaMetadataParser(); + private static final SubstituteSwaggerGenerator swaggerGenerator = new SubstituteSwaggerGenerator(); - private final InstanceRetrievalService instanceRetrievalService; + private final DiscoveryClient discoveryClient; private final GatewayClient gatewayClient; + private final TransformApiDocService transformApiDocService; + private final ApiDocRetrievalServiceLocal apiDocRetrievalServiceLocal; + private final ApiDocRetrievalServiceRest apiDocRetrievalServiceRest; + + ServiceInstance getInstanceInfo(String serviceId) { + return EurekaUtils.getInstanceInfo(discoveryClient, serviceId) + .orElseThrow(() -> new ApiDocNotFoundException("Could not load instance information for service " + serviceId + ".")); + } + + private ApiInfo getApiInfoSetAsDefault(List apiInfoList) { + ApiInfo defaultApiInfo = null; + for (ApiInfo apiInfo : apiInfoList) { + if (apiInfo.isDefaultApi()) { + if (defaultApiInfo != null) { + log.warn("Multiple API are set as default: '{} {}' and '{} {}'. Neither will be treated as the default.", + defaultApiInfo.getApiId(), apiInfo.getVersion(), + apiInfo.getApiId(), apiInfo.getVersion() + ); + return null; + } else { + defaultApiInfo = apiInfo; + } + } + } + return defaultApiInfo; + } + + /** + * Return the major version from the version field in ApiInfo. + *

+ * Major version is assumed to be the first integer in the version string. + *

+ * If there is no major version (that is, no integers in the version string), + * -1 is returned as it assumed valid major versions will be 0 or higher. Thus, + * -1 can be used in an integer comparison for highest integer. + * + * @param apiInfo ApiInfo for which major version will be retrieved. + * @return int representing major version. If no version integer + */ + private int getMajorVersion(ApiInfo apiInfo) { + if (apiInfo == null) { + return -1; + } + + return apiInfo.getMajorVersion(); + } + + private boolean isHigherVersion(ApiInfo toTest, ApiInfo comparedAgainst) { + int versionToTest = getMajorVersion(toTest); + int versionToCompare = getMajorVersion(comparedAgainst); + + return versionToTest > versionToCompare; + } + + private ApiInfo getHighestApiVersion(List apiInfoList) { + if (apiInfoList == null || apiInfoList.isEmpty()) { + return null; + } + + ApiInfo highestVersionApi = apiInfoList.get(0); + for (ApiInfo apiInfo : apiInfoList) { + if (isHigherVersion(apiInfo, highestVersionApi)) { + highestVersionApi = apiInfo; + } + } + return highestVersionApi; + } + + private ApiInfo getDefaultApiInfo(List apiInfoList) { + ApiInfo defaultApiInfo = getApiInfoSetAsDefault(apiInfoList); + + if (defaultApiInfo == null) { + log.debug("No API set as default, will use highest major version as default"); + defaultApiInfo = getHighestApiVersion(apiInfoList); + } + + return defaultApiInfo; + } + + private List retrieveApiVersions(@NonNull Map metadata) { + List apiInfoList = metadataParser.parseApiInfo(metadata); + List apiVersions = new ArrayList<>(); + for (ApiInfo apiInfo : apiInfoList) { + apiVersions.add(apiInfo.getApiId() + " v" + apiInfo.getVersion()); + } - private final EurekaMetadataParser metadataParser = new EurekaMetadataParser(); - private final SubstituteSwaggerGenerator swaggerGenerator = new SubstituteSwaggerGenerator(); + return apiVersions; + } - @InjectApimlLogger - private ApimlLogger apimlLogger = ApimlLogger.empty(); + private String retrieveDefaultApiVersion(@NonNull Map metadata) { + List apiInfoList = metadataParser.parseApiInfo(metadata); + ApiInfo defaultApiInfo = getDefaultApiInfo(apiInfoList); + + if (defaultApiInfo == null) { + return ""; + } + + return String.format("%s v%s", defaultApiInfo.getApiId(), defaultApiInfo.getVersion()); + } /** * Retrieves the available API versions for a registered service. @@ -79,30 +155,20 @@ public class APIDocRetrievalService { */ public List retrieveApiVersions(@NonNull String serviceId) { log.debug("Retrieving API versions for service '{}'", serviceId); - InstanceInfo instanceInfo; + ServiceInstance serviceInstance; try { - instanceInfo = getInstanceInfo(serviceId); + serviceInstance = getInstanceInfo(serviceId); } catch (ApiDocNotFoundException e) { throw new ApiVersionNotFoundException(e.getMessage()); } - List apiVersions = retrieveApiVersions(instanceInfo.getMetadata()); + List apiVersions = retrieveApiVersions(serviceInstance.getMetadata()); log.debug("For service '{}' found API versions '{}'", serviceId, apiVersions); return apiVersions; } - public List retrieveApiVersions(@NonNull Map metadata) { - List apiInfoList = metadataParser.parseApiInfo(metadata); - List apiVersions = new ArrayList<>(); - for (ApiInfo apiInfo : apiInfoList) { - apiVersions.add(apiInfo.getApiId() + " v" + apiInfo.getVersion()); - } - - return apiVersions; - } - /** * Retrieves the default API version for a registered service. * Uses 'apiml.service.apiInfo.defaultApi' field. @@ -114,241 +180,111 @@ public List retrieveApiVersions(@NonNull Map metadata) { */ public String retrieveDefaultApiVersion(@NonNull String serviceId) { log.debug("Retrieving default API version for service '{}'", serviceId); - InstanceInfo instanceInfo; + ServiceInstance serviceInstance; try { - instanceInfo = getInstanceInfo(serviceId); + serviceInstance = getInstanceInfo(serviceId); } catch (ApiDocNotFoundException e) { throw new ApiVersionNotFoundException(e.getMessage()); } - String defaultVersion = retrieveDefaultApiVersion(instanceInfo.getMetadata()); + String defaultVersion = retrieveDefaultApiVersion(serviceInstance.getMetadata()); log.debug("For service '{}' found default API version '{}'", serviceId, defaultVersion); return defaultVersion; } - public String retrieveDefaultApiVersion(@NonNull Map metadata) { - List apiInfoList = metadataParser.parseApiInfo(metadata); - ApiInfo defaultApiInfo = getDefaultApiInfo(apiInfoList); - - if (defaultApiInfo == null) { - return ""; - } - - return String.format("%s v%s", defaultApiInfo.getApiId(), defaultApiInfo.getVersion()); - } - - /** - * Retrieve the API docs for a registered service - *

- * API doc URL is taken from the application metadata in the following - * order: - *

- * 1. 'apiml.service.apiInfo.swaggerUrl' (preferred way) - * 2. 'apiml.service.apiInfo' is present and 'swaggerUrl' is not, ApiDoc info is automatically generated - * 3. URL is constructed from 'apiml.routes.api-doc.serviceUrl'. This method is deprecated and used for - * backwards compatibility only - * - * @param serviceId the unique service id - * @param apiVersion the version of the API - * @return the API doc and related information for transformation - * @throws ApiDocNotFoundException if the response is error - */ - public ApiDocInfo retrieveApiDoc(@NonNull String serviceId, String apiVersion) { - log.debug("Retrieving API doc for '{} {}'", serviceId, apiVersion); - InstanceInfo instanceInfo = getInstanceInfo(serviceId); - - List apiInfoList = metadataParser.parseApiInfo(instanceInfo.getMetadata()); - ApiInfo apiInfo = findApi(apiInfoList, apiVersion); - - return buildApiDocInfo(serviceId, apiInfo, instanceInfo); - } - - private ApiDocInfo buildApiDocInfo(String serviceId, ApiInfo apiInfo, InstanceInfo instanceInfo) { - RoutedServices routes = metadataParser.parseRoutes(instanceInfo.getMetadata()); - String apiDocUrl = getApiDocUrl(apiInfo, instanceInfo, routes); - - if (apiDocUrl == null) { - log.warn("No api doc URL for '{} {} {}'", serviceId, apiInfo.getApiId(), apiInfo.getVersion()); - return getApiDocInfoBySubstituteSwagger(instanceInfo, routes, apiInfo); - } - - String apiDocContent = ""; - try { - apiDocContent = getApiDocContentByUrl(serviceId, apiDocUrl); - } catch (IOException e) { - apimlLogger.log("org.zowe.apiml.apicatalog.apiDocHostCommunication", serviceId, e.getMessage()); - log.debug("Error retrieving api doc for '{}'", serviceId, e); - } - return new ApiDocInfo(apiInfo, apiDocContent, routes); - } - /** - * Retrieve the default API docs for a registered service. - *

- * Default API doc is selected via the configuration parameter 'apiml.service.apiInfo.isDefault'. - *

- * If there are multiple apiInfo elements with isDefault set to 'true', or there are none set to 'true', - * then the high API version will be selected. + * Creates a URL from the routing metadata 'apiml.routes.api-doc.serviceUrl' when 'apiml.apiInfo.swaggerUrl' is + * not present * - * @param serviceId the unique service id - * @return the default API doc and related information for transfer - * @throws ApiDocNotFoundException if the response is error + * @param serviceInstance the information about service instance + * @return the URL of API doc endpoint */ - public ApiDocInfo retrieveDefaultApiDoc(@NonNull String serviceId) { - log.debug("Retrieving default API doc for service '{}'", serviceId); - InstanceInfo instanceInfo = getInstanceInfo(serviceId); - - List apiInfoList = metadataParser.parseApiInfo(instanceInfo.getMetadata()); - ApiInfo defaultApiInfo = getDefaultApiInfo(apiInfoList); - - return buildApiDocInfo(serviceId, defaultApiInfo, instanceInfo); - } + private String createApiDocUrlFromRouting(ServiceInstance serviceInstance, RoutedServices routes) { + String scheme = serviceInstance.isSecure() ? "https" : "http"; + int port = serviceInstance.getPort(); - private ApiInfo getDefaultApiInfo(List apiInfoList) { - ApiInfo defaultApiInfo = getApiInfoSetAsDefault(apiInfoList); - - if (defaultApiInfo == null) { - log.debug("No API set as default, will use highest major version as default"); - defaultApiInfo = getHighestApiVersion(apiInfoList); - } - - return defaultApiInfo; - } - - private ApiInfo getApiInfoSetAsDefault(List apiInfoList) { - ApiInfo defaultApiInfo = null; - for (ApiInfo apiInfo : apiInfoList) { - if (apiInfo.isDefaultApi()) { - if (defaultApiInfo != null) { - log.warn("Multiple API are set as default: '{} {}' and '{} {}'. Neither will be treated as the default.", - defaultApiInfo.getApiId(), apiInfo.getVersion(), - apiInfo.getApiId(), apiInfo.getVersion() - ); - return null; - } else { - defaultApiInfo = apiInfo; - } - } + String path = null; + RoutedService route = routes.findServiceByGatewayUrl("api/v1/api-doc"); + if (route != null) { + path = route.getServiceUrl(); } - return defaultApiInfo; - } - private ApiInfo getHighestApiVersion(List apiInfoList) { - if (apiInfoList == null || apiInfoList.isEmpty()) { + if (path == null) { return null; } - ApiInfo highestVersionApi = apiInfoList.get(0); - for (ApiInfo apiInfo : apiInfoList) { - if (isHigherVersion(apiInfo, highestVersionApi)) { - highestVersionApi = apiInfo; - } - } - return highestVersionApi; + UriComponents uri = UriComponentsBuilder + .newInstance() + .scheme(scheme) + .host(serviceInstance.getHost()) + .port(port) + .path(path) + .build(); + + return uri.toUriString(); } - private boolean isHigherVersion(ApiInfo toTest, ApiInfo comparedAgainst) { - int versionToTest = getMajorVersion(toTest); - int versionToCompare = getMajorVersion(comparedAgainst); + private String getGatewayUrl() { + ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - return versionToTest > versionToCompare; + return String.format("%s://%s/", + gatewayConfigProperties.getScheme(), + gatewayConfigProperties.getHostname() + ); } /** - * Return the major version from the version field in ApiInfo. - *

- * Major version is assumed to be the first integer in the version string. - *

- * If there is no major version (that is, no integers in the version string), - * -1 is returned as it assumed valid major versions will be 0 or higher. Thus, - * -1 can be used in an integer comparison for highest integer. + * Get ApiDocInfo by Substitute Swagger * - * @param apiInfo ApiInfo for which major version will be retrieved. - * @return int representing major version. If no version integer + * @param serviceInstance the information about service instance + * @return the information about APIDocInfo */ - private int getMajorVersion(ApiInfo apiInfo) { - if (apiInfo == null) { - return -1; - } - - return apiInfo.getMajorVersion(); + private Mono getApiDocInfoBySubstituteSwagger(ServiceInstance serviceInstance, ApiInfo apiInfo) { + return Mono.fromSupplier(gatewayClient::getGatewayConfigProperties) + .map(gw -> swaggerGenerator.generateSubstituteSwaggerForService( + serviceInstance, + apiInfo, + gw.getScheme(), + gw.getHostname()) + ) + .map(content -> ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(content).routes(new RoutedServices()).build()); } - /** - * Get ApiDoc url - * - * @param apiInfo the apiInfo of service instance - * @param instanceInfo the information about service instance - * @param routes the routes of service instance - * @return the url of apidoc - */ - private String getApiDocUrl(ApiInfo apiInfo, InstanceInfo instanceInfo, RoutedServices routes) { - String apiDocUrl = null; + Mono retrieveApiDoc(ServiceInstance serviceInstance, ApiInfo apiInfo) { + String serviceId = StringUtils.lowerCase(serviceInstance.getServiceId()); + var routes = metadataParser.parseRoutes(serviceInstance.getMetadata()); + if (apiInfo == null) { - apiDocUrl = createApiDocUrlFromRouting(instanceInfo, routes); - } else if (apiInfo.getSwaggerUrl() != null) { - apiDocUrl = apiInfo.getSwaggerUrl(); + apiInfo = ApiInfo.builder().gatewayUrl(getGatewayUrl()).build(); } - return apiDocUrl; - } - - /** - * Get ApiDoc content by Url - * - * @param serviceId the unique service id - * @param apiDocUrl the url of apidoc - * @return the information about ApiDoc content as application/json - * @throws ApiDocNotFoundException if the response is error - */ - private String getApiDocContentByUrl(@NonNull String serviceId, String apiDocUrl) throws IOException { - HttpGet httpGet = new HttpGet(apiDocUrl); - httpGet.setHeader(org.apache.http.HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); - - return secureHttpClientWithoutKeystore.execute(httpGet, response -> { - String responseBody = ""; - var responseEntity = response.getEntity(); - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); - } + if (apiInfo.getSwaggerUrl() == null) { + apiInfo.setSwaggerUrl(createApiDocUrlFromRouting(serviceInstance, routes)); + } - if (HttpStatus.SC_OK == response.getCode()) { - return responseBody; - } else { - throw new ApiDocNotFoundException("No API Documentation was retrieved due to " + serviceId + - " server error: '" + responseBody + "'."); - } - } - ); - } + Mono apiDocInfo; + if (apiInfo.getSwaggerUrl() == null) { + apiDocInfo = getApiDocInfoBySubstituteSwagger(serviceInstance, apiInfo); + } else if (apiDocRetrievalServiceLocal.isSupported(serviceId)) { + apiDocInfo = apiDocRetrievalServiceLocal.retrieveApiDoc(serviceInstance, apiInfo); + } else { + apiDocInfo = apiDocRetrievalServiceRest.retrieveApiDoc(serviceInstance, apiInfo); + } - /** - * Get ApiDocInfo by Substitute Swagger - * - * @param instanceInfo the information about service instance - * @param routes the routes of service instance - * @param apiInfo the apiInfo of service instance - * @return the information about APIDocInfo - */ - private ApiDocInfo getApiDocInfoBySubstituteSwagger(InstanceInfo instanceInfo, - RoutedServices routes, - ApiInfo apiInfo) { - ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - String response = swaggerGenerator.generateSubstituteSwaggerForService( - instanceInfo, - apiInfo, - gatewayConfigProperties.getScheme(), - gatewayConfigProperties.getHostname()); - return new ApiDocInfo(apiInfo, response, routes); + return apiDocInfo + .map(a -> { + a.setRoutes(routes); + return transformApiDocService.transformApiDoc(serviceId, a); + }); } /** * Find ApiInfo for the corresponding version, if not found the first one is returned * * @param apiInfos the list of APIs information - * @param apiVersion the version to be find + * @param apiVersion the version to be found * @return the information about API */ private ApiInfo findApi(List apiInfos, String apiVersion) { @@ -376,59 +312,46 @@ private ApiInfo findApi(List apiInfos, String apiVersion) { }); } - private InstanceInfo getInstanceInfo(String serviceId) { - String errMsg = "Could not load instance information for service " + serviceId + "."; - try { - InstanceInfo instanceInfo = instanceRetrievalService.getInstanceInfo(serviceId); - if (instanceInfo == null) { - throw new ApiDocNotFoundException(errMsg); - } - - return instanceInfo; - } catch (InstanceInitializationException e) { - throw new ApiDocNotFoundException(errMsg, e); - } + /** + * Retrieve the API docs for a registered service + *

+ * API doc URL is taken from the application metadata in the following + * order: + *

+ * 1. 'apiml.service.apiInfo.swaggerUrl' (preferred way) + * 2. 'apiml.service.apiInfo' is present and 'swaggerUrl' is not, ApiDoc info is automatically generated + * 3. URL is constructed from 'apiml.routes.api-doc.serviceUrl'. This method is deprecated and used for + * backwards compatibility only + * + * @param serviceId the unique service id + * @param apiVersion the version of the API + * @return the API doc + * @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()); + var apiInfo = findApi(apiInfoList, apiVersion); + return retrieveApiDoc(serviceInstance, apiInfo); } /** - * Creates a URL from the routing metadata 'apiml.routes.api-doc.serviceUrl' when 'apiml.apiInfo.swaggerUrl' is - * not present + * Retrieve the default API docs for a registered service. + *

+ * Default API doc is selected via the configuration parameter 'apiml.service.apiInfo.isDefault'. + *

+ * If there are multiple apiInfo elements with isDefault set to 'true', or there are none set to 'true', + * then the high API version will be selected. * - * @param instanceInfo the information about service instance - * @return the URL of API doc endpoint - * @deprecated Added to support services which were on-boarded before 'apiml.apiInfo.swaggerUrl' parameter was - * introduced. It will be removed when all services will be using the new configuration style. + * @param serviceId the unique service id + * @return the default API doc + * @throws ApiDocNotFoundException if the response is error */ - @Deprecated(forRemoval = false) - private String createApiDocUrlFromRouting(InstanceInfo instanceInfo, RoutedServices routes) { - String scheme; - int port; - if (instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE)) { - scheme = "https"; - port = instanceInfo.getSecurePort(); - } else { - scheme = "http"; - port = instanceInfo.getPort(); - } - - String path = null; - RoutedService route = routes.findServiceByGatewayUrl("api/v1/api-doc"); - if (route != null) { - path = route.getServiceUrl(); - } - - if (path == null) { - throw new ApiDocNotFoundException("No API Documentation defined for service " + instanceInfo.getAppName().toLowerCase() + "."); - } - - UriComponents uri = UriComponentsBuilder - .newInstance() - .scheme(scheme) - .host(instanceInfo.getHostName()) - .port(port) - .path(path) - .build(); - - return uri.toUriString(); + public Mono retrieveDefaultApiDoc(@NonNull String serviceId) { + ServiceInstance serviceInstance = getInstanceInfo(serviceId); + List apiInfoList = metadataParser.parseApiInfo(serviceInstance.getMetadata()); + var apiInfo = getDefaultApiInfo(apiInfoList); + return retrieveApiDoc(serviceInstance, apiInfo); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java new file mode 100644 index 0000000000..8724c19a23 --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java @@ -0,0 +1,370 @@ +/* + * 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.apicatalog.swagger; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +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.zowe.apiml.apicatalog.model.APIContainer; +import org.zowe.apiml.apicatalog.model.APIService; +import org.zowe.apiml.apicatalog.model.CustomStyleConfig; +import org.zowe.apiml.auth.Authentication; +import org.zowe.apiml.auth.AuthenticationSchemes; +import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.product.routing.RoutedServices; +import org.zowe.apiml.product.routing.ServiceType; +import org.zowe.apiml.product.routing.transform.TransformService; +import org.zowe.apiml.product.routing.transform.URLTransformationException; +import org.zowe.apiml.util.EurekaUtils; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.netflix.appinfo.InstanceInfo.InstanceStatus.UP; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; +import static org.zowe.apiml.product.constants.CoreService.GATEWAY; + +/** + * Initialize the API catalog with the running instances. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ContainerService { + + private static final String DEFAULT_APIINFO_KEY = "default"; + + private final AuthenticationSchemes schemes = new AuthenticationSchemes(); + private final EurekaMetadataParser metadataParser = new EurekaMetadataParser(); + + private final DiscoveryClient discoveryClient; + private final TransformService transformService; + private final CustomStyleConfig customStyleConfig; + + @Value("${apiml.catalog.hide.serviceInfo:false}") + private boolean hideServiceInfo; + + @Value("${server.attls.enabled:false}") + private boolean isAttlsEnabled; + + @InjectApimlLogger + private final ApimlLogger apimlLog = ApimlLogger.empty(); + + private Set getProductIds() { + return discoveryClient.getServices().stream() + .map(discoveryClient::getInstances) + .flatMap(List::stream) + .map(ServiceInstance::getMetadata) + .map(metadata -> metadata.get(CATALOG_ID)) + .collect(Collectors.toSet()); + } + + /** + * Return all cached service instances + * + * @return instances + */ + public Collection getAllContainers() { + return getProductIds().stream() + .map(this::getContainerById) + .filter(Objects::nonNull) + .toList(); + } + + private boolean isSso(ServiceInstance serviceInstance) { + Map eurekaMetadata = serviceInstance.getMetadata(); + return Authentication.builder() + .scheme(schemes.map(eurekaMetadata.get(AUTHENTICATION_SCHEME))) + .supportsSso(BooleanUtils.toBooleanObject(eurekaMetadata.get(AUTHENTICATION_SSO))) + .build() + .supportsSso(); + } + + private String getHomePageUrl(ServiceInstance serviceInstance) { + if (serviceInstance instanceof EurekaServiceInstance eurekaServiceInstance) { + return eurekaServiceInstance.getInstanceInfo().getHomePageUrl(); + } + + return serviceInstance.getUri().toString(); + } + + private boolean isUp(ServiceInstance serviceInstance) { + if (serviceInstance instanceof EurekaServiceInstance eurekaServiceInstance) { + return eurekaServiceInstance.getInstanceInfo().getStatus() == UP; + } + + return true; + } + + private boolean hasHomePage(ServiceInstance serviceInstance) { + String instanceHomePage = getHomePageUrl(serviceInstance); + return instanceHomePage != null + && !instanceHomePage.isEmpty(); + } + + /** + * Try to transform the service homepage url and return it. If it fails, + * return the original homepage url + * + * @param serviceInstance the service instance + * @return the transformed homepage url + */ + private String getInstanceHomePageUrl(ServiceInstance serviceInstance) { + String serviceId = StringUtils.lowerCase(serviceInstance.getServiceId()); + String instanceHomePage = getHomePageUrl(serviceInstance); + + //Gateway homePage is used to hold DVIPA address and must not be modified + if (hasHomePage(serviceInstance) && !GATEWAY.getServiceId().equals(serviceId)) { + instanceHomePage = instanceHomePage.trim(); + RoutedServices routes = metadataParser.parseRoutes(serviceInstance.getMetadata()); + try { + instanceHomePage = transformService.transformURL( + ServiceType.UI, + serviceId, + instanceHomePage, + routes, + isAttlsEnabled); + } catch (URLTransformationException | IllegalArgumentException e) { + apimlLog.log("org.zowe.apiml.apicatalog.homePageTransformFailed", serviceId, e.getMessage()); + } + } + + log.debug("Homepage URL for {} service is: {}", serviceId, instanceHomePage); + return instanceHomePage; + } + + /** + * Get the base path for the service. + * + * @param serviceInstance the service instance + * @return the base URL + */ + private String getApiBasePath(ServiceInstance serviceInstance) { + if (hasHomePage(serviceInstance)) { + try { + String apiBasePath = serviceInstance.getMetadata().get("apiml.apiBasePath"); + if (apiBasePath != null) { + return apiBasePath; + } + + RoutedServices routes = metadataParser.parseRoutes(serviceInstance.getMetadata()); + return transformService.retrieveApiBasePath( + StringUtils.lowerCase(serviceInstance.getServiceId()), + getHomePageUrl(serviceInstance), + routes); + } catch (URLTransformationException e) { + apimlLog.log("org.zowe.apiml.apicatalog.getApiBasePathFailed", serviceInstance.getServiceId(), e.getMessage()); + } + } + return ""; + } + + /** + * Create a APIService object using the instances metadata + * + * @param serviceInstance the service instance + * @return a APIService object + */ + APIService createAPIServiceFromInstance(ServiceInstance serviceInstance) { + boolean secureEnabled = serviceInstance.isSecure(); + + String instanceHomePage = getInstanceHomePageUrl(serviceInstance); + String apiBasePath = getApiBasePath(serviceInstance); + Map apiInfoById = new HashMap<>(); + + try { + List apiInfoList = metadataParser.parseApiInfo(serviceInstance.getMetadata()); + apiInfoList.stream().filter(apiInfo -> apiInfo.getApiId() != null).forEach(apiInfo -> { + String id = (apiInfo.getMajorVersion() < 0) ? DEFAULT_APIINFO_KEY : apiInfo.getApiId() + " v" + apiInfo.getVersion(); + apiInfoById.put(id, apiInfo); + }); + if (!apiInfoById.containsKey(DEFAULT_APIINFO_KEY)) { + ApiInfo defaultApiInfo = apiInfoList.stream().filter(ApiInfo::isDefaultApi).findFirst().orElse(null); + apiInfoById.put(DEFAULT_APIINFO_KEY, defaultApiInfo); + } + } catch (Exception ex) { + log.info("createApiServiceFromInstance#incorrectVersions {}", ex.getMessage()); + } + + String serviceId = StringUtils.lowerCase(serviceInstance.getServiceId()); + String title = serviceInstance.getMetadata().get(SERVICE_TITLE); + if (GATEWAY.getServiceId().equals(serviceId)) { + if (RegistrationType.of(serviceInstance.getMetadata()).isAdditional()) { + // additional registration for GW means domain one, update serviceId and basePath with the ApimlId + String apimlId = serviceInstance.getMetadata().get(APIML_ID); + if (apimlId != null) { + serviceId = StringUtils.lowerCase(apimlId); + apiBasePath = String.join("/", "", serviceId); + title += " (" + apimlId + ")"; + } + } else { + apiBasePath = "/"; + } + } + + return new APIService.Builder(serviceId) + .title(title) + .description(serviceInstance.getMetadata().get(SERVICE_DESCRIPTION)) + .tileDescription(serviceInstance.getMetadata().get(CATALOG_DESCRIPTION)) + .secured(secureEnabled) + .baseUrl(getHomePageUrl(serviceInstance)) + .homePageUrl(instanceHomePage) + .basePath(apiBasePath) + .sso(isSso(serviceInstance)) + .apis(apiInfoById) + .instanceId(serviceInstance.getInstanceId()) + .build(); + } + + /** + * Create a new container based on information in a new instance + * + * @param productFamilyId parent id + * @param serviceInstances all instances + * @return a new container + */ + private APIContainer createNewContainerFromService(String productFamilyId, ServiceInstance...serviceInstances) { + if (serviceInstances.length == 0) { + return null; + } + + Map instanceInfoMetadata = serviceInstances[0].getMetadata(); + String title = instanceInfoMetadata.get(CATALOG_TITLE); + String description = instanceInfoMetadata.get(CATALOG_DESCRIPTION); + String version = instanceInfoMetadata.get(CATALOG_VERSION); + APIContainer container = new APIContainer(); + container.setStatus("UP"); + container.setId(productFamilyId); + container.setDescription(description); + container.setTitle(title); + container.setVersion(version); + log.debug("updated Container cache with product family: " + productFamilyId + ": " + title); + + // create API Service from instance and update container last changed date + for (ServiceInstance serviceInstance : serviceInstances) { + container.addService(createAPIServiceFromInstance(serviceInstance)); + } + return container; + } + + boolean update(APIService apiService) { + List instances = discoveryClient.getInstances(apiService.getServiceId()); + + boolean isUp = instances.stream().anyMatch(this::isUp); + boolean isSso = instances.stream().allMatch(this::isSso); + + apiService.setStatus(isUp ? "UP" : "DOWN"); + apiService.setSsoAllInstances(isSso); + + return isUp; + } + + private void setStatus(APIContainer apiContainer, int servicesCount, int activeServicesCount) { + apiContainer.setTotalServices(servicesCount); + apiContainer.setActiveServices(activeServicesCount); + + if (activeServicesCount == 0) { + apiContainer.setStatus("DOWN"); + } else if (activeServicesCount == servicesCount) { + apiContainer.setStatus("UP"); + } else { + apiContainer.setStatus("WARNING"); + } + } + + /** + * Map the configuration to customize the Catalog UI to the container + * + * @param apiContainer + */ + private void setCustomUiConfig(APIContainer apiContainer) { + apiContainer.setCustomStyleConfig(customStyleConfig); + } + + + /** + * Update the summary totals, sso and API IDs info for a container based on it's running services + * + * @param apiContainer calculate totals for this container + */ + public void calculateContainerServiceValues(APIContainer apiContainer) { + if (apiContainer.getServices() == null) { + apiContainer.setServices(new HashSet<>()); + } + + int servicesCount = apiContainer.getServices().size(); + int activeServicesCount = 0; + boolean isSso = servicesCount > 0; + for (APIService apiService : apiContainer.getServices()) { + if (update(apiService)) { + activeServicesCount++; + } + isSso &= apiService.isSsoAllInstances(); + } + + setStatus(apiContainer, servicesCount, activeServicesCount); + apiContainer.setSso(isSso); + apiContainer.setHideServiceInfo(hideServiceInfo); + + // set metadata to customize the UI + if (customStyleConfig != null) { + setCustomUiConfig(apiContainer); + } + + } + + /** + * return cached service instance by id + * + * @param id service identifier + * @return {@link APIContainer} + */ + public APIContainer getContainerById(String id) { + if (id == null) { + return null; + } + + var instances = discoveryClient.getServices().stream() + .map(discoveryClient::getInstances) + .flatMap(List::stream) + .filter(instance -> id.equalsIgnoreCase(instance.getMetadata().get(CATALOG_ID))) + .toArray(ServiceInstance[]::new); + + if (ArrayUtils.isEmpty(instances)) { + return null; + } + + var container = createNewContainerFromService(id, instances); + calculateContainerServiceValues(container); + return container; + } + + public APIService getService(String serviceId) { + return EurekaUtils.getInstanceInfo(discoveryClient, serviceId) + .map(instance -> { + var apiService = createAPIServiceFromInstance(instance); + update(apiService); + return apiService; + }) + .orElse(null); + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGenerator.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGenerator.java index 2fad2e898b..afe7c08749 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGenerator.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGenerator.java @@ -11,13 +11,13 @@ package org.zowe.apiml.apicatalog.swagger; import lombok.extern.slf4j.Slf4j; -import org.zowe.apiml.config.ApiInfo; -import com.netflix.appinfo.InstanceInfo; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; +import org.springframework.cloud.client.ServiceInstance; +import org.zowe.apiml.config.ApiInfo; import java.io.StringWriter; @@ -34,13 +34,13 @@ public SubstituteSwaggerGenerator() { ve.init(); } - public String generateSubstituteSwaggerForService(InstanceInfo service, + public String generateSubstituteSwaggerForService(ServiceInstance serviceInstance, ApiInfo api, String gatewayScheme, String gatewayHost) { - log.warn("Generating substitute swagger for service instance '{}' API '{} {}'", service.getInstanceId(), api.getApiId(), api.getVersion()); - String title = service.getMetadata().get(SERVICE_TITLE); - String description = service.getMetadata().get(SERVICE_DESCRIPTION); - String basePath = (api.getGatewayUrl().startsWith("/") ? "" : "/") + service.getAppName().toLowerCase() + log.warn("Generating substitute swagger for serviceInstance '{}' API '{} {}'", serviceInstance.getInstanceId(), api.getApiId(), api.getVersion()); + String title = serviceInstance.getMetadata().get(SERVICE_TITLE); + String description = serviceInstance.getMetadata().get(SERVICE_DESCRIPTION); + String basePath = (api.getGatewayUrl().startsWith("/") ? "" : "/") + serviceInstance.getServiceId().toLowerCase() + (api.getGatewayUrl().endsWith("/") ? "" : "/") + api.getGatewayUrl(); Template t = ve.getTemplate("substitute_swagger.json"); diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocService.java index 08ba81b1f2..542f311b2f 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocService.java @@ -10,12 +10,13 @@ package org.zowe.apiml.apicatalog.swagger; +import jakarta.validation.UnexpectedTypeException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; +import org.zowe.apiml.apicatalog.exceptions.ApiDocTransformationException; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.apicatalog.swagger.api.AbstractApiDocService; -import jakarta.validation.UnexpectedTypeException; import java.util.function.Function; /** @@ -24,6 +25,7 @@ @Service @RequiredArgsConstructor public class TransformApiDocService { + private final Function> beanApiDocFactory; /** @@ -44,4 +46,5 @@ public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { return abstractApiDocService.transformApiDoc(serviceId, apiDocInfo); } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocService.java index 22a2129c55..26d696d251 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocService.java @@ -13,9 +13,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; -import org.springframework.beans.factory.annotation.Value; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.RoutedServices; @@ -27,9 +27,7 @@ @Slf4j public abstract class AbstractApiDocService { - @Value("${apiml.catalog.standalone.enabled:false}") - protected boolean standalone; - + protected final ApplicationInfo applicationInfo; protected final GatewayClient gatewayClient; protected static final String EXTERNAL_DOCUMENTATION = "External documentation"; @@ -41,15 +39,8 @@ public abstract class AbstractApiDocService { protected abstract void updateExternalDoc(T swaggerAPI, ApiDocInfo apiDocInfo); - protected String getHostname(String serviceId) { - String hostname = gatewayClient.getGatewayConfigProperties().getHostname(); - if (!standalone) return hostname; - - StringBuilder sb = new StringBuilder(); - sb.append(hostname); - if (!hostname.endsWith("/")) sb.append('/'); - sb.append(serviceId); - return sb.toString(); + protected String getHostname() { + return gatewayClient.getGatewayConfigProperties().getHostname(); } protected void preparePath(N path, ApiDocPath apiDocPath, ApiDocInfo apiDocInfo, String basePath, String originalEndpoint, String serviceId) { diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2Service.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2Service.java index 6c813b635c..fe7612d4b9 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2Service.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2Service.java @@ -11,18 +11,16 @@ package org.zowe.apiml.apicatalog.swagger.api; import com.fasterxml.jackson.core.JsonProcessingException; -import io.swagger.models.ExternalDocs; -import io.swagger.models.Path; -import io.swagger.models.Scheme; -import io.swagger.models.Swagger; +import io.swagger.models.*; import io.swagger.parser.SwaggerParser; import io.swagger.util.Json; import jakarta.validation.UnexpectedTypeException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.swagger.ApiDocTransformationException; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; +import org.zowe.apiml.apicatalog.exceptions.ApiDocTransformationException; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; @@ -35,8 +33,8 @@ public class ApiDocV2Service extends AbstractApiDocService { @Value("${gateway.scheme.external:https}") private String scheme; - public ApiDocV2Service(GatewayClient gatewayClient) { - super(gatewayClient); + public ApiDocV2Service(ApplicationInfo applicationInfo, GatewayClient gatewayClient) { + super(applicationInfo, gatewayClient); } public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { @@ -46,9 +44,22 @@ public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { throw new UnexpectedTypeException(String.format("The Swagger definition for service '%s' was retrieved but was not a valid JSON document.", serviceId)); } + if (swagger.getInfo() == null) { + swagger.setInfo(new Info()); + } + if (swagger.getInfo().getVersion() == null) { + swagger.getInfo().setVersion(apiDocInfo.getApiInfo().getVersion()); + } + boolean hidden = swagger.getTag(HIDDEN_TAG) != null; - if (!isDefinedOnlyBypassRoutes(apiDocInfo)) { + /** + * When microservices are in place it is necessary to use path updates, it basically adds into the swagger + * routing. In case of modulith it is not wanted. The paths are the final one (REST calls does not use Gateway). + * One specific case is microservices and API Catalog. Even the api doc is downloaded locally it has to be + * handled by Gateway, so the routes should be added. + */ + if (!isDefinedOnlyBypassRoutes(apiDocInfo) && !(apiDocInfo.isLocal() && applicationInfo.isModulith())) { updateSchemeHost(swagger, serviceId); updatePaths(swagger, serviceId, apiDocInfo, hidden); } @@ -70,9 +81,9 @@ public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { * @param serviceId the unique service id */ private void updateSchemeHost(Swagger swagger, String serviceId) { - log.debug("Updating host for service with id: " + serviceId + " to: " + getHostname(serviceId)); + log.debug("Updating host for service with id: " + serviceId + " to: " + getHostname()); swagger.setSchemes(Collections.singletonList(Scheme.forValue(scheme))); - swagger.setHost(getHostname(serviceId)); + swagger.setHost(getHostname()); } private void updateSwaggerUrl(Swagger swagger, String serviceId, ApiInfo apiInfo, boolean hidden, String scheme) { diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java index 1f6ec5587a..5471cb6711 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3Service.java @@ -22,6 +22,7 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.security.SecurityScheme; @@ -32,10 +33,11 @@ import jakarta.validation.UnexpectedTypeException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.swagger.ApiDocTransformationException; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; +import org.zowe.apiml.apicatalog.exceptions.ApiDocTransformationException; import org.zowe.apiml.apicatalog.swagger.SecuritySchemeSerializer; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.routing.RoutedService; @@ -51,8 +53,8 @@ public class ApiDocV3Service extends AbstractApiDocService { @Value("${gateway.scheme.external:https}") private String scheme; - public ApiDocV3Service(GatewayClient gatewayClient) { - super(gatewayClient); + public ApiDocV3Service(ApplicationInfo applicationInfo, GatewayClient gatewayClient) { + super(applicationInfo, gatewayClient); } public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { @@ -70,11 +72,24 @@ public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { } } + if (openAPI.getInfo() == null) { + openAPI.setInfo(new Info()); + } + if (openAPI.getInfo().getVersion() == null) { + openAPI.getInfo().setVersion(apiDocInfo.getApiInfo().getVersion()); + } + boolean hidden = isHidden(openAPI.getTags()); - if (!isDefinedOnlyBypassRoutes(apiDocInfo)) { + /** + * When microservices are in place it is necessary to use path updates, it basically adds into the swagger + * routing. In case of modulith it is not wanted. The paths are the final one (REST calls does not use Gateway). + * One specific case is microservices and API Catalog. Even the api doc is downloaded locally it has to be + * handled by Gateway, so the routes should be added. + */ + if (!isDefinedOnlyBypassRoutes(apiDocInfo) && !(apiDocInfo.isLocal() && applicationInfo.isModulith())) { updatePaths(openAPI, serviceId, apiDocInfo, hidden); - updateServer(openAPI, serviceId); + updateServer(openAPI); } updateSwaggerUrl(openAPI, serviceId, apiDocInfo.getApiInfo(), hidden, scheme); updateExternalDoc(openAPI, apiDocInfo); @@ -87,11 +102,11 @@ public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { } } - private void updateServer(OpenAPI openAPI, String serviceId) { + private void updateServer(OpenAPI openAPI) { if (openAPI.getServers() != null) { openAPI.getServers() .forEach(server -> server.setUrl( - String.format("%s://%s/%s", scheme, getHostname(serviceId), server.getUrl()))); + String.format("%s://%s/%s", scheme, getHostname(), server.getUrl()))); } } @@ -196,7 +211,7 @@ private boolean isHidden(List tags) { return tags != null && tags.stream().anyMatch(tag -> tag.getName().equals(HIDDEN_TAG)); } - private ObjectMapper objectMapper() { + ObjectMapper objectMapper() { return new ObjectMapper() .setSerializationInclusion(JsonInclude.Include.NON_NULL) .registerModule(new SimpleModule().addSerializer(SecurityScheme.class, new SecuritySchemeSerializer())) @@ -205,4 +220,5 @@ private ObjectMapper objectMapper() { .addMixIn(Schema.class, SchemaMixin.class) .addMixIn(MediaType.class, MediaTypeMixin.class); } + } diff --git a/api-catalog-services/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/api-catalog-services/src/main/resources/META-INF/additional-spring-configuration-metadata.json deleted file mode 100644 index 8288380794..0000000000 --- a/api-catalog-services/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "properties": [ - { - "name": "apiml.catalog.standalone", - "type": "java.util.Map", - "description": "API Catalog standalone configuration.\nStandalone mode allows displaying, without the need for authentication, services that are stored on the disk. API Catalog does not connect to any other service." - }, - { - "name": "apiml.catalog.standalone.enabled", - "type": "java.lang.Boolean", - "defaultValue": "false", - "description": "Specifies whether to enable standalone mode.\nStandalone mode allows displaying, without the need for authentication, services that are stored on the disk. API Catalog does not connect to any other service." - } - ] -} diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index f3e99bc454..7b48060114 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -16,13 +16,17 @@ spring: main: allow-circular-references: true banner-mode: ${apiml.banner:"off"} - web-application-type: servlet + web-application-type: reactive profiles.group: dev: debug, diag springdoc: packagesToScan: org.zowe.apiml.apicatalog.controllers.api - + api-docs: + path: /apicatalog/v3/api-docs + group-configs: + - group: apicatalog + pathsToMatch: /apicatalog/** logging: level: ROOT: INFO @@ -58,7 +62,6 @@ apiml: hostname: localhost ipAddress: 0.0.0.0 port: 10014 - contextPath: /apicatalog scheme: https discoveryServiceUrls: https://localhost:10011/eureka/ @@ -85,7 +88,7 @@ apiml: id: apimediationlayer title: API Mediation Layer API description: The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation. - version: 1.0.0 + version: 1.0.0 # Configuration to customize the style of Catalog UI customStyle: logo: @@ -107,8 +110,6 @@ apiml: server: address: ${apiml.service.ipAddress} port: ${apiml.service.port} - servlet: - contextPath: ${apiml.service.contextPath} ssl: enabled: ${apiml.security.ssl.sslEnabled} protocol: ${apiml.security.ssl.protocol} @@ -133,10 +134,10 @@ eureka: instance: instanceId: ${apiml.service.hostname}:${apiml.service.id}:${apiml.service.port} hostname: ${apiml.service.hostname} - statusPageUrlPath: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/info - healthCheckUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/health - secureHealthCheckUrl: https://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/health - homePageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath} + statusPageUrlPath: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/info + healthCheckUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health + secureHealthCheckUrl: https://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health + homePageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/apicatalog port: ${apiml.service.port} securePort: ${apiml.service.port} nonSecurePortEnabled: ${apiml.service.nonSecurePortEnabled} @@ -162,7 +163,7 @@ eureka: - apiId: zowe.apiml.apicatalog version: 1.0.0 gatewayUrl: api/v1 - swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v3/api-docs + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs service: title: API Catalog @@ -182,7 +183,7 @@ management: endpoints: migrate-legacy-ids: true web: - base-path: /application + base-path: /apicatalog/application exposure: include: health,info,hystrixstream health: @@ -200,7 +201,6 @@ management: endpoints: migrate-legacy-ids: true web: - base-path: /application exposure: include: health,info,loggers,hystrixstream @@ -226,7 +226,6 @@ management: endpoints: migrate-legacy-ids: true web: - base-path: /application exposure: include: "*" --- @@ -242,7 +241,7 @@ eureka: securePort: 0 nonSecurePortEnabled: true securePortEnabled: false - secureHealthCheckUrl: http://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/health + secureHealthCheckUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health metadata-map: apiml: corsEnabled: true @@ -251,7 +250,7 @@ eureka: - apiId: zowe.apiml.apicatalog version: 1.0.0 gatewayUrl: api/v1 - swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v3/api-docs + swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs apiml: service: scheme: http diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTest.java deleted file mode 100644 index 41d75c16dc..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.security.config.annotation.web.WebSecurityConfigurer; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; - -import static org.hamcrest.Matchers.hasItem; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(SpringExtension.class) -@WebMvcTest(controllers = {ApiCatalogController.class}, - excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebSecurityConfigurer.class) }, - excludeAutoConfiguration = { SecurityAutoConfiguration.class} -) -@ContextConfiguration(classes = ApiCatalogControllerContainerRetrievalTestContextConfiguration.class) -class ApiCatalogControllerContainerRetrievalTest { - - @Autowired - private MockMvc mockMvc; - - @Test - void getContainers() throws Exception { - this.mockMvc.perform(get("/containers")) - .andExpect(status().is5xxServerError()) - .andExpect(jsonPath("$.messages[?(@.messageNumber == 'ZWEAC104E')].messageContent", - hasItem("Could not retrieve container statuses, java.lang.NullPointerException"))); - } - - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTestContextConfiguration.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTestContextConfiguration.java deleted file mode 100644 index bd9eb8cb12..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerContainerRetrievalTestContextConfiguration.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.springframework.context.annotation.Bean; -import org.zowe.apiml.apicatalog.controllers.handlers.ApiCatalogControllerExceptionHandler; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; - -import static org.mockito.Mockito.*; - -class ApiCatalogControllerContainerRetrievalTestContextConfiguration { - - @Bean - public CachedProductFamilyService cachedProductFamilyService() { - return mock(CachedProductFamilyService.class); - } - - @Bean - public ApiCatalogController apiCatalogController(CachedProductFamilyService cachedProductFamilyService) { - when(cachedProductFamilyService.getAllContainers()) - .thenThrow(new NullPointerException()); - - verify(cachedProductFamilyService, never()).getAllContainers(); - - return new ApiCatalogController(cachedProductFamilyService, null); - } - - @Bean - public MessageService messageService() { - return new YamlMessageService("/apicatalog-log-messages.yml"); - } - - @Bean - public ApiCatalogControllerExceptionHandler apiCatalogControllerExceptionHandler() { - return new ApiCatalogControllerExceptionHandler(messageService()); - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java deleted file mode 100644 index b008329481..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import io.restassured.module.mockmvc.RestAssuredMockMvc; -import org.junit.jupiter.api.*; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalThrowable; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.model.APIService; -import org.zowe.apiml.apicatalog.services.cached.CachedApiDocService; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; - -import java.lang.reflect.Field; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -import static io.restassured.module.mockmvc.RestAssuredMockMvc.standaloneSetup; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -class ApiCatalogControllerTests { - private final String pathToContainers = "/containers"; - - private CachedServicesService cachedServicesService; - private CachedProductFamilyService cachedProductFamilyService; - private CachedApiDocService cachedApiDocService; - - private ApiCatalogController underTest; - - @BeforeEach - void setUp() { - cachedServicesService = mock(CachedServicesService.class); - cachedProductFamilyService = mock(CachedProductFamilyService.class); - cachedApiDocService = mock(CachedApiDocService.class); - - underTest = new ApiCatalogController(cachedProductFamilyService, cachedApiDocService); - standaloneSetup(underTest); - } - - @Nested - class GivenThereAreNoValidContainers { - @Nested - class WhenAllContainersAreRequested { - @Test - void thenReturnNoContent() { - given(cachedProductFamilyService.getAllContainers()).willReturn(null); - - RestAssuredMockMvc.given(). - when(). - get(pathToContainers). - then(). - statusCode(HttpStatus.NO_CONTENT.value()); - } - } - - @Nested - class WhenSpecificContainerRequested { - @Test - void thenReturnOk() { - String containerId = "service1"; - given(cachedProductFamilyService.getContainerById(containerId)).willReturn(null); - - RestAssuredMockMvc.given(). - when(). - get(pathToContainers + "/" + containerId). - then(). - statusCode(HttpStatus.OK.value()); - } - } - } - - @Nested - class GivenMultipleValidContainers { - Application service1; - Application service2; - List apiVersions; - - @BeforeEach - void prepareApplications() { - service1 = new Application("service-1"); - service1.addInstance(getStandardInstance("service1", InstanceInfo.InstanceStatus.UP)); - - service2 = new Application("service-2"); - service1.addInstance(getStandardInstance("service2", InstanceInfo.InstanceStatus.DOWN)); - - apiVersions = Arrays.asList("1.0.0", "2.0.0"); - - given(cachedServicesService.getService("service1")).willReturn(service1); - given(cachedApiDocService.getDefaultApiDocForService("service1")).willReturn("service1"); - given(cachedApiDocService.getApiVersionsForService("service1")).willReturn(apiVersions); - - given(cachedServicesService.getService("service2")).willReturn(service2); - given(cachedApiDocService.getDefaultApiDocForService("service2")).willReturn("service2"); - given(cachedApiDocService.getApiVersionsForService("service2")).willReturn(apiVersions); - - given(cachedProductFamilyService.getContainerById("api-one")).willReturn(createContainers().get(0)); - } - - @Nested - class WhenGettingAllContainers { - @Test - void thenReturnContainersWithState() { - given(cachedProductFamilyService.getAllContainers()).willReturn(createContainers()); - - RestAssuredMockMvc.given(). - when(). - get(pathToContainers). - then(). - statusCode(HttpStatus.OK.value()); - } - } - - @Nested - class WhenGettingSpecificContainer { - @Test - void thenPopulateApiDocForServices() throws ContainerStatusRetrievalThrowable { - String defaultApiVersion = "v1"; - - given(cachedApiDocService.getDefaultApiVersionForService("service1")).willReturn(defaultApiVersion); - given(cachedApiDocService.getDefaultApiVersionForService("service2")).willReturn(defaultApiVersion); - - ResponseEntity> containers = underTest.getAPIContainerById("api-one"); - - containers.getBody().forEach(apiContainer -> - apiContainer.getServices().forEach(apiService -> { - assertEquals(apiService.getServiceId(), apiService.getApiDoc()); - assertEquals(apiVersions, apiService.getApiVersions()); - assertEquals(defaultApiVersion, apiService.getDefaultApiVersion()); - })); - } - - @Test - void thenPopulateApiDocForServicesExceptOneWhichFails() throws ContainerStatusRetrievalThrowable { - given(cachedApiDocService.getDefaultApiDocForService("service2")).willThrow(new RuntimeException()); - - ResponseEntity> containers = underTest.getAPIContainerById("api-one"); - assertThereIsOneContainer(containers); - - containers.getBody().forEach(apiContainer -> - apiContainer.getServices().forEach(apiService -> { - if (apiService.getServiceId().equals("service1")) { - assertEquals(apiService.getServiceId(), apiService.getApiDoc()); - assertEquals(apiService.getApiVersions(), apiVersions); - } - if (apiService.getServiceId().equals("service2")) { - Assertions.assertNull(apiService.getApiDoc()); - } - })); - } - - @Test - void thenPopulateApiVersionsForServicesExceptOneWhichFails() throws ContainerStatusRetrievalThrowable { - given(cachedApiDocService.getApiVersionsForService("service2")).willThrow(new RuntimeException()); - - ResponseEntity> containers = underTest.getAPIContainerById("api-one"); - assertThereIsOneContainer(containers); - - containers.getBody().forEach(apiContainer -> - apiContainer.getServices().forEach(apiService -> { - if (apiService.getServiceId().equals("service1")) { - assertEquals(apiService.getServiceId(), apiService.getApiDoc()); - assertEquals(apiService.getApiVersions(), apiVersions); - } - if (apiService.getServiceId().equals("service2")) { - assertEquals(apiService.getServiceId(), apiService.getApiDoc()); - Assertions.assertNull(apiService.getApiVersions()); - } - })); - } - - private void assertThereIsOneContainer(ResponseEntity> containers) { - assertThat(containers.getBody(), is(not(nullValue()))); - assertThat(containers.getBody().size(), is(1)); - } - } - } - - @Nested - class WhenGettingSpecificService { - private final String serviceId = "service1"; - private final APIService service = new APIService.Builder(serviceId) - .secured(true) - .baseUrl("url") - .basePath("base") - .sso(false) - .apis(Collections.emptyMap()) - .build(); - - @Test - void thenReturnNotFound() { - given(cachedProductFamilyService.getServices()).willReturn(null); - - String pathToServices = "/services"; - RestAssuredMockMvc.given(). - when(). - get(pathToServices + "/" + serviceId). - then(). - statusCode(HttpStatus.NOT_FOUND.value()); - } - - @Test - void thenReturnOk() throws ContainerStatusRetrievalThrowable { - String defaultApiVersion = "v1"; - - Map services = new ConcurrentHashMap<>(); - services.put(serviceId, service); - given(cachedProductFamilyService.getServices()).willReturn(services); - - given(cachedApiDocService.getDefaultApiVersionForService(serviceId)).willReturn(defaultApiVersion); - given(cachedApiDocService.getDefaultApiDocForService(serviceId)).willReturn("mockApiDoc"); - - ResponseEntity apiServicesById = underTest.getAPIServicesById(serviceId); - assertEquals(HttpStatus.OK, apiServicesById.getStatusCode()); - assertNotNull(apiServicesById.getBody()); - assertEquals( "mockApiDoc", apiServicesById.getBody().getApiDoc()); - assertEquals("v1", apiServicesById.getBody().getDefaultApiVersion()); - } - - @Test - void thenReturnOkWithApiDocNull() throws ContainerStatusRetrievalThrowable { - String defaultApiVersion = "v1"; - - Map services = new ConcurrentHashMap<>(); - services.put(serviceId, service); - given(cachedProductFamilyService.getServices()).willReturn(services); - - given(cachedApiDocService.getDefaultApiVersionForService(serviceId)).willReturn(defaultApiVersion); - given(cachedApiDocService.getDefaultApiDocForService(serviceId)).willReturn(null); - - ResponseEntity apiServicesById = underTest.getAPIServicesById(serviceId); - assertEquals(HttpStatus.OK, apiServicesById.getStatusCode()); - assertNotNull(apiServicesById.getBody()); - assertNull(apiServicesById.getBody().getApiDoc()); - } - } - - - - - // =========================================== Helper Methods =========================================== - - private List createContainers() { - Set services = new HashSet<>(); - - APIService service = new APIService.Builder("service1") - .title("service-1") - .description("service-1") - .secured(true) - .baseUrl("url") - .homePageUrl("home") - .basePath("base") - .sso(false) - .apis(Collections.emptyMap()) - .build(); - services.add(service); - - service = new APIService.Builder("service2") - .title("service-2") - .description("service-2") - .secured(true) - .baseUrl("url") - .homePageUrl("home") - .basePath("base") - .sso(false) - .apis(Collections.emptyMap()) - .build(); - services.add(service); - - APIContainer container = new APIContainer("api-one", "API One", "This is API One", services); - - APIContainer container1 = new APIContainer("api-two", "API Two", "This is API Two", services); - - return Arrays.asList(container, container1); - } - - private InstanceInfo getStandardInstance(String serviceId, InstanceInfo.InstanceStatus status) { - return new InstanceInfo(serviceId, null, null, "192.168.0.1", null, new InstanceInfo.PortWrapper(true, 9090), - null, null, null, null, null, null, null, 0, null, "hostname", status, null, null, null, null, null, - null, null, null, null); - } - - @Nested - class OidcProviders { - - private String[] env = { - "ZWE_components_gateway_spring_security_oauth2_client_provider_oidc1_authorizationUri", - "ZWE_components_gateway_spring_security_oauth2_client_registration_oidc2_clientId", - "ZWE_components_gateway_spring_security_oauth2_client_provider_oidc1_tokenUri" - }; - - Map getEnvMap() { - try { - Class envVarClass = System.getenv().getClass(); - Field mField = envVarClass.getDeclaredField("m"); - mField.setAccessible(true); - return (Map) mField.get(System.getenv()); - } catch (NoSuchFieldException | IllegalAccessException e) { - fail(e); - return null; - } - } - - @AfterEach - void tearDown() { - Arrays.stream(env).forEach(k -> getEnvMap().remove(k)); - } - - @Test - void givenSystemEnv_whenInvokeOidcProviders_thenReturnTheList() { - Arrays.stream(env).forEach(k -> getEnvMap().put(k, "anyValue")); - List oidcProviders = RestAssuredMockMvc.given() - .when().get("/oidc/provider") - .getBody().jsonPath().getList("."); - assertEquals(2, oidcProviders.size()); - assertTrue(oidcProviders.contains("oidc1")); - assertTrue(oidcProviders.contains("oidc2")); - } - - @Test - void givenNoSystemEnv_whenInvokeOidcProviders_thenReturnAnEmptyList() { - List oidcProviders = RestAssuredMockMvc.given() - .when().get("/oidc/provider") - .getBody().jsonPath().getList("."); - assertEquals(0, oidcProviders.size()); - } - - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerApiDocNotFoundTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerApiDocNotFoundTest.java new file mode 100644 index 0000000000..8ac73f1fbf --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerApiDocNotFoundTest.java @@ -0,0 +1,62 @@ +/* + * 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.apicatalog.controllers.api; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.zowe.apiml.apicatalog.config.BeanConfig; +import org.zowe.apiml.apicatalog.controllers.handlers.CatalogApiDocControllerExceptionHandler; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; + +import static org.hamcrest.Matchers.contains; +import static org.mockito.Mockito.when; + +@ContextConfiguration(classes = { + ApiDocControllerMicroservice.class, + CatalogApiDocControllerExceptionHandler.class, + BeanConfig.class +}) +@WebFluxTest(controllers = ApiDocControllerMicroservice.class, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ApiDocControllerApiDocNotFoundTest { + + @Autowired + private WebTestClient webTestClient; + + @MockitoBean + private ApiDocService apiDocService; + + @BeforeAll + void initApiDocRetrievalService() { + when(apiDocService.retrieveApiDoc("service2", "v1")) + .thenThrow(new ApiDocNotFoundException("Really bad stuff happened")); + + when(apiDocService.retrieveApiDoc("service2", null)) + .thenThrow(new ApiDocNotFoundException("Really bad stuff happened")); + } + + @Test + void getApiDocAndFailThenThrowApiDocNotFoundException() { + webTestClient.get().uri("/apicatalog/apidoc/service2/v1").exchange() + .expectStatus().isNotFound() + .expectBody().jsonPath("$.messages[?(@.messageNumber == 'ZWEAC103E')].messageContent") + .value(contains("API Documentation not retrieved, Really bad stuff happened")); + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerServiceNotFoundTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerServiceNotFoundTest.java new file mode 100644 index 0000000000..fe6a1c7aed --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerServiceNotFoundTest.java @@ -0,0 +1,59 @@ +/* + * 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.apicatalog.controllers.api; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.zowe.apiml.apicatalog.config.BeanConfig; +import org.zowe.apiml.apicatalog.controllers.handlers.CatalogApiDocControllerExceptionHandler; +import org.zowe.apiml.apicatalog.exceptions.ServiceNotFoundException; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; + +import static org.hamcrest.Matchers.contains; +import static org.mockito.Mockito.when; + +@WebFluxTest(controllers = ApiDocControllerMicroservice.class, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +@ContextConfiguration(classes = { + ApiDocControllerMicroservice.class, + CatalogApiDocControllerExceptionHandler.class, + BeanConfig.class +}) +@TestInstance(TestInstance.Lifecycle. PER_CLASS) +class ApiDocControllerServiceNotFoundTest { + + @Autowired + private WebTestClient webTestClient; + + @MockitoBean + private ApiDocService apiDocService; + + @BeforeAll + void initApiDocRetrievalService() { + when(apiDocService.retrieveApiDoc("service1", "v1")) + .thenThrow(new ServiceNotFoundException("API Documentation not retrieved, The service is running.")); + } + + @Test + void getApiDocForServiceDown() { + webTestClient.get().uri("/apicatalog/apidoc/service1/v1").exchange() + .expectStatus().isNotFound() + .expectBody().jsonPath("$.messages[?(@.messageNumber == 'ZWEAC706E')].messageContent") + .value(contains("Service not located, API Documentation not retrieved, The service is running.")); + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerTest.java new file mode 100644 index 0000000000..6b78c5e5ae --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiDocControllerTest.java @@ -0,0 +1,136 @@ +/* + * 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.apicatalog.controllers.api; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.openapitools.openapidiff.core.OpenApiCompare; +import org.openapitools.openapidiff.core.compare.OpenApiDiffOptions; +import org.openapitools.openapidiff.core.model.ChangedOpenApi; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ApiDocControllerTest { + + private static final String API_DOC = "Some API Doc"; + + private ApiDocService mockApiDocService; + private ApiDocController underTest; + + @BeforeEach + void setup() { + mockApiDocService = mock(ApiDocService.class); + underTest = new ApiDocController(mockApiDocService); + } + + @Test + void whenCreateController_thenItIsInstantiated() { + assertNotNull(underTest); + } + + @Nested + class GivenService { + + @Nested + class WhenGetApiDocByVersion { + + @Test + void givenApiDoc_thenReturnApiDoc() { + when(mockApiDocService.retrieveApiDoc("service", "1.0.0")).thenReturn(Mono.just(API_DOC)); + + var elapsed = StepVerifier.create(underTest.getApiDocInfo("service", "1.0.0")) + .assertNext(res -> { + assertNotNull(res); + assertEquals(API_DOC, res.getBody()); + }) + .verifyComplete(); + assertEquals(0L, elapsed.getSeconds()); + } + + @Test + void givenNoApiDoc_thenThrowException() { + when(mockApiDocService.retrieveApiDoc("service", "1.0.0")).thenThrow(new ApiDocNotFoundException("error")); + + var elapsed = StepVerifier.create(Mono.defer(() -> underTest.getApiDocInfo("service", "1.0.0"))) + .expectErrorMatches(ApiDocNotFoundException.class::isInstance) + .verify(); + assertEquals(0L, elapsed.toSeconds()); + } + + } + + @Nested + class WhenGetApiDocVersionDefault { + + @Test + void givenApiDocExists_thenReturnIt() { + when(mockApiDocService.retrieveDefaultApiDoc("service")).thenReturn(Mono.just(API_DOC)); + + var elapsed = StepVerifier.create(underTest.getDefaultApiDocInfo("service")) + .assertNext(res -> { + assertNotNull(res); + assertEquals(API_DOC, res.getBody()); + }) + .verifyComplete(); + assertEquals(0L, elapsed.getSeconds()); + } + + @Test + void givenNoApiDocExists_thenThrowException() { + when(mockApiDocService.retrieveDefaultApiDoc("service")).thenThrow(new ApiDocNotFoundException("error")); + + var elapsed = StepVerifier.create(Mono.defer(() -> underTest.getDefaultApiDocInfo("service"))) + .expectErrorMatches(ApiDocNotFoundException.class::isInstance) + .verify(); + assertEquals(0L, elapsed.toSeconds()); + } + + } + + @Test + void whenGetApiDiff_thenReturnApiDiffHtml() { + ChangedOpenApi changedOpenApi = new ChangedOpenApi(OpenApiDiffOptions.builder().build()); + changedOpenApi.setChangedOperations(Collections.emptyList()); + changedOpenApi.setMissingEndpoints(Collections.emptyList()); + changedOpenApi.setNewEndpoints(Collections.emptyList()); + doReturn(Mono.just("doc1")).when(mockApiDocService).retrieveApiDoc("service", "v1"); + doReturn(Mono.just("doc2")).when(mockApiDocService).retrieveApiDoc("service", "v2"); + + try (MockedStatic openApiCompare = Mockito.mockStatic(OpenApiCompare.class)) { + openApiCompare.when(() -> OpenApiCompare.fromContents("doc1", "doc2")).thenReturn(changedOpenApi); + var elapsed = StepVerifier.create(underTest.getApiDiff("service", "v1", "v2")) + .assertNext(res -> { + assertNotNull(res); + assertTrue(res.getBody().contains("Api Change Log")); + }) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); + } + + } + + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTest.java deleted file mode 100644 index b4dbc2d526..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.security.config.annotation.web.WebSecurityConfigurer; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.MockMvc; - -import static org.hamcrest.Matchers.hasItem; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(controllers = {CatalogApiDocController.class}, - excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebSecurityConfigurer.class) }, - excludeAutoConfiguration = { SecurityAutoConfiguration.class} -) -@ContextConfiguration(classes = CatalogApiDocControllerApiDocNotFoundTestContextConfiguration.class) -class CatalogApiDocControllerApiDocNotFoundTest { - - @Autowired - private MockMvc mockMvc; - - @Test - void getApiDocAndFailThenThrowApiDocNotFoundException() throws Exception { - this.mockMvc.perform(get("/apidoc/service2/v1")) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.messages[?(@.messageNumber == 'ZWEAC103E')].messageContent", - hasItem("API Documentation not retrieved, Really bad stuff happened"))); - } - - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTestContextConfiguration.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTestContextConfiguration.java deleted file mode 100644 index ac4ec2b36e..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerApiDocNotFoundTestContextConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.springframework.context.annotation.Bean; -import org.zowe.apiml.apicatalog.controllers.handlers.CatalogApiDocControllerExceptionHandler; -import org.zowe.apiml.apicatalog.services.status.APIServiceStatusService; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; - -import static org.mockito.Mockito.*; - -class CatalogApiDocControllerApiDocNotFoundTestContextConfiguration { - - @Bean - public APIServiceStatusService apiServiceStatusService() { - return mock(APIServiceStatusService.class); - } - - @Bean - public CatalogApiDocController catalogApiDocController(APIServiceStatusService apiServiceStatusService) { - when(apiServiceStatusService.getServiceCachedApiDocInfo("service2", "v1")) - .thenThrow(new ApiDocNotFoundException("Really bad stuff happened")); - - when(apiServiceStatusService.getServiceCachedApiDocInfo("service2", null)) - .thenThrow(new ApiDocNotFoundException("Really bad stuff happened")); - - verify(apiServiceStatusService, never()).getServiceCachedApiDocInfo("service2", "v1"); - - return new CatalogApiDocController(apiServiceStatusService); - } - - @Bean - public MessageService messageService() { - return new YamlMessageService("/apicatalog-log-messages.yml"); - } - - @Bean - public CatalogApiDocControllerExceptionHandler catalogApiDocControllerExceptionHandler(MessageService messageService) { - return new CatalogApiDocControllerExceptionHandler(messageService); - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTest.java deleted file mode 100644 index 23bba246fb..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.security.config.annotation.web.WebSecurityConfigurer; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; - -import static org.hamcrest.Matchers.hasItem; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(SpringExtension.class) -@WebMvcTest(controllers = {CatalogApiDocController.class}, - excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebSecurityConfigurer.class) }, - excludeAutoConfiguration = { SecurityAutoConfiguration.class} -) -@ContextConfiguration(classes = CatalogApiDocControllerServiceNotFoundTestContextConfiguration.class) -class CatalogApiDocControllerServiceNotFoundTest { - - @Autowired - private MockMvc mockMvc; - - @Test - void getApiDocForServiceDown() throws Exception { - this.mockMvc.perform(get("/apidoc/service1/v1")) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.messages[?(@.messageNumber == 'ZWEAC706E')].messageContent", - hasItem("Service not located, API Documentation not retrieved, The service is running."))); - } - - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTestContextConfiguration.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTestContextConfiguration.java deleted file mode 100644 index 65053eb455..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerServiceNotFoundTestContextConfiguration.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.springframework.context.annotation.Bean; -import org.zowe.apiml.apicatalog.controllers.handlers.CatalogApiDocControllerExceptionHandler; -import org.zowe.apiml.apicatalog.services.status.APIServiceStatusService; -import org.zowe.apiml.apicatalog.services.status.model.ServiceNotFoundException; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; - -import static org.mockito.Mockito.*; - -class CatalogApiDocControllerServiceNotFoundTestContextConfiguration { - - @Bean - public APIServiceStatusService apiServiceStatusService() { - return mock(APIServiceStatusService.class); - } - - @Bean - public CatalogApiDocController catalogApiDocController(APIServiceStatusService apiServiceStatusService) { - when(apiServiceStatusService.getServiceCachedApiDocInfo("service1", "v1")) - .thenThrow(new ServiceNotFoundException("API Documentation not retrieved, The service is running.")); - - verify(apiServiceStatusService, never()).getServiceCachedApiDocInfo("service1", "v1"); - - return new CatalogApiDocController(apiServiceStatusService); - } - - @Bean - public MessageService messageService() { - return new YamlMessageService("/apicatalog-log-messages.yml"); - } - - @Bean - public CatalogApiDocControllerExceptionHandler catalogApiDocControllerExceptionHandler() { - return new CatalogApiDocControllerExceptionHandler(messageService()); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerTest.java deleted file mode 100644 index f054a26fd3..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/CatalogApiDocControllerTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.zowe.apiml.apicatalog.services.status.APIServiceStatusService; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; - -class CatalogApiDocControllerTest { - - private APIServiceStatusService mockApiServiceStatusService; - private CatalogApiDocController underTest; - - @BeforeEach - void setup() { - mockApiServiceStatusService = Mockito.mock(APIServiceStatusService.class); - underTest = new CatalogApiDocController(mockApiServiceStatusService); - } - - @Test - void whenCreateController_thenItIsInstantiated() { - assertNotNull(underTest); - } - - @Nested - class GivenService { - @Nested - class WhenGetApiDocByVersion { - @Test - void givenApiDoc_thenReturnApiDoc() { - ResponseEntity response = new ResponseEntity<>("Some API Doc", HttpStatus.OK); - when(mockApiServiceStatusService.getServiceCachedApiDocInfo("service", "1.0.0")).thenReturn(response); - - ResponseEntity res = underTest.getApiDocInfo("service", "1.0.0"); - assertNotNull(res); - assertEquals("Some API Doc", res.getBody()); - } - - @Test - void givenNoApiDoc_thenThrowException() { - when(mockApiServiceStatusService.getServiceCachedApiDocInfo("service", "1.0.0")).thenThrow(new ApiDocNotFoundException("error")); - assertThrows(ApiDocNotFoundException.class, () -> underTest.getApiDocInfo("service", "1.0.0")); - } - } - - @Nested - class WhenGetApiDocVersionDefault { - @Test - void givenApiDocExists_thenReturnIt() { - ResponseEntity response = new ResponseEntity<>("Some API Doc", HttpStatus.OK); - when(mockApiServiceStatusService.getServiceCachedDefaultApiDocInfo("service")).thenReturn(response); - - ResponseEntity res = underTest.getDefaultApiDocInfo("service"); - assertNotNull(res); - assertEquals("Some API Doc", res.getBody()); - } - - @Test - void givenNoApiDocExists_thenThrowException() { - when(mockApiServiceStatusService.getServiceCachedDefaultApiDocInfo("service")).thenThrow(new ApiDocNotFoundException("error")); - assertThrows(ApiDocNotFoundException.class, () -> underTest.getDefaultApiDocInfo("service")); - } - } - - @Test - void whenGetApiDiff_thenReturnApiDiffHtml() { - String responseString = "Some Diff"; - ResponseEntity response = new ResponseEntity<>("Some Diff", HttpStatus.OK); - - when(mockApiServiceStatusService.getApiDiffInfo("service", "v1", "v2")).thenReturn(response); - ResponseEntity res = underTest.getApiDiff("service", "v1", "v2"); - assertNotNull(res); - assertEquals(responseString, res.getBody()); - } - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ImageControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ImageControllerTest.java index 4c7ce1991e..4cbee4c5f8 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ImageControllerTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ImageControllerTest.java @@ -10,87 +10,78 @@ package org.zowe.apiml.apicatalog.controllers.api; -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.InjectMocks; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(MockitoExtension.class) +@WebFluxTest(controllers = ImageControllerMicroservice.class, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +@ContextConfiguration(classes = ImageControllerMicroservice.class) class ImageControllerTest { - private MockMvc mockMvc; - - @InjectMocks - private ImageController imageController; + @Autowired + private WebTestClient webTestClient; - @BeforeEach - void setUp() { - mockMvc = MockMvcBuilders.standaloneSetup(imageController).build(); - } + @Autowired + private ImageControllerMicroservice imageController; @Nested class GivenImageEndpointRequest { + @Nested class WhenPngFormat { @Test - void thenDownloadImage() throws Exception { + void thenDownloadImage() { ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.png"); - mockMvc.perform(get("/custom-logo")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.IMAGE_PNG)) - .andReturn(); + webTestClient.get().uri("/apicatalog/custom-logo").exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.IMAGE_PNG); } } @Nested class WhenJpegFormat { @Test - void thenDownloadImage() throws Exception { + void thenDownloadImage() { ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.jpeg"); - mockMvc.perform(get("/custom-logo")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.IMAGE_JPEG)) - .andReturn(); + webTestClient.get().uri("/apicatalog/custom-logo").exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.IMAGE_JPEG); ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.jpg"); - mockMvc.perform(get("/custom-logo")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.IMAGE_JPEG)) - .andReturn(); + webTestClient.get().uri("/apicatalog/custom-logo").exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.IMAGE_JPEG); } } @Nested class WhenSvgFormat { @Test - void thenDownloadImage() throws Exception { + void thenDownloadImage() { ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.svg"); - mockMvc.perform(get("/custom-logo")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.valueOf("image/svg+xml"))) - .andReturn(); + webTestClient.get().uri("/apicatalog/custom-logo").exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.valueOf("image/svg+xml")); } } @Test - void thenReturnFileNotFound() throws Exception { + void thenReturnFileNotFound() { ReflectionTestUtils.setField(imageController, "image", "wrong/path/img.png"); - mockMvc.perform(get("/custom-logo")) - .andExpect(status().isNotFound()); + webTestClient.get().uri("/apicatalog/custom-logo").exchange() + .expectStatus().isNotFound(); } + } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/MockControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/MockControllerTest.java deleted file mode 100644 index 310774e38d..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/MockControllerTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.controllers.api; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.zowe.apiml.apicatalog.standalone.ExampleService; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest( - controllers = { MockController.class }, - excludeAutoConfiguration = { SecurityAutoConfiguration.class} -) -@ContextConfiguration(classes = MockControllerTest.Context.class) -@ActiveProfiles("test") -class MockControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ExampleService exampleService; - - @Nested - class GivenEnabledController { - - @Test - void whenGetRequest() throws Exception { - mockMvc.perform(get("/mock/something")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(content().string("{}")); - verify(exampleService).replyExample(any(), eq("GET"), eq("/something")); - } - - @Test - void whenPostRequest() throws Exception { - mockMvc.perform(post("/mock/something/else")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(content().string("{}")); - verify(exampleService).replyExample(any(), eq("POST"), eq("/something/else")); - } - - } - - @Configuration - @Profile("test") - static class Context { - - @Bean - public ExampleService exampleService() { - return spy(new ExampleService()); - } - - @Bean - public MockController mockController(ExampleService exampleService) { - return new MockController(exampleService); - } - - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/OidcControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/OidcControllerTest.java new file mode 100644 index 0000000000..2799d49ec1 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/OidcControllerTest.java @@ -0,0 +1,91 @@ +/* + * 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.apicatalog.controllers.api; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.reactive.server.WebTestClient; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +@WebFluxTest(controllers = OidcControllerMicroservice.class, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +@ContextConfiguration(classes = OidcControllerMicroservice.class) +class OidcControllerTest { + + @Autowired + private WebTestClient webTestClient; + + @Autowired + private OidcControllerMicroservice oidcController; + + @Nested + class OidcProviders { + + private String[] env = { + "ZWE_components_gateway_spring_security_oauth2_client_provider_oidc1_authorizationUri", + "ZWE_components_gateway_spring_security_oauth2_client_registration_oidc2_clientId", + "ZWE_components_gateway_spring_security_oauth2_client_provider_oidc1_tokenUri" + }; + + Map getEnvMap() { + try { + Class envVarClass = System.getenv().getClass(); + Field mField = envVarClass.getDeclaredField("m"); + mField.setAccessible(true); + return (Map) mField.get(System.getenv()); + } catch (NoSuchFieldException | IllegalAccessException e) { + fail(e); + return null; + } + } + + @BeforeEach + @AfterEach + void cleanUp() { + Arrays.stream(env).forEach(k -> getEnvMap().remove(k)); + ((AtomicReference>) ReflectionTestUtils.getField(oidcController, "oidcProviderCache")).set(null); + } + + @Test + void givenSystemEnv_whenInvokeOidcProviders_thenReturnTheList() { + Arrays.stream(env).forEach(k -> getEnvMap().put(k, "anyValue")); + List oidcProviders = webTestClient + .get().uri("/apicatalog/oidc/provider").exchange() + .returnResult(List.class).getResponseBody().blockFirst(); + assertEquals(2, oidcProviders.size()); + assertTrue(oidcProviders.contains("oidc1")); + assertTrue(oidcProviders.contains("oidc2")); + } + + @Test + void givenNoSystemEnv_whenInvokeOidcProviders_thenReturnAnEmptyList() { + List oidcProviders = webTestClient + .get().uri("/apicatalog/oidc/provider").exchange() + .returnResult(List.class).getResponseBody().blockFirst(); + assertEquals(0, oidcProviders.size()); + } + + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerContainerRetrievalTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerContainerRetrievalTest.java new file mode 100644 index 0000000000..49806708ad --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerContainerRetrievalTest.java @@ -0,0 +1,62 @@ +/* + * 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.apicatalog.controllers.api; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.zowe.apiml.apicatalog.config.BeanConfig; +import org.zowe.apiml.apicatalog.controllers.handlers.ApiCatalogControllerExceptionHandler; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; +import org.zowe.apiml.apicatalog.swagger.ContainerService; + +import static org.hamcrest.Matchers.contains; +import static org.mockito.Mockito.when; + +@ContextConfiguration(classes = { + ServicesControllerMicroservice.class, + ApiCatalogControllerExceptionHandler.class, + BeanConfig.class +}) +@WebFluxTest(controllers = ServicesControllerMicroservice.class, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ServicesControllerContainerRetrievalTest { + + @Autowired + private WebTestClient webTestClient; + + @MockitoBean + private ApiDocService apiDocService; + + @MockitoBean + private ContainerService containerService; + + @BeforeAll + void initContainerService() { + when(containerService.getAllContainers()) + .thenThrow(new NullPointerException()); + } + + @Test + void getContainers() { + webTestClient.get().uri("/apicatalog/containers").exchange() + .expectStatus().is5xxServerError() + .expectBody().jsonPath("$.messages[?(@.messageNumber == 'ZWEAC104E')].messageContent") + .value(contains("Could not retrieve container statuses, java.lang.NullPointerException")); + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerTests.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerTests.java new file mode 100644 index 0000000000..bd483fa000 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ServicesControllerTests.java @@ -0,0 +1,372 @@ +/* + * 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.apicatalog.controllers.api; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.shared.Application; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.zowe.apiml.apicatalog.config.BeanConfig; +import org.zowe.apiml.apicatalog.controllers.handlers.DefaultExceptionHandler; +import org.zowe.apiml.apicatalog.controllers.handlers.ApiCatalogControllerExceptionHandler; +import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalException; +import org.zowe.apiml.apicatalog.model.APIContainer; +import org.zowe.apiml.apicatalog.model.APIService; +import org.zowe.apiml.apicatalog.model.CustomStyleConfig; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; +import org.zowe.apiml.apicatalog.swagger.ContainerService; +import org.zowe.apiml.product.gateway.GatewayClient; +import org.zowe.apiml.product.instance.ServiceAddress; +import org.zowe.apiml.security.common.error.AuthExceptionHandler; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_ID; + +@WebFluxTest(controllers = ServicesControllerMicroservice.class, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +@ContextConfiguration(classes = { + ServicesControllerMicroservice.class, + ApiCatalogControllerExceptionHandler.class, + ContainerService.class, + DefaultExceptionHandler.class, + AuthExceptionHandler.class, + ServicesControllerTests.Context.class, + BeanConfig.class +}) +class ServicesControllerTests { + + private final String pathToContainers = "/apicatalog/containers"; + + @Autowired + private WebTestClient webTestClient; + + @Autowired + private ServicesController underTest; + + @MockitoBean + private DiscoveryClient discoveryClient; + + @MockitoBean + private CustomStyleConfig customStyleConfig; + + @MockitoSpyBean + private ContainerService containerService; + + @MockitoBean + private ApiDocService apiDocService; + + @Nested + class GivenThereAreNoValidContainers { + + @Nested + class WhenAllContainersAreRequested { + + @Test + void thenReturnNoContent() { + given(containerService.getAllContainers()).willReturn(null); + + webTestClient.get().uri(pathToContainers).exchange() + .expectStatus().isNoContent(); + } + + } + + @Nested + class WhenSpecificContainerRequested { + + private static final String SERVICE_ID = "service1"; + + @Test + void ifExistingInstanceThenReturnOk() { + var instance = InstanceInfo.Builder.newBuilder().setAppName(SERVICE_ID).setInstanceId("instance1").setMetadata(Map.of(CATALOG_ID, SERVICE_ID)).build(); + doReturn(Collections.singletonList(new EurekaServiceInstance(instance))).when(discoveryClient).getInstances(SERVICE_ID); + doReturn(Collections.singletonList(SERVICE_ID)).when(discoveryClient).getServices(); + doReturn(Mono.empty()).when(apiDocService).retrieveDefaultApiDoc(SERVICE_ID); + + webTestClient.get().uri(pathToContainers + "/" + SERVICE_ID).exchange() + .expectStatus().isOk(); + } + + @Test + void ifNonExistingInstanceThenReturnNotFound() { + given(containerService.getContainerById(SERVICE_ID)).willReturn(null); + + webTestClient.get().uri(pathToContainers + "/" + SERVICE_ID).exchange() + .expectStatus().isNotFound(); + } + + } + + } + + @Nested + class GivenMultipleValidContainers { + + Application service1; + Application service2; + List apiVersions; + + @BeforeEach + void prepareApplications() { + apiVersions = Arrays.asList("1.0.0", "2.0.0"); + + given(discoveryClient.getInstances("service1")).willReturn( + Collections.singletonList(new EurekaServiceInstance(getStandardInstance("service1", InstanceInfo.InstanceStatus.UP))) + ); + given(apiDocService.retrieveDefaultApiDoc("service1")).willReturn(Mono.just("service1")); + given(apiDocService.retrieveApiVersions("service1")).willReturn(apiVersions); + + given(discoveryClient.getInstances("service2")).willReturn( + Collections.singletonList(new EurekaServiceInstance(getStandardInstance("service2", InstanceInfo.InstanceStatus.DOWN))) + ); + given(apiDocService.retrieveDefaultApiDoc("service2")).willReturn(Mono.just("service2")); + given(apiDocService.retrieveApiVersions("service2")).willReturn(apiVersions); + + given(containerService.getContainerById("api-one")).willReturn(createContainers().get(0)); + } + + @Nested + class WhenGettingAllContainers { + + @Test + void thenReturnContainersWithState() { + given(containerService.getAllContainers()).willReturn(createContainers()); + + webTestClient.get().uri(pathToContainers).exchange() + .expectStatus().isOk(); + } + + } + + @Nested + class WhenGettingSpecificContainer { + + @Test + void thenPopulateApiDocForServices() throws ContainerStatusRetrievalException { + String defaultApiVersion = "v1"; + + given(apiDocService.retrieveDefaultApiVersion("service1")).willReturn(defaultApiVersion); + given(apiDocService.retrieveDefaultApiVersion("service2")).willReturn(defaultApiVersion); + + var elapsed = StepVerifier.create(underTest.getAPIContainerById("api-one")) + .assertNext(containers -> { + containers.getBody().forEach(apiContainer -> + apiContainer.getServices().forEach(apiService -> { + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + assertEquals(apiVersions, apiService.getApiVersions()); + assertEquals(defaultApiVersion, apiService.getDefaultApiVersion()); + })); + }) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void thenPopulateApiDocForServicesExceptOneWhichFails() throws ContainerStatusRetrievalException { + given(apiDocService.retrieveDefaultApiDoc("service2")).willReturn(Mono.error(new RuntimeException())); + + var elapsed = StepVerifier.create(underTest.getAPIContainerById("api-one")) + .assertNext(containers -> { + assertThereIsOneContainer(containers); + containers.getBody().forEach(apiContainer -> + apiContainer.getServices().forEach(apiService -> { + if (apiService.getServiceId().equals("service1")) { + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + assertEquals(apiService.getApiVersions(), apiVersions); + } + if (apiService.getServiceId().equals("service2")) { + Assertions.assertNull(apiService.getApiDoc()); + } + })); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void thenPopulateApiVersionsForServicesExceptOneWhichFails() throws ContainerStatusRetrievalException { + given(apiDocService.retrieveApiVersions("service2")).willThrow(new RuntimeException()); + + var elapsed = StepVerifier.create(underTest.getAPIContainerById("api-one")) + .assertNext(containers -> { + assertThereIsOneContainer(containers); + + containers.getBody().forEach(apiContainer -> + apiContainer.getServices().forEach(apiService -> { + if (apiService.getServiceId().equals("service1")) { + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + assertEquals(apiService.getApiVersions(), apiVersions); + } + if (apiService.getServiceId().equals("service2")) { + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + Assertions.assertNull(apiService.getApiVersions()); + } + })); + }) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); + } + + private void assertThereIsOneContainer(ResponseEntity> containers) { + assertThat(containers.getBody(), is(not(nullValue()))); + assertThat(containers.getBody().size(), is(1)); + } + + } + + } + + @Nested + class WhenGettingSpecificService { + + private final String serviceId = "service1"; + private final APIService service = new APIService.Builder(serviceId) + .secured(true) + .baseUrl("url") + .basePath("base") + .sso(false) + .apis(Collections.emptyMap()) + .build(); + + @Test + void thenReturnNotFound() { + given(discoveryClient.getServices()).willReturn(Collections.emptyList()); + + String pathToServices = "/apicatalog/services"; + webTestClient.get().uri(pathToServices + "/" + serviceId).exchange() + .expectStatus().isNotFound(); + } + + @Test + void thenReturnOk() { + var defaultApiVersion = "v1"; + + given(containerService.getService(serviceId)).willReturn(service); + given(apiDocService.retrieveDefaultApiVersion(serviceId)).willReturn(defaultApiVersion); + given(apiDocService.retrieveDefaultApiDoc(serviceId)).willReturn(Mono.just("mockApiDoc")); + + var elapsed = StepVerifier.create(underTest.getAPIServicesById(serviceId)) + .assertNext(apiServicesById -> { + assertEquals(HttpStatus.OK, apiServicesById.getStatusCode()); + assertNotNull(apiServicesById.getBody()); + assertEquals( "mockApiDoc", apiServicesById.getBody().getApiDoc()); + assertEquals("v1", apiServicesById.getBody().getDefaultApiVersion()); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void thenReturnOkWithoutApiDoc() { + var defaultApiVersion = "v1"; + + given(containerService.getService(serviceId)).willReturn(service); + given(apiDocService.retrieveDefaultApiVersion(serviceId)).willReturn(defaultApiVersion); + given(apiDocService.retrieveDefaultApiDoc(serviceId)).willReturn(Mono.empty()); + + var elapsed = StepVerifier.create(underTest.getAPIServicesById(serviceId)) + .assertNext(apiServicesById -> { + assertEquals(HttpStatus.OK, apiServicesById.getStatusCode()); + assertNotNull(apiServicesById.getBody()); + assertNull(apiServicesById.getBody().getApiDoc()); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + } + + // =========================================== Helper Methods =========================================== + + private List createContainers() { + Set services = new HashSet<>(); + + APIService service = new APIService.Builder("service1") + .title("service-1") + .description("service-1") + .secured(true) + .baseUrl("url") + .homePageUrl("home") + .basePath("base") + .sso(false) + .apis(Collections.emptyMap()) + .build(); + services.add(service); + + service = new APIService.Builder("service2") + .title("service-2") + .description("service-2") + .secured(true) + .baseUrl("url") + .homePageUrl("home") + .basePath("base") + .sso(false) + .apis(Collections.emptyMap()) + .build(); + services.add(service); + + APIContainer container = new APIContainer("api-one", "API One", "This is API One", services); + + APIContainer container1 = new APIContainer("api-two", "API Two", "This is API Two", services); + + return Arrays.asList(container, container1); + } + + private InstanceInfo getStandardInstance(String serviceId, InstanceInfo.InstanceStatus status) { + return new InstanceInfo(serviceId, null, null, "192.168.0.1", null, new InstanceInfo.PortWrapper(true, 9090), + null, null, null, null, null, null, null, 0, null, "hostname", status, null, null, null, null, null, + null, null, null, null); + } + + @TestConfiguration + static class Context { + + @Bean + GatewayClient gatewayClient() { + return new GatewayClient(ServiceAddress.builder().scheme("https").hostname("localhost").build()); + } + + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java new file mode 100644 index 0000000000..e5bf992988 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java @@ -0,0 +1,97 @@ +/* + * 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.apicatalog.controllers.api; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.client.RestClientException; +import org.zowe.apiml.apicatalog.config.BeanConfig; +import org.zowe.apiml.apicatalog.controllers.handlers.StaticAPIRefreshControllerExceptionHandler; +import org.zowe.apiml.apicatalog.controllers.handlers.StaticDefinitionControllerExceptionHandler; +import org.zowe.apiml.apicatalog.exceptions.ServiceNotFoundException; +import org.zowe.apiml.apicatalog.staticapi.StaticAPIResponse; +import org.zowe.apiml.apicatalog.staticapi.StaticAPIService; +import org.zowe.apiml.apicatalog.staticapi.StaticDefinitionGenerator; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.when; + +@ContextConfiguration(classes = { + StaticAPIRefreshControllerMicroservice.class, + StaticDefinitionControllerMicroservice.class, + StaticAPIRefreshControllerExceptionHandler.class, + StaticDefinitionControllerExceptionHandler.class, + BeanConfig.class +}) +@WebFluxTest(controllers = {StaticAPIRefreshControllerMicroservice.class, StaticDefinitionControllerMicroservice.class}, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +class StaticAPIRefreshControllerTest { + + private static final String API_REFRESH_ENDPOINT = "/apicatalog/static-api/refresh"; + + @Autowired + private WebTestClient webTestClient; + + @MockitoBean + private StaticAPIService staticAPIService; + + @MockitoBean + private StaticDefinitionGenerator staticDefinitionGenerator; + + @Test + void givenServiceNotFoundException_whenCallRefreshAPI_thenResponseShouldBe503WithSpecificMessage() { + when(staticAPIService.refresh()).thenThrow( + new ServiceNotFoundException("Exception") + ); + + webTestClient.post().uri(API_REFRESH_ENDPOINT).exchange() + .expectStatus().isEqualTo(HttpStatus.SC_SERVICE_UNAVAILABLE) + .expectBody() + .jsonPath("$.messages").value(hasSize(1)) + .jsonPath("$.messages[0].messageType").value(equalTo("ERROR")) + .jsonPath("$.messages[0].messageNumber").value(equalTo("ZWEAC706E")) + .jsonPath("$.messages[0].messageContent").value(equalTo("Service not located, discovery")) + .jsonPath("$.messages[0].messageKey").value(equalTo("org.zowe.apiml.apicatalog.serviceNotFound")); + } + + @Test + void givenRestClientException_whenCallRefreshAPI_thenResponseShouldBe500WithSpecificMessage() { + when(staticAPIService.refresh()).thenThrow( + new RestClientException("Exception") + ); + + webTestClient.post().uri(API_REFRESH_ENDPOINT).exchange() + .expectStatus().isEqualTo(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .expectBody() + .jsonPath("$.messages").value(hasSize(1)) + .jsonPath("$.messages[0].messageType").value(equalTo("ERROR")) + .jsonPath("$.messages[0].messageNumber").value(equalTo("ZWEAC707E")) + .jsonPath("$.messages[0].messageContent").value(equalTo("Static API refresh failed, caused by exception: org.springframework.web.client.RestClientException: Exception")) + .jsonPath("$.messages[0].messageKey").value(equalTo("org.zowe.apiml.apicatalog.StaticApiRefreshFailed")); + } + + @Test + void givenSuccessStaticResponse_whenCallRefreshAPI_thenResponseCodeShouldBe200() { + when(staticAPIService.refresh()).thenReturn( + new StaticAPIResponse(200, "This is body") + ); + + webTestClient.post().uri(API_REFRESH_ENDPOINT).exchange() + .expectStatus().isOk(); + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticDefinitionControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticDefinitionControllerTest.java new file mode 100644 index 0000000000..58ad4907df --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticDefinitionControllerTest.java @@ -0,0 +1,190 @@ +/* + * 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.apicatalog.controllers.api; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.zowe.apiml.apicatalog.config.BeanConfig; +import org.zowe.apiml.apicatalog.controllers.handlers.StaticDefinitionControllerExceptionHandler; +import org.zowe.apiml.apicatalog.staticapi.StaticAPIResponse; +import org.zowe.apiml.apicatalog.staticapi.StaticDefinitionGenerator; + +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.when; + +@ContextConfiguration(classes = { + StaticDefinitionControllerMicroservice.class, + StaticDefinitionControllerExceptionHandler.class, + BeanConfig.class +}) +@WebFluxTest(controllers = StaticDefinitionControllerMicroservice.class, excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) +class StaticDefinitionControllerTest { + + private static final String STATIC_DEF_GENERATE_ENDPOINT = "/apicatalog/static-api/generate"; + private static final String STATIC_DEF_OVERRIDE_ENDPOINT = "/apicatalog/static-api/override"; + private static final String STATIC_DEF_DELETE_ENDPOINT = "/apicatalog/static-api/delete"; + + @Autowired + private WebTestClient webTestClient; + + @MockitoBean + private StaticDefinitionGenerator staticDefinitionGenerator; + + @Nested + class GivenIOException { + + @Nested + class whenCallStaticGenerationAPI { + + @Test + void thenResponseShouldBe500WithSpecificMessage() throws Exception { + when(staticDefinitionGenerator.generateFile("services", "test")).thenThrow( + new IOException("Exception") + ); + + webTestClient.post().uri(STATIC_DEF_GENERATE_ENDPOINT) + .header("Service-Id", "test") + .bodyValue("services") + .exchange() + .expectStatus().isEqualTo(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .expectBody() + .jsonPath("$.messages").value(hasSize(1)) + .jsonPath("$.messages[0].messageType").value(equalTo("ERROR")) + .jsonPath("$.messages[0].messageNumber").value(equalTo("ZWEAC709E")) + .jsonPath("$.messages[0].messageContent").value(equalTo("Static definition generation failed, caused by exception: java.io.IOException: Exception")) + .jsonPath("$.messages[0].messageKey").value(equalTo("org.zowe.apiml.apicatalog.StaticDefinitionGenerationFailed")); + } + + } + + } + + @Nested + class GivenRequestWithNoContent { + + @Nested + class whenCallStaticGenerationAPI { + + @Test + void thenResponseIs400() { + webTestClient.post().uri(STATIC_DEF_GENERATE_ENDPOINT).exchange() + .expectStatus().isBadRequest(); + } + + } + + } + + @Nested + class GivenFileAlreadyExistsException { + + @Nested + class whenCallStaticGenerationAPI { + + @Test + void thenResponseShouldBe409WithSpecificMessage() throws Exception { + when(staticDefinitionGenerator.generateFile("invalid", "test")).thenThrow( + new FileAlreadyExistsException("Exception") + ); + + webTestClient.post().uri(STATIC_DEF_GENERATE_ENDPOINT) + .header("Service-Id", "test") + .bodyValue("invalid") + .exchange() + .expectBody() + .jsonPath("$.messages").value(hasSize(1)) + .jsonPath("$.messages[0].messageType").value(equalTo("ERROR")) + .jsonPath("$.messages[0].messageNumber").value(equalTo("ZWEAC709E")) + .jsonPath("$.messages[0].messageContent").value(equalTo("Static definition generation failed, caused by exception: java.nio.file.FileAlreadyExistsException: Exception")) + .jsonPath("$.messages[0].messageKey").value(equalTo("org.zowe.apiml.apicatalog.StaticDefinitionGenerationFailed")); + } + + } + + } + + @Nested + class GivenRequestWithValidContent { + + @Nested + class whenCallStaticGenerationAPI { + + @Test + void thenResponseIs201() throws Exception { + String payload = """ + services: + - serviceId: service" + + title: a" + + description: description" + + instanceBaseUrls:" + + - a" + + routes:" + + """; + when(staticDefinitionGenerator.generateFile(payload, "service")).thenReturn( + new StaticAPIResponse(201, "This is body") + ); + + webTestClient.post().uri(STATIC_DEF_GENERATE_ENDPOINT) + .header("Service-Id", "service") + .bodyValue(payload) + .exchange() + .expectStatus().is2xxSuccessful(); + } + + } + + @Nested + class whenCallStaticOverrideAPI { + + @Test + void thenResponseIs201() throws Exception { + String payload = "\"services:\\n - serviceId: service\\n title: a\\n description: description\\n instanceBaseUrls:\\n - a\\n routes:\\n "; + when(staticDefinitionGenerator.overrideFile(payload, "service")).thenReturn( + new StaticAPIResponse(201, "This is body") + ); + + webTestClient.post().uri(STATIC_DEF_OVERRIDE_ENDPOINT) + .header("Service-Id", "service") + .bodyValue(payload) + .exchange() + .expectStatus().is2xxSuccessful(); + } + + } + + @Nested + class WhenCallDelete { + + @Test + void givenValidId_thenResponseIsOK() throws Exception { + when(staticDefinitionGenerator.deleteFile("test-service")).thenReturn(new StaticAPIResponse(201, "OK")); + webTestClient.delete().uri(STATIC_DEF_DELETE_ENDPOINT) + .header("Service-Id", "test-service") + .exchange() + .expectStatus().is2xxSuccessful(); + } + + } + + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/TokenControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/TokenControllerTest.java new file mode 100644 index 0000000000..9e607a86b2 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/TokenControllerTest.java @@ -0,0 +1,177 @@ +/* + * 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.apicatalog.controllers.api; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.zowe.apiml.security.client.service.GatewaySecurityService; +import org.zowe.apiml.security.common.login.LoginRequest; +import org.zowe.apiml.security.common.token.QueryResponse; + +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.Optional; + +import static org.apache.hc.core5.http.HttpHeaders.AUTHORIZATION; +import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; + +@AutoConfigureWebTestClient +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class TokenControllerTest { + + private static final String VALID_USER = "user"; + private static final char[] VALID_PASSWORD = "password".toCharArray(); + private static final String INVALID_USER = "invalidUser"; + private static final char[] INVALID_PASSWORD = "invalidPassword".toCharArray(); + private static final String VALID_TOKEN = "valid.jwt.token"; + private static final String INVALID_TOKEN = "invalid.jwt.token"; + private static final String VALID_CREDENTIALS_BASE64 = Base64.getEncoder().encodeToString( + (VALID_USER + ":" + String.valueOf(VALID_PASSWORD)).getBytes() + ); + private static final String INVALID_CREDENTIALS_BASE64 = Base64.getEncoder().encodeToString( + (INVALID_USER + ":" + String.valueOf(INVALID_PASSWORD)).getBytes() + ); + + @MockitoBean + private GatewaySecurityService gatewaySecurityService; + + @Autowired + private WebTestClient webTestClient; + + @Nested + class Login { + + @BeforeEach + void mockGatewayClient() { + doReturn(Optional.of(VALID_TOKEN)).when(gatewaySecurityService).login(VALID_USER, VALID_PASSWORD, null); + doReturn(Optional.empty()).when(gatewaySecurityService).login(INVALID_USER, INVALID_PASSWORD, null); + } + + @Test + void givenValidCredentialsInHeader_whenLogin_thenSuccess() { + webTestClient.post() + .uri("/apicatalog/auth/login") + .header(AUTHORIZATION, "Basic " + VALID_CREDENTIALS_BASE64) + .exchange() + .expectStatus().isNoContent() + .expectCookie().value(COOKIE_AUTH_NAME, VALID_TOKEN::equals); + } + + @Test + void givenValidCredentialsInBody_whenLogin_thenSuccess() { + webTestClient.post() + .uri("/apicatalog/auth/login") + .bodyValue(new LoginRequest(VALID_USER, VALID_PASSWORD)) + .exchange() + .expectStatus().isNoContent() + .expectCookie().value(COOKIE_AUTH_NAME, VALID_TOKEN::equals); + } + + @Test + void givenInvalidCredentialsInHeader_whenLogin_thenRejected() { + webTestClient.post() + .uri("/apicatalog/auth/login") + .header(AUTHORIZATION, "Basic " + INVALID_CREDENTIALS_BASE64) + .exchange() + .expectStatus().isEqualTo(SC_UNAUTHORIZED); + } + + @Test + void givenInvalidCredentialsInBody_whenLogin_thenRejected() { + webTestClient.post() + .uri("/apicatalog/auth/login") + .bodyValue(new LoginRequest(INVALID_USER, INVALID_PASSWORD)) + .exchange() + .expectStatus().isEqualTo(SC_UNAUTHORIZED); + } + + @Test + void givenNoCredentials_whenLogin_thenBadRequest() { + webTestClient.post() + .uri("/apicatalog/auth/login") + .exchange() + .expectStatus().isBadRequest(); + } + + } + + @Nested + class Query { + + @BeforeEach + void mockGatewayClient() { + doReturn(new QueryResponse("domain", "user", new Date(), new Date(), "issuer", Collections.singletonList("scope"), null)) + .when(gatewaySecurityService).query(VALID_TOKEN); + doThrow(new BadCredentialsException("unauthorized")).when(gatewaySecurityService).query(INVALID_TOKEN); + } + + @Test + void givenValidCredentialsInHeader_whenQuery_thenSuccess() { + webTestClient.get() + .uri("/apicatalog/auth/query") + .header(AUTHORIZATION, "Bearer " + VALID_TOKEN) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("userId").value("user"::equals); + } + + @Test + void givenValidCredentialsInCookie_whenQuery_thenSuccess() { + webTestClient.get() + .uri("/apicatalog/auth/query") + .cookie(COOKIE_AUTH_NAME, VALID_TOKEN) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("userId").value("user"::equals); + } + + @Test + void givenInvalidCredentialsInHeader_whenQuery_thenReject() { + webTestClient.get() + .uri("/apicatalog/auth/query") + .header(AUTHORIZATION, "Bearer " + INVALID_TOKEN) + .exchange() + .expectStatus().isEqualTo(SC_UNAUTHORIZED); + } + + @Test + void givenInvalidCredentialsInCookie_whenQuery_thenReject() { + webTestClient.get() + .uri("/apicatalog/auth/query") + .cookie(COOKIE_AUTH_NAME, INVALID_TOKEN) + .exchange() + .expectStatus().isEqualTo(SC_UNAUTHORIZED); + } + + @Test + void givenNoCredentialsInCookie_whenQuery_thenReject() { + webTestClient.get() + .uri("/apicatalog/auth/query") + .exchange() + .expectStatus().isEqualTo(SC_UNAUTHORIZED); + } + + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/model/ApiDiffNotAvailableExceptionTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/exceptions/ApiDiffNotAvailableExceptionTest.java similarity index 87% rename from api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/model/ApiDiffNotAvailableExceptionTest.java rename to api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/exceptions/ApiDiffNotAvailableExceptionTest.java index ae8c73f27f..403ef88666 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/model/ApiDiffNotAvailableExceptionTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/exceptions/ApiDiffNotAvailableExceptionTest.java @@ -8,14 +8,15 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.services.status.model; +package org.zowe.apiml.apicatalog.exceptions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class ApiDiffNotAvailableExceptionTest { + @Nested class GivenExceptionMessage { @@ -25,5 +26,7 @@ void thenReturnMessage() { ApiDiffNotAvailableException exception = new ApiDiffNotAvailableException(message); assertEquals(message, exception.getMessage()); } + } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java index 541deb4e0d..1101455516 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java @@ -53,4 +53,5 @@ protected String getCatalogUriWithPath(String path) { protected String getCatalogUriWithPath(String scheme, String path) { return String.format("%s://%s:%d/%s", scheme, hostname, port, path); } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogProtectedEndpointTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogProtectedEndpointTest.java index 7264d930ba..12bd2e19cd 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogProtectedEndpointTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogProtectedEndpointTest.java @@ -22,6 +22,7 @@ @TestPropertySource( properties = {"apiml.health.protected=false"} ) @DirtiesContext public class ApiCatalogProtectedEndpointTest extends ApiCatalogFunctionalTest { + @Test void requestSuccessWithBody() { // the method could return 200 or 503 depends on the state, but the aim is to check if it is accessible @@ -32,4 +33,5 @@ void requestSuccessWithBody() { .body("status", not(nullValue())) .body("components.apiCatalog.status", not(nullValue())); } + } 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 6298f76d25..0f6355a24d 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 @@ -10,19 +10,32 @@ package org.zowe.apiml.apicatalog.functional; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; import org.apache.http.HttpStatus; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.slf4j.LoggerFactory; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.filter.AttlsHttpHandler; import javax.net.ssl.SSLException; import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; @TestPropertySource( properties = { @@ -40,13 +53,19 @@ class GivenAttlsModeEnabled { @Nested class WhenContextLoads { + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + @Test void requestFailsWithHttps() { try { given() .log().all() .when() - .get(getCatalogUriWithPath("containers")) + .get(getCatalogUriWithPath("apicatalog/containers")) .then() .log().all() .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); @@ -58,6 +77,10 @@ void requestFailsWithHttps() { @Test void requestFailsWithAttlsContextReasonWithHttp() { + var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); + given() .log().all() .when() @@ -65,9 +88,16 @@ void requestFailsWithAttlsContextReasonWithHttp() { .then() .log().all() .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) - .body(containsString("Connection is not secure.")) - .body(containsString("AttlsContext.getStatConn")); + .body(containsString("org.zowe.apiml.common.internalServerError")); + + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); } + } + } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java deleted file mode 100644 index dbc05dcfae..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.junit.jupiter.api.Assertions; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.retry.RetryException; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.gateway.GatewayNotAvailableException; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.registry.CannotRegisterServiceException; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static org.mockito.Mockito.*; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; - -@ExtendWith(MockitoExtension.class) -class InstanceInitializeServiceTest { - - @Mock - private CachedServicesService cachedServicesService; - - @Mock - private InstanceRetrievalService instanceRetrievalService; - - @Mock - private CachedProductFamilyService cachedProductFamilyService; - - @SuppressWarnings("unused") - @Mock - private InstanceRefreshService instanceRefreshService; - - @InjectMocks - private InstanceInitializeService instanceInitializeService; - - @Test - void testRetrieveAndRegisterAllInstancesWithCatalog() throws CannotRegisterServiceException { - Map instanceInfoMap = createInstances(); - - String catalogId = CoreService.API_CATALOG.getServiceId(); - InstanceInfo apiCatalogInstance = instanceInfoMap.get(catalogId.toUpperCase()); - when( - instanceRetrievalService.getInstanceInfo(catalogId) - ).thenReturn(apiCatalogInstance); - - Applications applications = new Applications(); - instanceInfoMap.values().forEach(f -> - applications.addApplication(new Application(f.getAppName(), Collections.singletonList(f))) - ); - - when( - instanceRetrievalService.getAllInstancesFromDiscovery(false) - ).thenReturn(applications); - - - instanceInitializeService.retrieveAndRegisterAllInstancesWithCatalog(); - - verify(cachedProductFamilyService, times(2)).saveContainerFromInstance( - apiCatalogInstance.getMetadata().get(CATALOG_ID), - apiCatalogInstance - ); - - - Optional catalogApplication = applications.getRegisteredApplications() - .stream() - .filter(f -> f.getName().equals(catalogId.toUpperCase())) - .findFirst(); - - Assertions.assertTrue(catalogApplication.isPresent()); - verify(cachedServicesService, times(2)).updateService(catalogApplication.get().getName(), catalogApplication.get()); - - - instanceInfoMap.values() - .stream() - .filter(f -> !f.getAppName().equals(catalogId.toUpperCase())) - .forEach(instanceInfo -> - verify(cachedProductFamilyService, times(1)).saveContainerFromInstance( - instanceInfo.getMetadata().get(CATALOG_ID), - instanceInfo - )); - } - - @Test - void shouldThrowExceptionWhenCatalogNotFound() throws CannotRegisterServiceException { - String catalogId = CoreService.API_CATALOG.getServiceId(); - when(instanceRetrievalService.getInstanceInfo(catalogId)).thenReturn(null); - - Exception exception = Assertions.assertThrows(CannotRegisterServiceException.class, () -> { - instanceInitializeService.retrieveAndRegisterAllInstancesWithCatalog(); - }); - Assertions.assertTrue(exception.getCause() instanceof RetryException); - } - - @Test - void shouldThrowRetryExceptionOnInstanceInitializationException() throws CannotRegisterServiceException { - String catalogId = CoreService.API_CATALOG.getServiceId(); - when(instanceRetrievalService.getInstanceInfo(catalogId)).thenThrow(new InstanceInitializationException("ERROR")); - - Exception exception = Assertions.assertThrows(RetryException.class, () -> { - instanceInitializeService.retrieveAndRegisterAllInstancesWithCatalog(); - }); - Assertions.assertEquals("ERROR", exception.getMessage()); - } - - @Test - void shouldThrowRetryExceptionOnGatewayNotAvailableException() throws CannotRegisterServiceException { - String catalogId = CoreService.API_CATALOG.getServiceId(); - when(instanceRetrievalService.getInstanceInfo(catalogId)).thenThrow(new GatewayNotAvailableException("ERROR")); - - Exception exception = Assertions.assertThrows(RetryException.class, () -> { - instanceInitializeService.retrieveAndRegisterAllInstancesWithCatalog(); - }); - Assertions.assertEquals("ERROR", exception.getMessage()); - } - - private Map createInstances() { - Map instanceInfoMap = new HashMap<>(); - - InstanceInfo instanceInfo = getStandardInstance( - CoreService.GATEWAY.getServiceId(), - InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("apimediationlayer", "/" + CoreService.GATEWAY.getServiceId()), - "gateway", - "https://localhost:9090/"); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance( - CoreService.ZAAS.getServiceId(), - InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("apimediationlayer", "/" + CoreService.ZAAS.getServiceId()), - "zaas", - "https://localhost:9090/"); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance( - CoreService.API_CATALOG.getServiceId(), - InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("apimediationlayer", "/" + CoreService.API_CATALOG.getServiceId()), - "apicatalog", - "https://localhost:9090/"); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance( - "STATICCLIENT", - InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("static", "/discoverableclient"), - "staticclient", - "https://localhost:9090/discoverableclient"); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance( - "STATICCLIENT2", - InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("static", "/discoverableclient"), - "staticclient2", - null); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance( - "ZOSMF1", - InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("zosmf", "/zosmf1"), - "zosmf1", - null); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance( - "ZOSMF2", - InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("zosmf", "/zosmf2"), - "zosmf2", - null); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - return instanceInfoMap; - } - - private HashMap getMetadataByCatalogUiTitleId(String catalogUiTileId, String uiRoute) { - HashMap metadata = new HashMap<>(); - metadata.put(CATALOG_ID, catalogUiTileId); - metadata.put(ROUTES + ".ui-v1." + ROUTES_SERVICE_URL, uiRoute); - metadata.put(ROUTES + ".ui-v1." + ROUTES_GATEWAY_URL, "ui/v1"); - metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "api/v1"); - metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "/"); - - return metadata; - } - - - private InstanceInfo getStandardInstance(String serviceId, - InstanceInfo.InstanceStatus status, - HashMap metadata, - String vipAddress, - String homePageUrl) { - - return InstanceInfo.Builder.newBuilder() - .setInstanceId(serviceId) - .setAppName(serviceId) - .setIPAddr("192.168.0.1") - .enablePort(InstanceInfo.PortType.SECURE, true) - .setSecurePort(9090) - .setHostName("localhost") - .setHomePageUrl(homePageUrl, homePageUrl) - .setSecureVIPAddress("localhost") - .setMetadata(metadata) - .setVIPAddress(vipAddress) - .setStatus(status) - .build(); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshServiceTest.java deleted file mode 100644 index 10b8cbc25d..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRefreshServiceTest.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.apicatalog.util.ContainerServiceMockUtil; -import org.zowe.apiml.apicatalog.util.ContainerServiceState; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.gateway.GatewayClient; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; - -import static org.mockito.Mockito.*; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_ID; - -class InstanceRefreshServiceTest { - - private final ContainerServiceMockUtil containerServiceMockUtil = new ContainerServiceMockUtil(); - - private GatewayClient gatewayClient; - private CachedProductFamilyService cachedProductFamilyService; - private CachedServicesService cachedServicesService; - private InstanceRetrievalService instanceRetrievalService; - - private InstanceRefreshService underTest; - - @BeforeEach - void setup() { - gatewayClient = mock(GatewayClient.class); - cachedProductFamilyService = mock(CachedProductFamilyService.class); - cachedServicesService = mock(CachedServicesService.class); - instanceRetrievalService = mock(InstanceRetrievalService.class); - - underTest = new InstanceRefreshService(cachedProductFamilyService, cachedServicesService, instanceRetrievalService); - underTest.start(); - - addApiCatalogToCache(); - } - - @Nested - class WhenRefreshingCacheFromDiscovery { - @Nested - class GivenValidCache { - ContainerServiceState discoveredState; - ContainerServiceState cachedState; - - @BeforeEach - void prepareState() { - cachedState = containerServiceMockUtil.createContainersServicesAndInstances(); - containerServiceMockUtil.mockServiceRetrievalFromCache(cachedServicesService, cachedState.getApplications()); - - discoveredState = new ContainerServiceState(); - discoveredState.setServices(new ArrayList<>()); - discoveredState.setContainers(new ArrayList<>()); - discoveredState.setInstances(new ArrayList<>()); - discoveredState.setApplications(new ArrayList<>()); - } - - @Nested - class AndServiceDoesntExist { - InstanceInfo newInstanceOfService; - - @BeforeEach - void prepareApplication() { - // start up a new instance of service 5 and add it to the service1 application - HashMap metadata = new HashMap<>(); - metadata.put(CATALOG_ID, "api-five"); - newInstanceOfService - = containerServiceMockUtil.createInstance("service5", "service5:9999", InstanceInfo.InstanceStatus.UP, - InstanceInfo.ActionType.ADDED, metadata); - discoveredState.getInstances().add(newInstanceOfService); - Application service5 = new Application("service5", Collections.singletonList(newInstanceOfService)); - discoveredState.getApplications().add(service5); - - teachMocks(); - } - - @Test - void addServiceToCache() { - when(cachedProductFamilyService.saveContainerFromInstance("api-five", newInstanceOfService)) - .thenReturn(new APIContainer()); - - underTest.refreshCacheFromDiscovery(); - - verify(cachedProductFamilyService, times(1)) - .saveContainerFromInstance("api-five", newInstanceOfService); - } - } - - @Nested - class AndServiceAlreadyExists { - Application service3; - InstanceInfo changedInstanceOfService; - - @BeforeEach - void prepareService() { - service3 = cachedState.getApplications() - .stream() - .filter(application -> application.getName().equalsIgnoreCase("service3")) - .toList().get(0); - - changedInstanceOfService = service3.getInstances().get(0); - changedInstanceOfService.getMetadata().put(CATALOG_ID, "api-three"); - service3.getInstances().add(0, changedInstanceOfService); - discoveredState.getApplications().add(service3); - - teachMocks(); - - } - - @Test - void serviceIsRemovedFromCache() { - changedInstanceOfService.setActionType(InstanceInfo.ActionType.DELETED); - - underTest.refreshCacheFromDiscovery(); - - verify(cachedProductFamilyService, times(1)) - .removeInstance("api-three", changedInstanceOfService); - verify(cachedServicesService, never()).updateService(anyString(), any(Application.class)); - verify(cachedProductFamilyService, never()).saveContainerFromInstance("api-three", changedInstanceOfService); - } - - @Test - void serviceIsModifiedInCache() { - changedInstanceOfService.setActionType(InstanceInfo.ActionType.MODIFIED); - - APIContainer apiContainer3 = cachedState.getContainers() - .stream() - .filter(apiContainer -> apiContainer.getId().equals("api-three")) - .findFirst() - .orElse(new APIContainer()); - - when(cachedProductFamilyService.saveContainerFromInstance("api-three", changedInstanceOfService)) - .thenReturn(apiContainer3); - - underTest.refreshCacheFromDiscovery(); - - verify(cachedServicesService, times(1)).updateService(changedInstanceOfService.getAppName(), service3); - verify(cachedProductFamilyService, times(1)) - .saveContainerFromInstance("api-three", changedInstanceOfService); - } - } - - void teachMocks() { - // Mock the discovery and cached service query - Applications discoveredServices = new Applications("123", 2L, discoveredState.getApplications()); - when(instanceRetrievalService.getAllInstancesFromDiscovery(true)).thenReturn(discoveredServices); - Applications cachedServices = new Applications("456", 1L, cachedState.getApplications()); - when(cachedServicesService.getAllCachedServices()).thenReturn(cachedServices); - } - } - - @Nested - class GivenClientIsNotInitialized { - @Test - void cacheIsntUpdated() { - when(gatewayClient.isInitialized()).thenReturn(false); - - underTest.refreshCacheFromDiscovery(); - - verify(cachedServicesService, never()) - .updateService(anyString(), any(Application.class)); - } - } - - @Nested - class GivenApiCatalogIsntInCache { - @Test - void cacheIsntUpdated() { - when(cachedServicesService.getService(CoreService.API_CATALOG.getServiceId())).thenReturn(null); - - underTest.refreshCacheFromDiscovery(); - - verify(cachedServicesService, never()) - .updateService(anyString(), any(Application.class)); - } - } - } - - - private void addApiCatalogToCache() { - InstanceInfo apiCatalogInstance = containerServiceMockUtil.createInstance( - CoreService.API_CATALOG.getServiceId(), - "service:9999", - InstanceInfo.InstanceStatus.UP, - InstanceInfo.ActionType.ADDED, - new HashMap<>()); - - when(cachedServicesService.getService(CoreService.API_CATALOG.getServiceId())) - .thenReturn( - new Application(CoreService.API_CATALOG.getServiceId(), - Collections.singletonList(apiCatalogInstance) - ) - ); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java deleted file mode 100644 index 9675f70de0..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import org.apache.commons.io.IOUtils; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.ClassicHttpRequest; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.io.HttpClientResponseHandler; -import org.apache.hc.core5.http.io.entity.BasicHttpEntity; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; -import org.zowe.apiml.apicatalog.util.ApplicationsWrapper; -import org.zowe.apiml.constants.EurekaMetadataDefinition; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.registry.ApplicationWrapper; -import org.zowe.apiml.util.HttpClientMockHelper; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static org.apache.hc.core5.http.ContentType.APPLICATION_JSON; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.*; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.REGISTRATION_TYPE; - -class InstanceRetrievalServiceTest { - - private static final String UNKNOWN = "unknown"; - - private InstanceInfo getStandardInstance(String serviceId, InstanceInfo.InstanceStatus status) { - return InstanceInfo.Builder.newBuilder() - .setInstanceId(serviceId) - .setAppName(serviceId) - .setStatus(status) - .build(); - } - - @Nested - @ExtendWith(SpringExtension.class) - @TestPropertySource(locations = "/application.yml") - @ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class, classes = InstanceServicesContextConfiguration.class) - class SingleDomain { - - private InstanceRetrievalService instanceRetrievalService; - - @Autowired - private DiscoveryConfigProperties discoveryConfigProperties; - - @Mock - private CloseableHttpClient httpClient; - - @Mock - private CloseableHttpResponse response; - - @BeforeEach - void setup() { - HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK); - - instanceRetrievalService = new InstanceRetrievalService(discoveryConfigProperties, httpClient); - } - - @Test - void whenDiscoveryServiceIsNotAvailable_thenTryOthersFromTheList() throws IOException { - when(response.getCode()).thenReturn(HttpStatus.SC_FORBIDDEN).thenReturn(HttpStatus.SC_OK); - - instanceRetrievalService.getAllInstancesFromDiscovery(false); - verify(httpClient, times(2)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); - } - - @Test - void testGetInstanceInfo_whenServiceIdIsUNKNOWN() { - InstanceInfo instanceInfo = instanceRetrievalService.getInstanceInfo(UNKNOWN); - assertNull(instanceInfo); - } - - @Test - void providedNoInstanceInfoIsReturned_thenInstanceInitializationExceptionIsThrown() { - String serviceId = CoreService.API_CATALOG.getServiceId(); - when(response.getCode()).thenReturn(HttpStatus.SC_FORBIDDEN); - - assertThrows(InstanceInitializationException.class, () -> instanceRetrievalService.getInstanceInfo(serviceId)); - } - - @Test - void testGetInstanceInfo_whenResponseHasEmptyBody() { - HttpClientMockHelper.mockResponse(response, ""); - InstanceInfo instanceInfo = instanceRetrievalService.getInstanceInfo(CoreService.API_CATALOG.getServiceId()); - assertNull(instanceInfo); - } - - @Test - void testGetInstanceInfo_whenResponseCodeIsSuccessWithUnParsedJsonText() { - HttpClientMockHelper.mockResponse(response, "UNPARSABLE_JSON"); - InstanceInfo instanceInfo = instanceRetrievalService.getInstanceInfo(CoreService.API_CATALOG.getServiceId()); - assertNull(instanceInfo); - } - - @Test - void testGetInstanceInfo() throws IOException { - InstanceInfo expectedInstanceInfo = getStandardInstance( - CoreService.API_CATALOG.getServiceId(), - InstanceInfo.InstanceStatus.UP - ); - - ObjectMapper mapper = new ObjectMapper(); - String bodyCatalog = mapper.writeValueAsString( - new ApplicationWrapper(new Application( - CoreService.API_CATALOG.getServiceId(), - Collections.singletonList(expectedInstanceInfo) - ))); - BasicHttpEntity responseEntity = new BasicHttpEntity(IOUtils.toInputStream(bodyCatalog, StandardCharsets.UTF_8), APPLICATION_JSON); - when(response.getEntity()).thenReturn(responseEntity); - - InstanceInfo actualInstanceInfo = instanceRetrievalService.getInstanceInfo(CoreService.API_CATALOG.getServiceId()); - - assertNotNull(actualInstanceInfo); - assertThat(actualInstanceInfo, hasProperty("instanceId", equalTo(expectedInstanceInfo.getInstanceId()))); - assertThat(actualInstanceInfo, hasProperty("appName", equalTo(expectedInstanceInfo.getAppName()))); - assertThat(actualInstanceInfo, hasProperty("status", equalTo(expectedInstanceInfo.getStatus()))); - } - - @Test - void testGetAllInstancesFromDiscovery_whenResponseCodeIsNotSuccess() { - when(response.getCode()).thenReturn(HttpStatus.SC_FORBIDDEN); - - Applications actualApplications = instanceRetrievalService.getAllInstancesFromDiscovery(false); - assertNull(actualApplications); - } - - @Test - void testGetAllInstancesFromDiscovery_whenResponseCodeIsSuccessWithUnParsedJsonText() { - Applications actualApplications = instanceRetrievalService.getAllInstancesFromDiscovery(false); - assertNull(actualApplications); - } - - @Test - void testGetAllInstancesFromDiscovery_whenNeedApplicationsWithoutFilter() throws IOException { - Map instanceInfoMap = createInstances(); - - - Applications expectedApplications = new Applications(); - instanceInfoMap.forEach((key, value) -> expectedApplications.addApplication(new Application(value.getAppName(), Collections.singletonList(value)))); - - ObjectMapper mapper = new ObjectMapper(); - String bodyAll = mapper.writeValueAsString(new ApplicationsWrapper(expectedApplications)); - BasicHttpEntity responseEntity = new BasicHttpEntity(IOUtils.toInputStream(bodyAll, StandardCharsets.UTF_8), APPLICATION_JSON); - when(response.getEntity()).thenReturn(responseEntity); - - Applications actualApplications = instanceRetrievalService.getAllInstancesFromDiscovery(false); - - assertEquals(expectedApplications.size(), actualApplications.size()); - - List actualApplicationList = - new ArrayList<>(actualApplications.getRegisteredApplications()); - - - expectedApplications - .getRegisteredApplications() - .forEach(expectedApplication -> - assertThat(actualApplicationList, hasItem(hasProperty("name", equalTo(expectedApplication.getName())))) - ); - } - - @Test - void testGetAllInstancesFromDiscovery_whenNeedApplicationsWithDeltaFilter() throws IOException { - Map instanceInfoMap = createInstances(); - - Applications expectedApplications = new Applications(); - instanceInfoMap.forEach((key, value) -> expectedApplications.addApplication(new Application(value.getAppName(), Collections.singletonList(value)))); - - ObjectMapper mapper = new ObjectMapper(); - String bodyAll = mapper.writeValueAsString(new ApplicationsWrapper(expectedApplications)); - BasicHttpEntity responseEntity = new BasicHttpEntity(IOUtils.toInputStream(bodyAll, StandardCharsets.UTF_8), APPLICATION_JSON); - when(response.getEntity()).thenReturn(responseEntity); - - Applications actualApplications = instanceRetrievalService.getAllInstancesFromDiscovery(true); - - assertEquals(expectedApplications.size(), actualApplications.size()); - - List actualApplicationList = - new ArrayList<>(actualApplications.getRegisteredApplications()); - - - expectedApplications - .getRegisteredApplications() - .forEach(expectedApplication -> - assertThat(actualApplicationList, hasItem(hasProperty("name", equalTo(expectedApplication.getName())))) - ); - } - - private Map createInstances() { - Map instanceInfoMap = new HashMap<>(); - - InstanceInfo instanceInfo = getStandardInstance(CoreService.GATEWAY.getServiceId(), InstanceInfo.InstanceStatus.UP); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance(CoreService.ZAAS.getServiceId(), InstanceInfo.InstanceStatus.UP); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance(CoreService.API_CATALOG.getServiceId(), InstanceInfo.InstanceStatus.UP); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - - instanceInfo = getStandardInstance("STATICCLIENT", InstanceInfo.InstanceStatus.UP); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - - instanceInfo = getStandardInstance("STATICCLIENT2", InstanceInfo.InstanceStatus.UP); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - - instanceInfo = getStandardInstance("ZOSMF1", InstanceInfo.InstanceStatus.UP); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - instanceInfo = getStandardInstance("ZOSMF2", InstanceInfo.InstanceStatus.UP); - instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - - return instanceInfoMap; - } - - } - - @Nested - class MultiDomain { - - private static final String APIML_CENTRAL = "apimlidcentral"; - private static final String APIML_ID_1 = "apimlid1"; - - private InstanceRetrievalService instanceRetrievalService; - private ObjectMapper mapper = mock(ObjectMapper.class); - - @BeforeEach - void init() throws IOException { - DiscoveryConfigProperties discoveryConfig = new DiscoveryConfigProperties(); - ReflectionTestUtils.setField(discoveryConfig, "locations", new String[] { "https://ds:10011/eureka" }); - - instanceRetrievalService = spy(new InstanceRetrievalService(discoveryConfig, null)); - ReflectionTestUtils.setField(instanceRetrievalService, "mapper", mapper); - - // construct Eureka representation of Gateway instances - InstanceInfo centralApiml = getStandardInstance(CoreService.GATEWAY.getServiceId(), InstanceInfo.InstanceStatus.UP); - ReflectionTestUtils.setField(centralApiml, "instanceId", "centralApiml:instance:1"); - centralApiml.getMetadata().put(APIML_ID, APIML_CENTRAL); - centralApiml.getMetadata().put(REGISTRATION_TYPE, EurekaMetadataDefinition.RegistrationType.PRIMARY.getValue()); - InstanceInfo apiml1 = getStandardInstance(CoreService.GATEWAY.getServiceId(), InstanceInfo.InstanceStatus.UP); - ReflectionTestUtils.setField(apiml1, "instanceId", "domainApiml:instance:1"); - apiml1.getMetadata().put(APIML_ID, APIML_ID_1); - apiml1.getMetadata().put(REGISTRATION_TYPE, EurekaMetadataDefinition.RegistrationType.ADDITIONAL.getValue()); - Application application = new Application(CoreService.GATEWAY.getServiceId()); - application.addInstance(centralApiml); - application.addInstance(apiml1); - ApplicationWrapper applications = new ApplicationWrapper(application); - - // mock obtaining and mapping of APIML instance - doReturn("gatewayJson").when(instanceRetrievalService).queryDiscoveryForInstances(argThat(request -> - CoreService.GATEWAY.getServiceId().equalsIgnoreCase(request.getServiceId()) - )); - doReturn(applications).when(mapper).readValue("gatewayJson", ApplicationWrapper.class); - } - - @Test - void givenAdditionalRegistrationOfGateway_whenAskWithApimlId_thenFindIt() { - InstanceInfo instanceInfo = instanceRetrievalService.getInstanceInfo(APIML_ID_1); - assertEquals(CoreService.GATEWAY.getServiceId(), instanceInfo.getAppName().toLowerCase(Locale.ROOT)); - assertEquals(APIML_ID_1, instanceInfo.getMetadata().get(APIML_ID)); - assertEquals(EurekaMetadataDefinition.RegistrationType.ADDITIONAL.getValue(), instanceInfo.getMetadata().get(REGISTRATION_TYPE)); - } - - @Test - void givenUnknownServiceId_whenGetInstanceInfo_thenMakeTwoQueries() throws IOException { - instanceRetrievalService.getInstanceInfo("unknown-service"); - verify(instanceRetrievalService, times(2)).queryDiscoveryForInstances(any()); - } - - @Test - void givenMultipleGateways_whenAskForGatewayService_thenReturnThePrimaryOne() { - InstanceInfo instanceInfo = instanceRetrievalService.getInstanceInfo(CoreService.GATEWAY.getServiceId()); - assertEquals(CoreService.GATEWAY.getServiceId(), instanceInfo.getAppName().toLowerCase(Locale.ROOT)); - assertEquals(APIML_CENTRAL, instanceInfo.getMetadata().get(APIML_ID)); - assertEquals(EurekaMetadataDefinition.RegistrationType.PRIMARY.getValue(), instanceInfo.getMetadata().get(REGISTRATION_TYPE)); - } - - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceServicesContextConfiguration.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceServicesContextConfiguration.java deleted file mode 100644 index 956dbd46c6..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceServicesContextConfiguration.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.instance; - -import org.springframework.context.annotation.Bean; -import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; - -public class InstanceServicesContextConfiguration { - @Bean - public DiscoveryConfigProperties discoveryConfigProperties() { - return new DiscoveryConfigProperties(); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/model/SemanticVersionTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/model/SemanticVersionTest.java index 69e77fa4f9..a9ed698bd7 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/model/SemanticVersionTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/model/SemanticVersionTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class SemanticVersionTest { diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandlerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandlerTest.java index d1601214eb..103a7aec44 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandlerTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/security/ApiCatalogLogoutSuccessHandlerTest.java @@ -10,44 +10,56 @@ package org.zowe.apiml.apicatalog.security; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.TokenAuthentication; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.mock.web.MockHttpSession; - -import jakarta.servlet.http.Cookie; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.web.server.WebFilterChain; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import reactor.test.StepVerifier; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +@ExtendWith(MockitoExtension.class) class ApiCatalogLogoutSuccessHandlerTest { @Test void testOnLogoutSuccess() { - MockHttpServletRequest httpServletRequest = new MockHttpServletRequest(); - MockHttpSession mockHttpSession = new MockHttpSession(); - httpServletRequest.setSession(mockHttpSession); - - MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); + var request = MockServerHttpRequest.get("/logout") + .header(HttpHeaders.AUTHORIZATION, "Bearer token123") + .build(); + var exchange = MockServerWebExchange.from(request); + WebFilterChain mockChain = mock(WebFilterChain.class); + var webFilterExchange = new WebFilterExchange(exchange, mockChain); AuthConfigurationProperties securityConfigurationProperties = new AuthConfigurationProperties(); ApiCatalogLogoutSuccessHandler apiCatalogLogoutSuccessHandler = new ApiCatalogLogoutSuccessHandler(securityConfigurationProperties); - apiCatalogLogoutSuccessHandler.onLogoutSuccess( - httpServletRequest, - httpServletResponse, - new TokenAuthentication("TEST_TOKEN_STRING") - ); + StepVerifier.create(apiCatalogLogoutSuccessHandler.onLogoutSuccess( + webFilterExchange, + new TokenAuthentication("TEST_TOKEN_STRING") + )) + .verifyComplete(); - assertTrue(mockHttpSession.isInvalid()); - assertEquals(HttpStatus.OK.value(), httpServletResponse.getStatus()); + StepVerifier.create(exchange.getSession()) + .assertNext(session -> { + assertFalse(session.isStarted()); + }) + .verifyComplete(); - Cookie cookie = httpServletResponse.getCookie( + assertEquals(HttpStatus.OK.value(), exchange.getResponse().getStatusCode().value()); + + var cookie = exchange.getResponse().getCookies().getFirst( securityConfigurationProperties.getCookieProperties().getCookieName()); assertNotNull(cookie); - assertTrue(cookie.getSecure()); + assertTrue(cookie.isSecure()); assertTrue(cookie.isHttpOnly()); } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/ApiDocKeyTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/ApiDocKeyTest.java deleted file mode 100644 index 4cef254f9b..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/ApiDocKeyTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services; - -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocCacheKey; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class ApiDocKeyTest { - - @Test - void testCreationOfObjectAndValuesSet() { - ApiDocCacheKey apiDocCacheKey = new ApiDocCacheKey("service", "1.0.0"); - Assertions.assertNotNull(apiDocCacheKey); - Assertions.assertEquals("service", apiDocCacheKey.getServiceId()); - Assertions.assertEquals("1.0.0", apiDocCacheKey.getApiVersion()); - apiDocCacheKey.setApiVersion("2.0.0"); - apiDocCacheKey.setServiceId("service1"); - Assertions.assertEquals("service1", apiDocCacheKey.getServiceId()); - Assertions.assertEquals("2.0.0", apiDocCacheKey.getApiVersion()); - } - - @Test - void testEqualsAndHasCode() { - ApiDocCacheKey apiDocCacheKey = new ApiDocCacheKey("service", "1.0.0"); - ApiDocCacheKey apiDocCacheKey1 = new ApiDocCacheKey("service1", "2.0.0"); - Assertions.assertNotEquals(apiDocCacheKey, apiDocCacheKey1); - Assertions.assertNotEquals(apiDocCacheKey.hashCode(), apiDocCacheKey1.hashCode()); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocServiceTest.java deleted file mode 100644 index 94b0f15a7e..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedApiDocServiceTest.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.cached; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.services.status.APIDocRetrievalService; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; -import org.zowe.apiml.apicatalog.services.status.model.ApiVersionNotFoundException; -import org.zowe.apiml.apicatalog.swagger.TransformApiDocService; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class CachedApiDocServiceTest { - private CachedApiDocService cachedApiDocService; - - @Mock - APIDocRetrievalService apiDocRetrievalService; - - @Mock - TransformApiDocService transformApiDocService; - - @BeforeEach - private void setUp() { - cachedApiDocService = new CachedApiDocService(apiDocRetrievalService, transformApiDocService); - cachedApiDocService.resetCache(); - } - - @Nested - class GivenValidApiDoc_thenReturnIt { - @Test - void whenRetrieving() { - String serviceId = "Service"; - String version = "v1"; - String expectedApiDoc = "This is some api doc"; - - ApiDocInfo apiDocInfo = new ApiDocInfo(null, expectedApiDoc, null); - - when(apiDocRetrievalService.retrieveApiDoc(serviceId, version)) - .thenReturn(apiDocInfo); - when(transformApiDocService.transformApiDoc(serviceId, apiDocInfo)) - .thenReturn(expectedApiDoc); - - String apiDoc = cachedApiDocService.getApiDocForService(serviceId, version); - - assertNotNull(apiDoc); - assertEquals(expectedApiDoc, apiDoc); - } - - @Test - void whenUpdating() { - String serviceId = "Service"; - String version = "v1"; - String expectedApiDoc = "This is some api doc"; - String updatedApiDoc = "This is some updated API Doc"; - - - ApiDocInfo apiDocInfo = new ApiDocInfo(null, expectedApiDoc, null); - - when(apiDocRetrievalService.retrieveApiDoc(serviceId, version)) - .thenReturn(apiDocInfo); - when(transformApiDocService.transformApiDoc(serviceId, apiDocInfo)) - .thenReturn(expectedApiDoc); - - String apiDoc = cachedApiDocService.getApiDocForService(serviceId, version); - - assertNotNull(apiDoc); - assertEquals(expectedApiDoc, apiDoc); - - cachedApiDocService.updateApiDocForService(serviceId, version, updatedApiDoc); - - apiDocInfo = new ApiDocInfo(null, updatedApiDoc, null); - - when(apiDocRetrievalService.retrieveApiDoc(serviceId, version)) - .thenReturn(apiDocInfo); - when(transformApiDocService.transformApiDoc(serviceId, apiDocInfo)) - .thenReturn(updatedApiDoc); - - apiDoc = cachedApiDocService.getApiDocForService(serviceId, version); - - assertNotNull(apiDoc); - assertEquals(updatedApiDoc, apiDoc); - } - } - - @Test - void givenInvalidApiDoc_whenRetrieving_thenThrowException() { - String serviceId = "Service"; - String version = "v1"; - - Exception exception = assertThrows(ApiDocNotFoundException.class, - () -> cachedApiDocService.getApiDocForService(serviceId, version), - "Expected exception is not ApiDocNotFoundException"); - assertEquals("No API Documentation was retrieved for the service Service. Root cause: ", exception.getMessage()); - } - - @Nested - class GivenValidApiVersions_thenReturnThem { - @Test - void whenRetrieving() { - String serviceId = "service"; - List expectedVersions = Arrays.asList("1.0.0", "2.0.0"); - - when(apiDocRetrievalService.retrieveApiVersions(serviceId)).thenReturn(expectedVersions); - - List versions = cachedApiDocService.getApiVersionsForService(serviceId); - assertEquals(expectedVersions, versions); - } - - @Test - void whenUpdating() { - String serviceId = "service"; - List initialVersions = Arrays.asList("1.0.0", "2.0.0"); - List updatedVersions = Arrays.asList("1.0.0", "2.0.0", "3.0.0"); - - when(apiDocRetrievalService.retrieveApiVersions(serviceId)).thenReturn(initialVersions); - - List versions = cachedApiDocService.getApiVersionsForService(serviceId); - assertEquals(initialVersions, versions); - - cachedApiDocService.updateApiVersionsForService(serviceId, updatedVersions); - - when(apiDocRetrievalService.retrieveApiVersions(serviceId)).thenReturn(updatedVersions); - - versions = cachedApiDocService.getApiVersionsForService(serviceId); - assertEquals(updatedVersions, versions); - } - } - - @Test - void givenInvalidApiVersion_whenRetrieving_thenThrowException() { - String serviceId = "service"; - - when(apiDocRetrievalService.retrieveApiVersions(serviceId)).thenReturn(Collections.emptyList()); - - Exception exception = assertThrows(ApiVersionNotFoundException.class, - () -> cachedApiDocService.getApiVersionsForService(serviceId), "Exception is not ApiVersionsNotFoundException"); - assertEquals("No API versions were retrieved for the service service.", exception.getMessage()); - } - - @Nested - class GivenValidApiDocs { - @Test - void whenRetrievingDefault_thenReturnLatestApi() { - String serviceId = "service"; - String expectedApiDoc = "This is some api doc"; - ApiDocInfo apiDocInfo = new ApiDocInfo(null, expectedApiDoc, null); - - when(apiDocRetrievalService.retrieveDefaultApiDoc(serviceId)).thenReturn(apiDocInfo); - when(transformApiDocService.transformApiDoc(serviceId, apiDocInfo)).thenReturn(expectedApiDoc); - - String apiDoc = cachedApiDocService.getDefaultApiDocForService(serviceId); - assertEquals(expectedApiDoc, apiDoc); - } - - @Test - void whenUpdatingDefault_thenRetrieveDefault() { - String serviceId = "service"; - String initialApiDoc = "This is some api doc"; - String updatedApiDoc = "This is some updated api doc"; - ApiDocInfo initialApiDocInfo = new ApiDocInfo(null, initialApiDoc, null); - ApiDocInfo updatedApiDocInfo = new ApiDocInfo(null, updatedApiDoc, null); - - when(apiDocRetrievalService.retrieveDefaultApiDoc(serviceId)).thenReturn(initialApiDocInfo); - when(transformApiDocService.transformApiDoc(serviceId, initialApiDocInfo)).thenReturn(initialApiDoc); - - String apiDoc = cachedApiDocService.getDefaultApiDocForService(serviceId); - assertEquals(initialApiDoc, apiDoc); - - cachedApiDocService.updateDefaultApiDocForService(serviceId, updatedApiDoc); - - when(apiDocRetrievalService.retrieveDefaultApiDoc(serviceId)).thenReturn(updatedApiDocInfo); - when(transformApiDocService.transformApiDoc(serviceId, updatedApiDocInfo)).thenReturn(updatedApiDoc); - - apiDoc = cachedApiDocService.getDefaultApiDocForService(serviceId); - assertEquals(updatedApiDoc, apiDoc); - } - - @Test - void whenRetrievingDefaultNocContent_thenException() { - String serviceId = "service"; - ApiDocInfo apiDocInfo = new ApiDocInfo(null, null, null); - - when(apiDocRetrievalService.retrieveDefaultApiDoc(serviceId)).thenReturn(apiDocInfo); - - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getDefaultApiDocForService(serviceId)); - verifyNoInteractions(transformApiDocService); - } - } - - @Test - void givenInvalidApiDoc_whenRetrievingDefault_thenThrowException() { - String serviceId = "service"; - - Exception exception = assertThrows(ApiDocNotFoundException.class, - () -> cachedApiDocService.getDefaultApiDocForService(serviceId), - "Expected exception is not ApiDocNotFoundException"); - assertEquals("No API Documentation was retrieved for the service service. Root cause: ", exception.getMessage()); - } - - @Nested - class GivenDefaultApiVersion_thenReturnIt { - @Test - void whenRetrieveDefaultVersion() { - String serviceId = "service"; - String expected = "v1"; - when(apiDocRetrievalService.retrieveDefaultApiVersion(serviceId)).thenReturn(expected); - - String actual = cachedApiDocService.getDefaultApiVersionForService(serviceId); - assertEquals(expected, actual); - } - - @Test - void whenUpdateDefaultVersion() { - String serviceId = "service"; - String initialVersion = "v1"; - String updatedVersion = "v2"; - - when(apiDocRetrievalService.retrieveDefaultApiVersion(serviceId)).thenReturn(initialVersion); - String version = cachedApiDocService.getDefaultApiVersionForService(serviceId); - assertEquals(initialVersion, version); - - cachedApiDocService.updateDefaultApiVersionForService(serviceId, updatedVersion); - - when(apiDocRetrievalService.retrieveDefaultApiVersion(serviceId)).thenReturn(null); - version = cachedApiDocService.getDefaultApiVersionForService(serviceId); - assertEquals(updatedVersion, version); - } - } - - @Test - void givenErrorRetrievingDefaultApiVersion_whenGetDefaultVersion_thenThrowException() { - String serviceId = "service"; - - when(apiDocRetrievalService.retrieveDefaultApiVersion(serviceId)).thenThrow(new RuntimeException("error")); - - Exception exception = assertThrows(ApiVersionNotFoundException.class, - () -> cachedApiDocService.getDefaultApiVersionForService(serviceId)); - assertEquals("Error trying to find default API version", exception.getMessage()); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java deleted file mode 100644 index 20839e7376..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java +++ /dev/null @@ -1,562 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.cached; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import org.apache.commons.lang3.tuple.Pair; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.model.APIService; -import org.zowe.apiml.apicatalog.model.CustomStyleConfig; -import org.zowe.apiml.apicatalog.util.ServicesBuilder; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.gateway.GatewayClient; -import org.zowe.apiml.product.instance.ServiceAddress; -import org.zowe.apiml.product.routing.RoutedServices; -import org.zowe.apiml.product.routing.ServiceType; -import org.zowe.apiml.product.routing.transform.TransformService; -import org.zowe.apiml.product.routing.transform.URLTransformationException; - -import java.util.*; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; - -/** - * Container is used the same way as Tile - */ -@SuppressWarnings({ "squid:S2925" }) // replace with proper wait test library -class CachedProductFamilyServiceTest { - - private static final String SERVICE_ID = "service_test_id"; - - private final Integer cacheRefreshUpdateThresholdInMillis = 2000; - - private CachedProductFamilyService underTest; - - private TransformService transformService; - private CachedServicesService cachedServicesService; - private ServicesBuilder servicesBuilder; - private CustomStyleConfig customStyleConfig; - @BeforeEach - void setUp() { - cachedServicesService = mock(CachedServicesService.class); - transformService = mock(TransformService.class); - customStyleConfig = mock(CustomStyleConfig.class); - - underTest = new CachedProductFamilyService( - cachedServicesService, - transformService, - cacheRefreshUpdateThresholdInMillis, customStyleConfig); - - servicesBuilder = new ServicesBuilder(underTest); - } - - @Nested - class WhenCallSaveContainerFromInstance { - @Nested - class AndWhenCachedInstance { - private InstanceInfo instance; - private InstanceInfo updatedInstance; - - @BeforeEach - void prepareInstances() throws URLTransformationException { - Map metadata = new HashMap<>(); - metadata.put(CATALOG_ID, "demoapp"); - metadata.put(CATALOG_TITLE, "Title"); - metadata.put(CATALOG_DESCRIPTION, "Description"); - metadata.put(CATALOG_VERSION, "1.0.0"); - metadata.put(SERVICE_TITLE, "sTitle"); - metadata.put(SERVICE_DESCRIPTION, "sDescription"); - instance = servicesBuilder.createInstance("service1", InstanceInfo.InstanceStatus.UP, metadata); - - Map updatedMetadata = new HashMap<>(); - updatedMetadata.put(CATALOG_ID, "demoapp"); - updatedMetadata.put(CATALOG_TITLE, "Title2"); - updatedMetadata.put(CATALOG_DESCRIPTION, "Description2"); - updatedMetadata.put(CATALOG_VERSION, "2.0.0"); - updatedMetadata.put(SERVICE_TITLE, "sTitle2"); - updatedMetadata.put(SERVICE_DESCRIPTION, "sDescription2"); - updatedInstance = servicesBuilder.createInstance("service1", InstanceInfo.InstanceStatus.UP, - updatedMetadata); - - when(transformService.transformURL( - any(ServiceType.class), any(String.class), any(String.class), any(RoutedServices.class), eq(false))) - .thenReturn(instance.getHomePageUrl()); - } - - @Nested - class GivenInstanceIsNotInCache { - @Test - void createNew() { - APIContainer originalContainer = underTest.saveContainerFromInstance("demoapp", instance); - - List lsContainer = underTest.getRecentlyUpdatedContainers(); - assertThatContainerIsCorrect(lsContainer, originalContainer, instance); - } - - @Nested - class WhenCheckingIfAttlsEnabled { - @Test - void givenNoAttls_thenGetInstanceHomePageUrl() throws URLTransformationException { - when(transformService.transformURL( - any(ServiceType.class), any(String.class), any(String.class), any(RoutedServices.class), eq(true))) - .thenReturn(instance.getHomePageUrl()); - APIContainer originalContainer = underTest.saveContainerFromInstance("demoapp", instance); - - List lsContainer = underTest.getRecentlyUpdatedContainers(); - assertThatContainerIsCorrect(lsContainer, originalContainer, instance); - } - } - @Test - void givenAttlsEnabled_thenGetInstanceHomePageUrl() throws URLTransformationException { - ReflectionTestUtils.setField(underTest, "isAttlsEnabled", true); - when(transformService.transformURL( - any(ServiceType.class), any(String.class), any(String.class), any(RoutedServices.class), eq(true))) - .thenReturn(instance.getHomePageUrl()); - APIContainer originalContainer = underTest.saveContainerFromInstance("demoapp", instance); - - List lsContainer = underTest.getRecentlyUpdatedContainers(); - assertThatContainerIsCorrect(lsContainer, originalContainer, instance); - } - } - - @Nested - class GivenInstanceIsInTheCache { - @Test - void update() throws InterruptedException { - APIContainer originalContainer = underTest.saveContainerFromInstance("demoapp", instance); - Calendar createdTimestamp = originalContainer.getLastUpdatedTimestamp(); - - Thread.sleep(100); - - APIContainer updatedContainer = underTest.saveContainerFromInstance("demoapp", updatedInstance); - Calendar updatedTimestamp = updatedContainer.getLastUpdatedTimestamp(); - - assertThat(updatedTimestamp, is(not(createdTimestamp))); - - List lsContainer = underTest.getRecentlyUpdatedContainers(); - assertThatContainerIsCorrect(lsContainer, updatedContainer, updatedInstance); - } - } - } - - private void assertThatContainerIsCorrect(List lsContainer, APIContainer containerToVerify, - InstanceInfo instance) { - assertThat(lsContainer.size(), is(1)); - assertThatMetadataAreCorrect(containerToVerify, instance.getMetadata()); - - Set apiServices = containerToVerify.getServices(); - assertThat(apiServices.size(), is(1)); - assertThatInstanceIsCorrect(apiServices.iterator().next(), instance); - } - - private void assertThatInstanceIsCorrect(APIService result, InstanceInfo correct) { - assertThat(result.getServiceId(), is(correct.getAppName().toLowerCase())); - assertThat(result.isSecured(), is(correct.isPortEnabled(InstanceInfo.PortType.SECURE))); - assertThat(result.getHomePageUrl(), is(correct.getHomePageUrl())); - } - - private void assertThatMetadataAreCorrect(APIContainer result, Map correct) { - assertThat(result.getId(), is(correct.get(CATALOG_ID))); - assertThat(result.getTitle(), is(correct.get(CATALOG_TITLE))); - assertThat(result.getDescription(), is(correct.get(CATALOG_DESCRIPTION))); - assertThat(result.getVersion(), is(correct.get(CATALOG_VERSION))); - } - } - - @Nested - class WhenRemovingService { - @Nested - class GivenServiceExists { - InstanceInfo removedInstance; - String removedInstanceFamilyId = "service1"; - - @BeforeEach - void prepareExistingInstance() { - Map metadata = new HashMap<>(); - metadata.put(CATALOG_ID, "demoapp"); - metadata.put(CATALOG_TITLE, "Title"); - metadata.put(CATALOG_DESCRIPTION, "Description"); - metadata.put(CATALOG_VERSION, "1.0.0"); - metadata.put(SERVICE_TITLE, "sTitle"); - metadata.put(SERVICE_DESCRIPTION, "sDescription"); - removedInstance = servicesBuilder.createInstance("service1", InstanceInfo.InstanceStatus.UP, metadata); - - underTest.saveContainerFromInstance(removedInstanceFamilyId, removedInstance); - } - - @Nested - class AndWholeTileIsRemoved { - @Test - void tileIsntPresentInCache() { - underTest.removeInstance(removedInstanceFamilyId, removedInstance); - - // The container should be removed - APIContainer receivedContainer = underTest.getContainerById(removedInstanceFamilyId); - assertThat(receivedContainer, is(nullValue())); - } - } - - @Nested - class AndTileRemains { - InstanceInfo remainingInstance; - - @Nested - class AndTheInstancesAreFromTheSameService { - @BeforeEach - void prepareTileWithMultipleInstancesOfSameService() { - underTest.saveContainerFromInstance(removedInstanceFamilyId, removedInstance); - - Map metadata = new HashMap<>(); - metadata.put(CATALOG_ID, "demoapp"); - metadata.put(CATALOG_TITLE, "Title"); - metadata.put(CATALOG_DESCRIPTION, "Description"); - metadata.put(CATALOG_VERSION, "1.0.0"); - metadata.put(SERVICE_TITLE, "sTitle"); - metadata.put(SERVICE_DESCRIPTION, "sDescription"); - remainingInstance = servicesBuilder.createInstance("service1", InstanceInfo.InstanceStatus.UP, metadata); - underTest.saveContainerFromInstance(removedInstanceFamilyId, remainingInstance); - } - - @Test - void tileIsInCacheButServiceIsntInTile() { - underTest.removeInstance(removedInstanceFamilyId, removedInstance); - - APIContainer result = underTest.getContainerById(removedInstanceFamilyId); - assertThat(result, is(not(nullValue()))); - - Set remainingServices = result.getServices(); - assertThat(remainingServices.size(), is(1)); - APIService remainingService = remainingServices.iterator().next(); - assertThat(remainingService.getInstances().size(), is(1)); - assertThat(remainingService.getInstances().get(0), is("service13")); - } - } - - @Nested - class AndTheInstancesAreFromDifferentService { - @BeforeEach - void prepareTileWithMultipleInstancesOfSameService() { - underTest.saveContainerFromInstance(removedInstanceFamilyId, removedInstance); - - Map metadata = new HashMap<>(); - metadata.put(CATALOG_ID, "demoapp"); - metadata.put(CATALOG_TITLE, "Title"); - metadata.put(CATALOG_DESCRIPTION, "Description"); - metadata.put(CATALOG_VERSION, "1.0.0"); - metadata.put(SERVICE_TITLE, "sTitle"); - metadata.put(SERVICE_DESCRIPTION, "sDescription"); - remainingInstance = servicesBuilder.createInstance("service2", InstanceInfo.InstanceStatus.UP, metadata); - underTest.saveContainerFromInstance(removedInstanceFamilyId, remainingInstance); - } - - @Test - void tileIsInCacheButServiceIsntInTile() { - underTest.removeInstance(removedInstanceFamilyId, removedInstance); - - APIContainer result = underTest.getContainerById(removedInstanceFamilyId); - assertThat(result, is(not(nullValue()))); - - Set remainingServices = result.getServices(); - assertThat(remainingServices.size(), is(1)); - - APIService remainingService = remainingServices.iterator().next(); - assertThat(remainingService.getServiceId(), is("service2")); - } - } - } - - @Nested - class GivenRemovingNonExistentService { - @Test - void nothingHappens() { - underTest.removeInstance("nonexistent", removedInstance); - - APIContainer result = underTest.getContainerById(removedInstanceFamilyId); - assertThat(result, is(not(nullValue()))); - } - - } - } - } - - @Nested - class WhenRetrievingUpdatedContainers { - @Nested - class GivenExistingTilesAreValid { - @Test - void returnExistingTiles() { - underTest.saveContainerFromInstance("demoapp", servicesBuilder.instance1); - underTest.saveContainerFromInstance("demoapp2", servicesBuilder.instance2); - - Collection containers = underTest.getRecentlyUpdatedContainers(); - assertEquals(2, containers.size()); - } - } - - @Nested - class GivenOneTileIsTooOld { - @Test - void returnOnlyOneService() throws InterruptedException { - // To speed up the test, create instance which consider even 5 milliseconds as - // old. - underTest = new CachedProductFamilyService( - null, - transformService, - 5, null); - // This is considered as old update. - underTest.saveContainerFromInstance("demoapp", servicesBuilder.instance1); - - Thread.sleep(10); - - underTest.saveContainerFromInstance("demoapp2", servicesBuilder.instance2); - - Collection containers = underTest.getRecentlyUpdatedContainers(); - assertEquals(1, containers.size()); - } - } - } - - @Nested - class WhenCalculatingContainerTotals { - @Nested - class AndStatusIsInvolved { - InstanceInfo instance1; - InstanceInfo instance2; - - @BeforeEach - void prepareApplications() { - instance1 = servicesBuilder.createInstance("service1", "demoapp"); - instance2 = servicesBuilder.createInstance("service2", "demoapp"); - Application application1 = new Application(); - application1.addInstance(instance1); - Application application2 = new Application(); - application2.addInstance(instance2); - - when(cachedServicesService.getService("service1")).thenReturn(application1); - when(cachedServicesService.getService("service2")).thenReturn(application2); - underTest = new CachedProductFamilyService( - cachedServicesService, - transformService, - cacheRefreshUpdateThresholdInMillis, null); - } - - @Nested - class GivenAllServicesAreUp { - @Test - void containerStatusIsUp() { - underTest.saveContainerFromInstance("demoapp", instance1); - underTest.addServiceToContainer("demoapp", instance2); - - APIContainer container = underTest.getContainerById("demoapp"); - assertNotNull(container); - - underTest.calculateContainerServiceValues(container); - assertThatContainerHasValidState(container, "UP", 2); - } - } - - @Nested - class GivenAllServicesAreDown { - @Test - void containerStatusIsDown() { - instance1.setStatus(InstanceInfo.InstanceStatus.DOWN); - instance2.setStatus(InstanceInfo.InstanceStatus.DOWN); - - underTest.saveContainerFromInstance("demoapp", instance1); - underTest.addServiceToContainer("demoapp", instance2); - - APIContainer container = underTest.getContainerById("demoapp"); - assertNotNull(container); - - underTest.calculateContainerServiceValues(container); - assertThatContainerHasValidState(container, "DOWN", 0); - } - } - - @Nested - class GivenSomeServicesAreDown { - @Test - void containerStatusIsWarning() { - instance2.setStatus(InstanceInfo.InstanceStatus.DOWN); - - underTest.saveContainerFromInstance("demoapp", instance1); - underTest.addServiceToContainer("demoapp", instance2); - - APIContainer container = underTest.getContainerById("demoapp"); - assertNotNull(container); - - underTest.calculateContainerServiceValues(container); - assertThatContainerHasValidState(container, "WARNING", 1); - } - } - } - - @Nested - class GivenMultipleApiIds { - @Test - void groupThem() { - Application application = servicesBuilder.createApp( - SERVICE_ID, - servicesBuilder.createInstance(SERVICE_ID, "catalog1", - Pair.of("apiml.apiInfo.api-v1.apiId", "api1"), - Pair.of("apiml.apiInfo.api-v1.version", "1.0.0"), - Pair.of("apiml.apiInfo.api-v2.apiId", "api2"), - Pair.of("apiml.apiInfo.api-v2.version", "2"), - Pair.of("apiml.apiInfo.api-v3.apiId", "api3"))); - doReturn(application).when(cachedServicesService).getService(SERVICE_ID); - APIContainer apiContainer = underTest.getContainerById(SERVICE_ID); - underTest.calculateContainerServiceValues(apiContainer); - - APIService apiService = apiContainer.getServices().iterator().next(); - assertNotNull(apiService.getApis()); - assertEquals(3, apiService.getApis().size()); - assertNotNull(apiService.getApis().get("api1 v1.0.0")); - assertNotNull(apiService.getApis().get("api2 v2")); - assertNotNull(apiService.getApis().get("default")); - } - } - - @Nested - class AndSsoInvolved { - - @Nested - class GivenSsoAndNonSsoInstances { - @Test - void returnNonSso() { - Application application = servicesBuilder.createApp( - SERVICE_ID, - servicesBuilder.createInstance(SERVICE_ID, "catalog1", - Pair.of(AUTHENTICATION_SCHEME, "bypass")), - servicesBuilder.createInstance(SERVICE_ID, "catalog2", - Pair.of(AUTHENTICATION_SCHEME, "zoweJwt"))); - doReturn(application).when(cachedServicesService).getService(SERVICE_ID); - - APIContainer apiContainer = underTest.getContainerById(SERVICE_ID); - underTest.calculateContainerServiceValues(apiContainer); - - assertFalse(apiContainer.isSso()); - for (APIService apiService : apiContainer.getServices()) { - assertFalse(apiService.isSsoAllInstances()); - } - } - } - - @Nested - class GivenAllInstancesAreSso { - @Test - void returnSso() { - InstanceInfo instanceInfo = servicesBuilder.createInstance(SERVICE_ID, "catalog1", - Pair.of(AUTHENTICATION_SCHEME, "zoweJwt")); - doReturn(servicesBuilder.createApp(SERVICE_ID, instanceInfo)).when(cachedServicesService) - .getService(SERVICE_ID); - APIContainer apiContainer = underTest.getContainerById(SERVICE_ID); - underTest.calculateContainerServiceValues(apiContainer); - - assertTrue(apiContainer.isSso()); - for (APIService apiService : apiContainer.getServices()) { - assertTrue(apiService.isSso()); - assertTrue(apiService.isSsoAllInstances()); - } - } - } - } - - @Nested - class GivenHideServiceInfo { - @Test - void thenSetToApiService() { - InstanceInfo instanceInfo = servicesBuilder.createInstance(SERVICE_ID, "catalog1", - Pair.of(AUTHENTICATION_SCHEME, "zoweJwt")); - doReturn(servicesBuilder.createApp(SERVICE_ID, instanceInfo)).when(cachedServicesService) - .getService(SERVICE_ID); - APIContainer apiContainer = underTest.getContainerById(SERVICE_ID); - ReflectionTestUtils.setField(underTest, "hideServiceInfo", true); - underTest.calculateContainerServiceValues(apiContainer); - assertTrue(apiContainer.isHideServiceInfo()); - } - } - - void assertThatContainerHasValidState(APIContainer container, String state, int activeServices) { - assertNotNull(container); - - underTest.calculateContainerServiceValues(container); - assertEquals(state, container.getStatus()); - assertEquals(2, container.getTotalServices().intValue()); - assertEquals(activeServices, container.getActiveServices().intValue()); - } - } - - @Nested - class WhenGettingContainerWithoutServiceInstances { - @Test - void noServicesAreWithinTheContainer() { - assertThat(underTest.getContainerCount(), is(0)); - assertThat(underTest.getAllContainers().size(), is(0)); - } - } - - @Nested - class MultiTenancy { - - private CachedProductFamilyService cachedProductFamilyService; - - @BeforeEach - void init() { - cachedProductFamilyService = new CachedProductFamilyService( - new CachedServicesService(), - new TransformService(new GatewayClient(ServiceAddress.builder().scheme("https").hostname("localhost").build())), - 30000, - new CustomStyleConfig() - ); - } - - private APIService createDto(RegistrationType registrationType) { - Map metadata = new HashMap<>(); - metadata.put(APIML_ID, "apimlId"); - metadata.put(SERVICE_TITLE, "title"); - metadata.put(REGISTRATION_TYPE, registrationType.getValue()); - var service = InstanceInfo.Builder.newBuilder() - .setAppName(CoreService.GATEWAY.getServiceId()) - .setMetadata(metadata) - .build(); - return cachedProductFamilyService.createAPIServiceFromInstance(service); - } - - @Test - void givenPrimaryInstance_whenCreateDto_thenDoNotUpdateTitle() { - var dto = createDto(RegistrationType.ADDITIONAL); - assertEquals("title (apimlId)", dto.getTitle()); - assertEquals("apimlid", dto.getServiceId()); - assertEquals("/apimlid", dto.getBasePath()); - } - - @Test - void givenPrimaryInstance_whenCreateDto_thenAddApimlIdIntoTitle() { - var dto = createDto(RegistrationType.PRIMARY); - assertEquals("title", dto.getTitle()); - assertEquals("gateway", dto.getServiceId()); - assertEquals("/", dto.getBasePath()); - } - - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesServiceTest.java deleted file mode 100644 index 78cc6e0b85..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedServicesServiceTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.cached; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import com.netflix.discovery.shared.Applications; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; - -class CachedServicesServiceTest { - - @Test - void testReturningNoServices() { - CachedServicesService cachedServicesService = new CachedServicesService(); - cachedServicesService.clearAllServices(); - Applications services = cachedServicesService.getAllCachedServices(); - Assertions.assertNull(services); - } - - @Test - void testReturningOneService() { - CachedServicesService cachedServicesService = new CachedServicesService(); - cachedServicesService.clearAllServices(); - Applications applications = cachedServicesService.getAllCachedServices(); - Assertions.assertNull(applications); - - InstanceInfo instance = getStandardInstance("service", InstanceInfo.InstanceStatus.DOWN, null); - Application application = new Application("service"); - application.addInstance(instance); - cachedServicesService.updateService("service", application); - - applications = cachedServicesService.getAllCachedServices(); - Assertions.assertNotNull(applications.getRegisteredApplications()); - } - - @Test - void testGetAService() { - CachedServicesService cachedServicesService = new CachedServicesService(); - cachedServicesService.clearAllServices(); - Applications applications = cachedServicesService.getAllCachedServices(); - Assertions.assertNull(applications); - - InstanceInfo instance = getStandardInstance("service", InstanceInfo.InstanceStatus.DOWN, null); - Application application = new Application("service"); - application.addInstance(instance); - cachedServicesService.updateService("service", application); - - Application service = cachedServicesService.getService("service"); - Assertions.assertNotNull(service); - Assertions.assertEquals("service", service.getName()); - } - - private InstanceInfo getStandardInstance(String serviceId, InstanceInfo.InstanceStatus status, - HashMap metadata) { - return new InstanceInfo(serviceId, serviceId.toUpperCase(), null, "192.168.0.1", null, - new InstanceInfo.PortWrapper(true, 9090), null, null, null, null, null, null, null, 0, null, "hostname", - status, null, null, null, null, metadata, null, null, null, null); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/ApiServiceStatusServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/ApiServiceStatusServiceTest.java deleted file mode 100644 index 060ccf0bde..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/ApiServiceStatusServiceTest.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status; - -import org.junit.jupiter.api.Nested; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.model.APIService; -import org.zowe.apiml.apicatalog.services.cached.CachedApiDocService; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.apicatalog.services.status.event.model.ContainerStatusChangeEvent; -import org.zowe.apiml.apicatalog.services.status.event.model.STATUS_EVENT_TYPE; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Applications; -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.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.zowe.apiml.apicatalog.services.status.model.ApiDiffNotAvailableException; - -import java.util.*; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class ApiServiceStatusServiceTest { - - @Mock - private CachedProductFamilyService cachedProductFamilyService; - - @Mock - private CachedServicesService cachedServicesService; - - @Mock - private CachedApiDocService cachedApiDocService; - - @Mock - private OpenApiCompareProducer openApiCompareProducer; - - @InjectMocks - private APIServiceStatusService apiServiceStatusService; - - @Test - void givenCachedServices_whenGetCachedApplicationsState_thenReturnState() { - when(cachedServicesService.getAllCachedServices()).thenReturn(new Applications()); - ResponseEntity responseEntity = apiServiceStatusService.getCachedApplicationStateResponse(); - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - } - - @Test - void givenContainers_whenGetContainersStateEvents_thenReturnEvents() { - List containers = new ArrayList<>(createContainers()); - when(cachedProductFamilyService.getAllContainers()).thenReturn(containers); - doNothing().when(this.cachedProductFamilyService).calculateContainerServiceValues(any(APIContainer.class)); - - List expectedEvents = new ArrayList<>(); - containers.forEach(container -> { - STATUS_EVENT_TYPE eventType; - if (InstanceInfo.InstanceStatus.DOWN.name().equalsIgnoreCase(container.getStatus())) { - eventType = STATUS_EVENT_TYPE.CANCEL; - } else if (container.getCreatedTimestamp().equals(container.getLastUpdatedTimestamp())) { - eventType = STATUS_EVENT_TYPE.CREATED_CONTAINER; - } else { - eventType = STATUS_EVENT_TYPE.RENEW; - } - expectedEvents.add(new ContainerStatusChangeEvent( - container.getId(), - container.getTitle(), - container.getStatus(), - container.getTotalServices(), - container.getActiveServices(), - container.getServices(), - eventType) - ); - }); - - List actualEvents = apiServiceStatusService.getContainersStateAsEvents(); - assertEquals(expectedEvents.size(), actualEvents.size()); - for (int eventIndex = 0; eventIndex < expectedEvents.size(); eventIndex++) { - assertEquals(expectedEvents.get(eventIndex), actualEvents.get(eventIndex)); - } - } - - @Nested - class GivenCachedApiDoc { - @Test - void whenGetApiDocForService_thenSuccessfulResponse() { - String apiDoc = "this is the api doc"; - when(cachedApiDocService.getApiDocForService(anyString(), anyString())).thenReturn(apiDoc); - - ResponseEntity expectedResponse = new ResponseEntity<>(apiDoc, HttpStatus.OK); - ResponseEntity actualResponse = apiServiceStatusService.getServiceCachedApiDocInfo("aaa", "v1"); - - assertEquals(expectedResponse.getStatusCode(), actualResponse.getStatusCode()); - assertEquals(expectedResponse.getBody(), actualResponse.getBody()); - } - - @Test - void whenGetDefaultApiDocForService_thenSuccessfulResponse() { - String apiDoc = "this is the api doc"; - when(cachedApiDocService.getDefaultApiDocForService(anyString())).thenReturn(apiDoc); - - ResponseEntity expectedResponse = new ResponseEntity<>(apiDoc, HttpStatus.OK); - ResponseEntity actualResponse = apiServiceStatusService.getServiceCachedDefaultApiDocInfo("aaa"); - - assertEquals(expectedResponse.getStatusCode(), actualResponse.getStatusCode()); - assertEquals(expectedResponse.getBody(), actualResponse.getBody()); - } - - @Test - void whenGetApiDiff_thenReturnsApiDiff() { - String apiDoc = "{}"; - when(cachedApiDocService.getApiDocForService(anyString(), anyString())).thenReturn(apiDoc); - OpenApiCompareProducer actualProducer = new OpenApiCompareProducer(); - when(openApiCompareProducer.fromContents(anyString(), anyString())).thenReturn(actualProducer.fromContents(apiDoc, apiDoc)); - ResponseEntity actualResponse = apiServiceStatusService.getApiDiffInfo("service", "v1", "v2"); - assertNotNull(actualResponse.getBody()); - assertTrue(actualResponse.getBody().contains("Api Change Log")); - assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); - } - } - - @Test - void givenInvalidAPIs_whenDifferenceIsProduced_thenTheProperExceptionIsRaised() { - when(openApiCompareProducer.fromContents(anyString(), anyString())).thenThrow(new RuntimeException()); - Exception ex = assertThrows(ApiDiffNotAvailableException.class, () -> - apiServiceStatusService.getApiDiffInfo("service", "v1", "v2") - ); - assertEquals("Error retrieving API diff for 'service' with versions 'v1' and 'v2'", ex.getMessage()); - } - - @Test - void givenUpdatedContainers_whenGetRecentlyUpdatedContainers_thenUpdatedContainersAreReturned() { - List containers = createContainers(); - when(cachedProductFamilyService.getRecentlyUpdatedContainers()).thenReturn(containers); - doNothing().when(this.cachedProductFamilyService).calculateContainerServiceValues(any(APIContainer.class)); - List events = apiServiceStatusService.getRecentlyUpdatedContainersAsEvents(); - assertNotNull(events); - assertEquals(2, events.size()); - assertEquals("API One", events.get(0).getTitle()); - assertEquals("API Two", events.get(1).getTitle()); - } - - private List createContainers() { - Set services = new HashSet<>(); - - APIService service = new APIService.Builder("service1") - .title("service-1") - .description("service-1") - .secured(false) - .baseUrl("base") - .homePageUrl("home") - .basePath("base") - .sso(false) - .apis(Collections.emptyMap()) - .build(); - services.add(service); - - service = new APIService.Builder("service2") - .title("service-2") - .description("service-2") - .secured(true) - .baseUrl("base") - .homePageUrl("home") - .basePath("base") - .sso(false) - .apis(Collections.emptyMap()) - .build(); - services.add(service); - - APIContainer container = new APIContainer("api-one", "API One", "This is API One", services); - container.setTotalServices(2); - container.setActiveServices(2); - container.setStatus(InstanceInfo.InstanceStatus.UP.name()); - APIContainer container1 = new APIContainer("api-two", "API Two", "This is API Two", services); - container1.setTotalServices(2); - container1.setActiveServices(2); - container.setStatus(InstanceInfo.InstanceStatus.DOWN.name()); - return Arrays.asList(container, container1); - } - - private InstanceInfo getStandardInstance(String serviceId, InstanceInfo.InstanceStatus status, - HashMap metadata, String ipAddress, int port) { - return new InstanceInfo(serviceId + ":" + port, serviceId.toUpperCase(), null, ipAddress, null, - new InstanceInfo.PortWrapper(true, port), null, null, null, null, null, null, null, 0, null, "hostname", - status, null, null, null, null, metadata, null, null, null, null); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java deleted file mode 100644 index 35ee59fe3b..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/LocalApiDocServiceTest.java +++ /dev/null @@ -1,504 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status; - -import com.netflix.appinfo.InstanceInfo; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.HttpStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.apicatalog.instance.InstanceRetrievalService; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; -import org.zowe.apiml.apicatalog.services.status.model.ApiVersionNotFoundException; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.gateway.GatewayClient; -import org.zowe.apiml.product.instance.ServiceAddress; -import org.zowe.apiml.util.HttpClientMockHelper; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class LocalApiDocServiceTest { - private static final String SERVICE_ID = "service"; - private static final String SERVICE_HOST = "service"; - private static final int SERVICE_PORT = 8080; - private static final String SERVICE_VERSION = "1.0.0"; - private static final String HIGHER_SERVICE_VERSION = "2.0.0"; - private static final String SERVICE_VERSION_V = "test.app v1.0.0"; - private static final String HIGHER_SERVICE_VERSION_V = "test.app v2.0.0"; - private static final String GATEWAY_SCHEME = "http"; - private static final String GATEWAY_HOST = "gateway:10000"; - private static final String GATEWAY_URL = "api/v1"; - private static final String API_ID = "test.app"; - private static final String SWAGGER_URL = "https://service:8080/service/api-doc"; - - @Mock - private InstanceRetrievalService instanceRetrievalService; - - private APIDocRetrievalService apiDocRetrievalService; - - @Mock - private ApimlLogger apimlLogger; - - @Mock - private CloseableHttpClient httpClient; - - @Mock - private CloseableHttpResponse response; - - @BeforeEach - void setup() { - HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); - - apiDocRetrievalService = new APIDocRetrievalService( - httpClient, - instanceRetrievalService, - new GatewayClient(getProperties())); - - ReflectionTestUtils.setField(apiDocRetrievalService, "apimlLogger", apimlLogger); - } - - @Nested - class WhenGetApiDoc { - @Test - void givenValidApiInfo_thenReturnApiDoc() { - String responseBody = "api-doc body"; - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getStandardMetadata(), true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); - - assertEquals(API_ID, actualResponse.getApiInfo().getApiId()); - assertEquals(GATEWAY_URL, actualResponse.getApiInfo().getGatewayUrl()); - assertEquals(SERVICE_VERSION, actualResponse.getApiInfo().getVersion()); - assertEquals(SWAGGER_URL, actualResponse.getApiInfo().getSwaggerUrl()); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - assertEquals(responseBody, actualResponse.getApiDocContent()); - - assertEquals("[api -> api=RoutedService(subServiceId=api-v1, gatewayUrl=api, serviceUrl=/)]", actualResponse.getRoutes().toString()); - } - - @Nested - class ThenThrowException { - @Test - void givenNoApiDocFoundForService() { - Exception exception = assertThrows(ApiDocNotFoundException.class, () -> apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)); - assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); - } - - @Test - void givenServerErrorWhenRequestingSwaggerUrl() { - String responseBody = "Server not found"; - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getStandardMetadata(), true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_INTERNAL_SERVER_ERROR, responseBody); - - Exception exception = assertThrows(ApiDocNotFoundException.class, () -> apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)); - assertEquals("No API Documentation was retrieved due to " + SERVICE_ID + " server error: '" + responseBody + "'.", exception.getMessage()); - } - - @Test - void givenNoInstanceMetadata() { - String responseBody = "api-doc body"; - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(new HashMap<>(), true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - Exception exception = assertThrows(ApiDocNotFoundException.class, () -> apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)); - assertEquals("No API Documentation defined for service " + SERVICE_ID + ".", exception.getMessage()); - } - - } - - @Test - void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() { - //language=JSON - String generatedResponseBody = """ - { - "swagger": "2.0", - "info": { - "title": "Test service" - , "description": "Test service description" - , "version": "1.0.0" - }, - "host": "gateway:10000", - "basePath": "/service/api/v1", - "schemes": ["http"], - "tags": [ - { - "name": "apimlHidden" - } - ], - "paths": { - "/apimlHidden": { - "get": { - "tags": ["apimlHidden"], - "responses": { - "200": { - "description": "OK" - } - } - } - } - } - } - """; - String responseBody = "api-doc body"; - - generatedResponseBody = generatedResponseBody.replaceAll("\\s+", ""); - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getMetadataWithoutSwaggerUrl(), true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); - - assertEquals(API_ID, actualResponse.getApiInfo().getApiId()); - assertEquals(GATEWAY_URL, actualResponse.getApiInfo().getGatewayUrl()); - assertEquals(SERVICE_VERSION, actualResponse.getApiInfo().getVersion()); - assertNull(actualResponse.getApiInfo().getSwaggerUrl()); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - assertEquals(generatedResponseBody, actualResponse.getApiDocContent().replaceAll("\\s+", "")); - - assertEquals("[api -> api=RoutedService(subServiceId=api-v1, gatewayUrl=api, serviceUrl=/)]", actualResponse.getRoutes().toString()); - } - - @Test - void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() { - String responseBody = "api-doc body"; - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getMetadataWithoutApiInfo(), true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - - assertEquals(responseBody, actualResponse.getApiDocContent()); - } - - @Test - void shouldCreateApiDocUrlFromRoutingAndUseHttp() { - String responseBody = "api-doc body"; - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getMetadataWithoutApiInfo(), false)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - - assertEquals(responseBody, actualResponse.getApiDocContent()); - } - - @Test - void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() { - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getStandardMetadata(), true)); - - var exception = new IOException("Unable to reach the host"); - HttpClientMockHelper.whenExecuteThenThrow(httpClient, exception); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); - - assertEquals(API_ID, actualResponse.getApiInfo().getApiId()); - assertEquals(GATEWAY_URL, actualResponse.getApiInfo().getGatewayUrl()); - assertEquals(SERVICE_VERSION, actualResponse.getApiInfo().getVersion()); - assertEquals(SWAGGER_URL, actualResponse.getApiInfo().getSwaggerUrl()); - - assertEquals("", actualResponse.getApiDocContent()); - - verify(apimlLogger, times(1)).log("org.zowe.apiml.apicatalog.apiDocHostCommunication", SERVICE_ID, exception.getMessage()); - } - } - - @Nested - class WhenGetDefaultApiDoc { - @Test - void givenDefaultApiDoc_thenReturnIt() { - String responseBody = "api-doc body"; - Map metadata = getMetadataWithMultipleApiInfo(); - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(metadata, true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); - - assertEquals(API_ID, actualResponse.getApiInfo().getApiId()); - assertEquals(GATEWAY_URL, actualResponse.getApiInfo().getGatewayUrl()); - assertEquals(SERVICE_VERSION, actualResponse.getApiInfo().getVersion()); - assertEquals(SWAGGER_URL, actualResponse.getApiInfo().getSwaggerUrl()); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - assertEquals(responseBody, actualResponse.getApiDocContent()); - - assertEquals("[api -> api=RoutedService(subServiceId=api-v1, gatewayUrl=api, serviceUrl=/)]", actualResponse.getRoutes().toString()); - } - - @Test - void givenNoDefaultApiDoc_thenReturnHighestVersion() { - String responseBody = "api-doc body"; - Map metadata = getMetadataWithMultipleApiInfo(); - metadata.remove(API_INFO + ".1." + API_INFO_IS_DEFAULT); // unset default API, so higher version becomes default - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(metadata, true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); - - assertEquals(API_ID, actualResponse.getApiInfo().getApiId()); - assertEquals(GATEWAY_URL, actualResponse.getApiInfo().getGatewayUrl()); - assertEquals(HIGHER_SERVICE_VERSION, actualResponse.getApiInfo().getVersion()); - assertEquals(SWAGGER_URL, actualResponse.getApiInfo().getSwaggerUrl()); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - assertEquals(responseBody, actualResponse.getApiDocContent()); - - assertEquals("[api -> api=RoutedService(subServiceId=api-v1, gatewayUrl=api, serviceUrl=/)]", actualResponse.getRoutes().toString()); - } - - @Test - void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() { - String responseBody = "api-doc body"; - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getMetadataWithMultipleApiInfoWithDifferentVersionFormat(), true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); - - assertEquals(API_ID, actualResponse.getApiInfo().getApiId()); - assertEquals(GATEWAY_URL, actualResponse.getApiInfo().getGatewayUrl()); - assertEquals(HIGHER_SERVICE_VERSION_V, actualResponse.getApiInfo().getVersion()); - assertEquals(SWAGGER_URL, actualResponse.getApiInfo().getSwaggerUrl()); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - assertEquals(responseBody, actualResponse.getApiDocContent()); - - assertEquals("[api -> api=RoutedService(subServiceId=api-v1, gatewayUrl=api, serviceUrl=/)]", actualResponse.getRoutes().toString()); - } - - @Test - void givenNoApiDocs_thenReturnNull() { - String responseBody = "api-doc body"; - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getMetadataWithoutApiInfo(), true)); - - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); - - ApiDocInfo actualResponse = apiDocRetrievalService.retrieveDefaultApiDoc(SERVICE_ID); - - assertNotNull(actualResponse); - assertNotNull(actualResponse.getApiDocContent()); - - assertEquals(responseBody, actualResponse.getApiDocContent()); - } - } - - @Nested - class WhenGetApiVersions { - @Test - void givenApiVersions_thenReturnThem() { - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getStandardMetadata(), false)); - - List actualVersions = apiDocRetrievalService.retrieveApiVersions(SERVICE_ID); - assertEquals(Collections.singletonList(SERVICE_VERSION_V), actualVersions); - } - - @Test - void givenNoApiVersions_thenThrowException() { - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)).thenReturn(null); - - Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> - apiDocRetrievalService.retrieveApiVersions(SERVICE_ID) - ); - assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); - } - } - - @Nested - class WhenGetDefaultApiVersion { - @Test - void givenDefaultApiVersion_thenReturnIt() { - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(getMetadataWithMultipleApiInfo(), false)); - - String defaultVersion = apiDocRetrievalService.retrieveDefaultApiVersion(SERVICE_ID); - assertEquals(SERVICE_VERSION_V, defaultVersion); - } - - @Test - void givenNoDefaultApiVersion_thenReturnHighestVersion() { - Map metadata = getMetadataWithMultipleApiInfo(); - metadata.remove(API_INFO + ".1." + API_INFO_IS_DEFAULT); // unset default API, so higher version becomes default - - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)) - .thenReturn(getStandardInstance(metadata, false)); - - String defaultVersion = apiDocRetrievalService.retrieveDefaultApiVersion(SERVICE_ID); - assertEquals(HIGHER_SERVICE_VERSION_V, defaultVersion); - } - - @Test - void givenNoApiInfo_thenThrowException() { - when(instanceRetrievalService.getInstanceInfo(SERVICE_ID)).thenReturn(null); - - Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> - apiDocRetrievalService.retrieveDefaultApiVersion(SERVICE_ID) - ); - assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); - } - } - - private InstanceInfo getStandardInstance(Map metadata, Boolean isPortSecure) { - return InstanceInfo.Builder.newBuilder() - .setAppName(SERVICE_ID) - .setHostName(SERVICE_HOST) - .setPort(SERVICE_PORT) - .setSecurePort(SERVICE_PORT) - .enablePort(InstanceInfo.PortType.SECURE, isPortSecure) - .setStatus(InstanceInfo.InstanceStatus.UP) - .setMetadata(metadata) - .build(); - } - - private Map getStandardMetadata() { - Map metadata = new HashMap<>(); - metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); - metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); - metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION); - metadata.put(API_INFO + ".1." + API_INFO_SWAGGER_URL, SWAGGER_URL); - metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); - metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); - metadata.put(SERVICE_TITLE, "Test service"); - metadata.put(SERVICE_DESCRIPTION, "Test service description"); - - return metadata; - } - - private Map getMetadataWithoutSwaggerUrl() { - Map metadata = new HashMap<>(); - metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); - metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); - metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION); - metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); - metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); - metadata.put(SERVICE_TITLE, "Test service"); - metadata.put(SERVICE_DESCRIPTION, "Test service description"); - - return metadata; - } - - private Map getMetadataWithMultipleApiInfo() { - Map metadata = new HashMap<>(); - metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); - metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); - metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION); - metadata.put(API_INFO + ".1." + API_INFO_SWAGGER_URL, SWAGGER_URL); - metadata.put(API_INFO + ".1." + API_INFO_IS_DEFAULT, "true"); - - metadata.put(API_INFO + ".2." + API_INFO_API_ID, API_ID); - metadata.put(API_INFO + ".2." + API_INFO_GATEWAY_URL, GATEWAY_URL); - metadata.put(API_INFO + ".2." + API_INFO_VERSION, HIGHER_SERVICE_VERSION); - metadata.put(API_INFO + ".2." + API_INFO_SWAGGER_URL, SWAGGER_URL); - - metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); - metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); - metadata.put(SERVICE_TITLE, "Test service"); - metadata.put(SERVICE_DESCRIPTION, "Test service description"); - - return metadata; - } - - private Map getMetadataWithMultipleApiInfoWithDifferentVersionFormat() { - Map metadata = new HashMap<>(); - metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); - metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); - metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION_V); - metadata.put(API_INFO + ".1." + API_INFO_SWAGGER_URL, SWAGGER_URL); - - metadata.put(API_INFO + ".2." + API_INFO_API_ID, API_ID); - metadata.put(API_INFO + ".2." + API_INFO_GATEWAY_URL, GATEWAY_URL); - metadata.put(API_INFO + ".2." + API_INFO_VERSION, HIGHER_SERVICE_VERSION_V); - metadata.put(API_INFO + ".2." + API_INFO_SWAGGER_URL, SWAGGER_URL); - - metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); - metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); - metadata.put(SERVICE_TITLE, "Test service"); - metadata.put(SERVICE_DESCRIPTION, "Test service description"); - - return metadata; - } - - private Map getMetadataWithoutApiInfo() { - Map metadata = new HashMap<>(); - metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); - metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); - metadata.put(ROUTES + ".apidoc." + ROUTES_GATEWAY_URL, "api/v1/api-doc"); - metadata.put(ROUTES + ".apidoc." + ROUTES_SERVICE_URL, SERVICE_ID + "/api-doc"); - metadata.put(SERVICE_TITLE, "Test service"); - metadata.put(SERVICE_DESCRIPTION, "Test service description"); - - return metadata; - } - - private ServiceAddress getProperties() { - return ServiceAddress.builder() - .scheme(GATEWAY_SCHEME) - .hostname(GATEWAY_HOST) - .build(); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListenerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListenerTest.java deleted file mode 100644 index 31351a8e0b..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/status/listeners/GatewayLookupEventListenerTest.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.services.status.listeners; - -import org.junit.jupiter.api.Test; -import org.zowe.apiml.apicatalog.instance.InstanceInitializeService; - -import static org.mockito.Mockito.*; - -class GatewayLookupEventListenerTest { - - @Test - void retrieveAndRegisterAllInstancesWithCatalog_onlyOnce() throws Exception { - InstanceInitializeService instanceInitializeService = mock(InstanceInitializeService.class); - GatewayLookupEventListener gatewayLookupEventListener = new GatewayLookupEventListener(instanceInitializeService); - for (int i = 0; i < 10; i++) { - gatewayLookupEventListener.onApplicationEvent(); - } - verify(instanceInitializeService, times(1)).retrieveAndRegisterAllInstancesWithCatalog(); - } - - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMockTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMockTest.java deleted file mode 100644 index 44a6db6796..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ApiDocTransformForMockTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.zowe.apiml.product.instance.ServiceAddress; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@SpringBootTest(classes = ApiDocTransformForMock.class, properties = { - "apiml.catalog.standalone.enabled=true", - "server.hostname=host", - "server.port=123", - "service.schema=http" -}) -class ApiDocTransformForMockTest { - - @Autowired - private ServiceAddress gatewayConfigProperties; - - @Test - void gatewayConfigPropertiesForMock() { - assertNotNull(gatewayConfigProperties); - assertEquals("host:123/apicatalog/mock", gatewayConfigProperties.getHostname()); - assertEquals("http", gatewayConfigProperties.getScheme()); - } - -} \ No newline at end of file diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ExampleServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ExampleServiceTest.java deleted file mode 100644 index ed911c0e2f..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/ExampleServiceTest.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import io.swagger.parser.OpenAPIParser; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.responses.ApiResponse; -import io.swagger.v3.oas.models.responses.ApiResponses; -import io.swagger.v3.parser.core.models.SwaggerParseResult; - -import org.apache.commons.io.IOUtils; -import org.json.JSONArray; -import org.json.JSONException; -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.mockito.Mockito; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.util.ReflectionTestUtils; - -import java.io.IOException; -import java.nio.charset.Charset; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -class ExampleServiceTest { - - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class ExampleRepository { - - private final ExampleService exampleService = new ExampleService(); - - @BeforeAll - void test1() throws IOException { - String apiDoc = IOUtils.toString( - new ClassPathResource("standalone/services/apiDocs/service2_zowe v2.0.0.json").getURL(), - Charset.forName("UTF-8") - ); - - ((Map>) ReflectionTestUtils.getField(exampleService, "examples")).clear(); - exampleService.generateExamples("testService", apiDoc); - } - - @Test - void existingGetExample() throws JSONException { - ExampleService.Example example = exampleService.getExample("GET", "/testService/pet/findByStatus"); - assertEquals("GET", example.getMethod()); - assertEquals("/testService/pet/findByStatus", example.getPath()); - assertEquals(200, example.getResponseCode()); - JSONArray json = new JSONArray(example.getContent()); - assertEquals(1, json.length()); - assertEquals(10, json.getJSONObject(0).get("id")); - assertEquals("doggie", json.getJSONObject(0).get("name")); - } - - @Test - void generateExampleDoesNotThrowException() throws IOException { - - String apiDoc = IOUtils.toString( - new ClassPathResource("standalone/services/apiDocs/service2_zowe v2.0.0.json").getURL(), - Charset.forName("UTF-8") - ); - assertDoesNotThrow( () -> exampleService.generateExamples("testService", apiDoc)); - } - - @Test - void generateExampleThrowsExceptionWhenPathIsNull() { - - OpenAPIParser openAPIParser = Mockito.mock(OpenAPIParser.class); - SwaggerParseResult swaggerParseResult = Mockito.mock(SwaggerParseResult.class); - OpenAPI openAPI = Mockito.mock(OpenAPI.class); - - Mockito.when(openAPIParser.readContents(any(),any(),any())).thenReturn(swaggerParseResult); - Mockito.when(swaggerParseResult.getOpenAPI()).thenReturn(openAPI); - Mockito.when(openAPI.getPaths()).thenReturn(null); - assertDoesNotThrow( () -> exampleService.generateExamples("testService", null)); - } - @Test - void nonExistingGetExample() { - ExampleService.Example example = exampleService.getExample("GET", "/unkwnown"); - assertEquals(200, example.getResponseCode()); - assertEquals("{}", example.getContent()); - } - - @Test - void replay() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - exampleService.replyExample(response, "GET", "/unknown"); - assertEquals(200, response.getStatus()); - assertEquals("{}", response.getContentAsString()); - assertEquals(MediaType.APPLICATION_JSON_VALUE, response.getHeaders("Content-Type").get(0)); - } - - } - - @Nested - class Example { - - @Test - void antMatcherIsWorking() { - ExampleService.Example example = ExampleService.Example.builder() - .path("/some/path/{id}") - .build(); - assertTrue(example.isMatching("/some/path/1")); - assertTrue(example.isMatching("/some/path/abc")); - assertFalse(example.isMatching("/some/otherPath/2")); - } - - } - - @Nested - class InternalMethods { - - @Nested - class GetResponseCode { - - @Test - void whenDefaultThenReturn200() { - assertEquals(200, ExampleService.getResponseCode("default")); - } - - @Test - void whenIsNumericThenParse() { - assertEquals(213, ExampleService.getResponseCode("213")); - } - - @Test - void whenIsNotNumericThenReturn0() { - assertEquals(0, ExampleService.getResponseCode("unknown")); - } - - } - - @Nested - class GetFirstApiRespones { - - private Operation createOperation(String...responseCodes) { - Map responses = new LinkedHashMap<>(); - for (String responseCode : responseCodes) { - responses.put(responseCode, mock(ApiResponse.class)); - } - - ApiResponses apiResponses = mock(ApiResponses.class); - Operation operation = mock(Operation.class); - - doReturn(responses.entrySet()).when(apiResponses).entrySet(); - doReturn(apiResponses).when(operation).getResponses(); - - return operation; - } - - @Test - void whenContainsSuccessResponseCode() { - assertEquals("200", ExampleService.getFirstApiResponses(createOperation("303", "200", "400")).getKey()); - } - - @Test - void whenDoesntContainsSuccessResponseCode() { - assertEquals("400", ExampleService.getFirstApiResponses(createOperation("400", "401", "404")).getKey()); - } - - @Test - void whenIsEmpty() { - assertNull(ExampleService.getFirstApiResponses(createOperation())); - } - - } - - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalServiceTest.java deleted file mode 100644 index a91192a5f5..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneAPIDocRetrievalServiceTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class StandaloneAPIDocRetrievalServiceTest { - - private final StandaloneAPIDocRetrievalService standaloneAPIDocRetrievalService = new StandaloneAPIDocRetrievalService(); - - @Nested - class ThenNothing { - - @Test - void whenRetrieveApiDoc() { - assertNull(standaloneAPIDocRetrievalService.retrieveApiDoc("service", null)); - } - - @Test - void whenRetrieveDefaultApiDoc() { - assertNull(standaloneAPIDocRetrievalService.retrieveDefaultApiDoc("service")); - } - - @Test - void whenRetrieveApiVersions() { - List apiVersions = standaloneAPIDocRetrievalService.retrieveApiVersions("service"); - assertTrue(apiVersions.isEmpty()); - } - - @Test - void whenRetrieveDefaultApiVersion() { - assertNull(standaloneAPIDocRetrievalService.retrieveDefaultApiVersion("service")); - } - } - - @Nested - class ThenItDoesntAcceptNullValues { - - @Test - void whenRetrieveApiDoc() { - assertThrows(NullPointerException.class, () -> standaloneAPIDocRetrievalService.retrieveApiDoc(null, null)); - } - - @Test - void whenRetrieveDefaultApiDoc() { - assertThrows(NullPointerException.class, () -> standaloneAPIDocRetrievalService.retrieveDefaultApiDoc(null)); - } - - @Test - void whenRetrieveApiVersions() { - assertThrows(NullPointerException.class, () -> standaloneAPIDocRetrievalService.retrieveApiVersions((String) null)); - } - - @Test - void whenRetrieveDefaultApiVersion() { - assertThrows(NullPointerException.class, () -> standaloneAPIDocRetrievalService.retrieveDefaultApiVersion((String )null)); - } - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializerTest.java deleted file mode 100644 index 9fd0f06b09..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneInitializerTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.mockito.Mockito.*; - -@ExtendWith(SpringExtension.class) -@ActiveProfiles("test") -class StandaloneInitializerTest { - - @Mock - SpringApplication application; - - @Mock - ConfigurableApplicationContext registry; - - @Autowired - private StandaloneLoaderService standaloneLoaderService; - - @Autowired - private ApplicationEventPublisher publisher; - - @Test - void testEventIsCalledOnlyOnce() { - ApplicationReadyEvent event = mock(ApplicationReadyEvent.class); - mockContext(event, true); - - publisher.publishEvent(event); - publisher.publishEvent(event); - - verify(standaloneLoaderService, times(1)).initializeCache(); - } - - @Test - void notCalledIfStandaloneDisabled() { - ApplicationReadyEvent event = mock(ApplicationReadyEvent.class); - mockContext(event, false); - - publisher.publishEvent(event); - - verifyNoInteractions(standaloneLoaderService); - } - - private ConfigurableApplicationContext mockContext(ApplicationReadyEvent event, boolean isStandalone) { - ConfigurableApplicationContext ctx = mock(ConfigurableApplicationContext.class); - ConfigurableEnvironment env = mock(ConfigurableEnvironment.class); - when(event.getApplicationContext()).thenReturn(ctx); - when(env.getProperty("apiml.catalog.standalone.enabled", "false")).thenReturn(String.valueOf(isStandalone)); - when(ctx.getEnvironment()).thenReturn(env); - return ctx; - } - - @Configuration - @Profile("test") - public static class TestConfiguration { - - @Bean - public StandaloneLoaderService standaloneLoaderService() { - return mock(StandaloneLoaderService.class); - } - - @Bean - public StandaloneInitializer getStandaloneInitializer(StandaloneLoaderService standaloneLoaderService) { - return new StandaloneInitializer(standaloneLoaderService); - } - - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderServiceTest.java deleted file mode 100644 index 1a12887559..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/standalone/StandaloneLoaderServiceTest.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.standalone; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.apicatalog.instance.InstanceInitializeService; -import org.zowe.apiml.apicatalog.services.cached.CachedApiDocService; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.apicatalog.services.status.model.ApiDocNotFoundException; -import org.zowe.apiml.apicatalog.services.status.model.ApiVersionNotFoundException; -import org.zowe.apiml.apicatalog.swagger.api.AbstractApiDocService; -import org.zowe.apiml.product.gateway.GatewayClient; -import org.zowe.apiml.product.instance.ServiceAddress; -import org.zowe.apiml.product.routing.transform.TransformService; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -class StandaloneLoaderServiceTest { - - private StandaloneLoaderService standaloneLoaderService; - - @Nested - class WhenInitializeCache { - - private CachedServicesService cachedServicesService; - private CachedApiDocService cachedApiDocService; - private String testData; - - @BeforeEach - void init() throws IOException { - GatewayClient gatewayClient = new GatewayClient(null); - TransformService transformService = new TransformService(gatewayClient); - cachedServicesService = new CachedServicesService(); - CachedProductFamilyService cachedProductFamilyService = - new CachedProductFamilyService(cachedServicesService, transformService, 1000, null); - InstanceInitializeService instanceInitializeService = new InstanceInitializeService(cachedProductFamilyService, cachedServicesService, null, null); - StandaloneAPIDocRetrievalService standaloneAPIDocRetrievalService = new StandaloneAPIDocRetrievalService(); - cachedApiDocService = new CachedApiDocService(standaloneAPIDocRetrievalService, null); - standaloneLoaderService = new StandaloneLoaderService( - new ObjectMapper(), - instanceInitializeService, - cachedApiDocService, - standaloneAPIDocRetrievalService, - s -> { - AbstractApiDocService abstractApiDocService = mock(AbstractApiDocService.class); - doReturn(s).when(abstractApiDocService).transformApiDoc(any(), any()); - return abstractApiDocService; - }, - ServiceAddress.builder().scheme("https").hostname("localhost:10014").build(), - mock(ExampleService.class) - ); - - testData = setTestData("standalone/services"); - cachedApiDocService.resetCache(); - } - - @Test - void thenContainerCacheIsPopulated() { - assertNull(cachedServicesService.getAllCachedServices()); - - standaloneLoaderService.initializeCache(); - - assertEquals(3, cachedServicesService.getAllCachedServices().size()); - assertEquals("SERVICE1-INSTANCE1", cachedServicesService.getAllCachedServices().getRegisteredApplications("service1-instance1").getName()); - assertEquals("SERVICE1-INSTANCE2", cachedServicesService.getAllCachedServices().getRegisteredApplications("service1-instance2").getName()); - assertEquals("SERVICE2", cachedServicesService.getAllCachedServices().getRegisteredApplications("service2").getName()); - } - - @Test - void thenApiDocCacheIsPopulated() throws IOException { - String expectedService1Instance1apiDoc = readTestData("/apiDocs/service1-instance1_zowe v1.0.0_default.json"); - String expectedService1Instance2apiDoc = readTestData("/apiDocs/service1-instance2_zowe v1.0.0_default.json"); - String expectedService2apiDoc1 = readTestData("/apiDocs/service2_zowe v1.0.0_default.json"); - String expectedService2apiDoc2 = readTestData("/apiDocs/service2_zowe v2.0.0.json"); - - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getDefaultApiDocForService("service1-instance1")); - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getDefaultApiDocForService("service1-instance2")); - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getDefaultApiDocForService("service2")); - - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getApiDocForService("service1-instance1", "zowe v1.0.0")); - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getApiDocForService("service1-instance2", "zowe v1.0.0")); - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getApiDocForService("service2", "zowe v1.0.0")); - assertThrows(ApiDocNotFoundException.class, () -> cachedApiDocService.getApiDocForService("service2", "zowe v2.0.0")); - - standaloneLoaderService.initializeCache(); - - assertEquals(expectedService1Instance1apiDoc, cachedApiDocService.getDefaultApiDocForService("service1-instance1")); - assertEquals(expectedService1Instance2apiDoc, cachedApiDocService.getDefaultApiDocForService("service1-instance2")); - assertEquals(expectedService2apiDoc1, cachedApiDocService.getDefaultApiDocForService("service2")); - - assertEquals(expectedService1Instance1apiDoc, cachedApiDocService.getApiDocForService("service1-instance1", "zowe v1.0.0")); - assertEquals(expectedService1Instance2apiDoc, cachedApiDocService.getApiDocForService("service1-instance2", "zowe v1.0.0")); - assertEquals(expectedService2apiDoc1, cachedApiDocService.getApiDocForService("service2", "zowe v1.0.0")); - assertEquals(expectedService2apiDoc2, cachedApiDocService.getApiDocForService("service2", "zowe v2.0.0")); - } - - @Test - void thenVersionCacheIsPopulated() { - assertThrows(ApiVersionNotFoundException.class, () -> cachedApiDocService.getDefaultApiVersionForService("service1-instance1")); - assertThrows(ApiVersionNotFoundException.class, () -> cachedApiDocService.getDefaultApiVersionForService("service1-instance2")); - assertThrows(ApiVersionNotFoundException.class, () -> cachedApiDocService.getDefaultApiVersionForService("service2")); - - assertThrows(ApiVersionNotFoundException.class, () -> cachedApiDocService.getApiVersionsForService("service1-instance1")); - assertThrows(ApiVersionNotFoundException.class, () -> cachedApiDocService.getApiVersionsForService("service1-instance2")); - assertThrows(ApiVersionNotFoundException.class, () -> cachedApiDocService.getApiVersionsForService("service2")); - - standaloneLoaderService.initializeCache(); - - assertEquals("zowe v1.0.0", cachedApiDocService.getDefaultApiVersionForService("service1-instance1")); - assertEquals("zowe v1.0.0", cachedApiDocService.getDefaultApiVersionForService("service1-instance2")); - assertEquals("zowe v1.0.0", cachedApiDocService.getDefaultApiVersionForService("service2")); - - assertEquals(Collections.singletonList("zowe v1.0.0"), cachedApiDocService.getApiVersionsForService("service1-instance1")); - assertEquals(Collections.singletonList("zowe v1.0.0"), cachedApiDocService.getApiVersionsForService("service1-instance2")); - assertEquals(Arrays.asList("zowe v1.0.0", "zowe v2.0.0"), cachedApiDocService.getApiVersionsForService("service2")); - } - - private String readTestData(String fileName) throws IOException { - return IOUtils.toString(Files.newInputStream(new File(testData + fileName).toPath()), StandardCharsets.UTF_8); - } - - } - - @Nested - class thenNoException { - - @BeforeEach - void init() { - standaloneLoaderService = - new StandaloneLoaderService(new ObjectMapper(), null, null, null, null, null, null); - } - - @Test - void givenDefinitionDirectoryNotExist_whenInitializeCache() { - ReflectionTestUtils.setField(standaloneLoaderService, "servicesDirectory", "not-exists"); - - assertDoesNotThrow(standaloneLoaderService::initializeCache); - } - - @Test - void givenApiDocInvalidName_whenInitializeCache() throws IOException { - setTestData("standalone/invalid-apiDocName"); - - assertDoesNotThrow(standaloneLoaderService::initializeCache); - } - - @Test - void givenInvalidAppJson_whenInitializeCache() throws IOException { - setTestData("standalone/invalid-app"); - - assertDoesNotThrow(standaloneLoaderService::initializeCache); - } - - } - - private String setTestData(String data) throws IOException { - String testData = new ClassPathResource(data).getFile().getAbsolutePath(); - ReflectionTestUtils.setField(standaloneLoaderService, "servicesDirectory", testData); - - return testData; - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshControllerTest.java deleted file mode 100644 index 9350f833e2..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIRefreshControllerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.staticapi; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.security.config.annotation.web.WebSecurityConfigurer; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.web.client.RestClientException; -import org.zowe.apiml.apicatalog.services.status.model.ServiceNotFoundException; - -import static org.hamcrest.Matchers.hasSize; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(controllers = {StaticAPIRefreshController.class}, - excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebSecurityConfigurer.class) }, - excludeAutoConfiguration = { SecurityAutoConfiguration.class} -) -@ContextConfiguration(classes = StaticApiContextConfiguration.class) -class StaticAPIRefreshControllerTest { - - private static final String API_REFRESH_ENDPOINT = "/static-api/refresh"; - - @Autowired - private MockMvc mockMvc; - - @Autowired - private StaticAPIService staticAPIService; - - @BeforeEach - void setUp() { - reset(staticAPIService); - } - - @Test - void givenServiceNotFoundException_whenCallRefreshAPI_thenResponseShouldBe503WithSpecificMessage() throws Exception { - when(staticAPIService.refresh()).thenThrow( - new ServiceNotFoundException("Exception") - ); - - mockMvc.perform(post(API_REFRESH_ENDPOINT)) - .andExpect(jsonPath("$.messages", hasSize(1))) - .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) - .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAC706E")) - .andExpect(jsonPath("$.messages[0].messageContent").value("Service not located, discovery")) - .andExpect(jsonPath("$.messages[0].messageKey").value("org.zowe.apiml.apicatalog.serviceNotFound")) - .andExpect(status().isServiceUnavailable()); - } - - @Test - void givenRestClientException_whenCallRefreshAPI_thenResponseShouldBe500WithSpecificMessage() throws Exception { - when(staticAPIService.refresh()).thenThrow( - new RestClientException("Exception") - ); - - mockMvc.perform(post(API_REFRESH_ENDPOINT)) - .andExpect(jsonPath("$.messages", hasSize(1))) - .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) - .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAC707E")) - .andExpect(jsonPath("$.messages[0].messageContent").value("Static API refresh failed, caused by exception: org.springframework.web.client.RestClientException: Exception")) - .andExpect(jsonPath("$.messages[0].messageKey").value("org.zowe.apiml.apicatalog.StaticApiRefreshFailed")) - .andExpect(status().isInternalServerError()); - } - - @Test - void givenSuccessStaticResponse_whenCallRefreshAPI_thenResponseCodeShouldBe200() throws Exception { - when(staticAPIService.refresh()).thenReturn( - new StaticAPIResponse(200, "This is body") - ); - - mockMvc.perform(post(API_REFRESH_ENDPOINT)) - .andExpect(status().isOk()); - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java index aad56b809d..b91f5138aa 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java @@ -24,7 +24,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; import org.zowe.apiml.util.HttpClientMockHelper; import java.io.ByteArrayInputStream; diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticApiContextConfiguration.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticApiContextConfiguration.java deleted file mode 100644 index 128e43bfc9..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticApiContextConfiguration.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.staticapi; - -import org.springframework.context.annotation.Bean; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; - -import static org.mockito.Mockito.mock; - -public class StaticApiContextConfiguration { - - @Bean - public StaticAPIService staticAPIService() { - return mock(StaticAPIService.class); - } - - @Bean - public MessageService messageService() { - return new YamlMessageService("/apicatalog-log-messages.yml"); - } - - @Bean - public StaticAPIRefreshControllerExceptionHandler staticAPIRefreshControllerExceptionHandler(MessageService messageService) { - return new StaticAPIRefreshControllerExceptionHandler(messageService); - } - - @Bean - public StaticAPIRefreshController apiCatalogController(StaticAPIService staticAPIService) { - return new StaticAPIRefreshController(staticAPIService); - } - - @Bean - public StaticDefinitionGenerator staticDefinitionGenerator() { - return mock(StaticDefinitionGenerator.class); - } - - @Bean - public StaticDefinitionControllerExceptionHandler staticDefinitionControllerExceptionHandler(MessageService messageService) { - return new StaticDefinitionControllerExceptionHandler(messageService); - } - - @Bean - public StaticDefinitionController staticAPIRefreshController(StaticDefinitionGenerator staticDefinitionGenerator) { - return new StaticDefinitionController(staticDefinitionGenerator); - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionControllerTest.java deleted file mode 100644 index f970680f33..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticDefinitionControllerTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.staticapi; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.security.config.annotation.web.WebSecurityConfigurer; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; - -import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; - -import static org.hamcrest.Matchers.hasSize; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(SpringExtension.class) -@WebMvcTest(controllers = {StaticDefinitionController.class}, - excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebSecurityConfigurer.class)}, - excludeAutoConfiguration = {SecurityAutoConfiguration.class} -) -@ContextConfiguration(classes = StaticApiContextConfiguration.class) -class StaticDefinitionControllerTest { - - private static final String STATIC_DEF_GENERATE_ENDPOINT = "/static-api/generate"; - private static final String STATIC_DEF_OVERRIDE_ENDPOINT = "/static-api/override"; - private static final String STATIC_DEF_DELETE_ENDPOINT = "/static-api/delete"; - - @Autowired - private MockMvc mockMvc; - - @Autowired - private StaticDefinitionGenerator staticDefinitionGenerator; - - @Nested - class GivenIOException { - @Nested - class whenCallStaticGenerationAPI { - @Test - void thenResponseShouldBe500WithSpecificMessage() throws Exception { - when(staticDefinitionGenerator.generateFile("services", "test")).thenThrow( - new IOException("Exception") - ); - - mockMvc.perform(post(STATIC_DEF_GENERATE_ENDPOINT).header("Service-Id", "test").content("services")) - .andExpect(jsonPath("$.messages", hasSize(1))) - .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) - .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAC709E")) - .andExpect(jsonPath("$.messages[0].messageContent").value("Static definition generation failed, caused by exception: java.io.IOException: Exception")) - .andExpect(jsonPath("$.messages[0].messageKey").value("org.zowe.apiml.apicatalog.StaticDefinitionGenerationFailed")) - .andExpect(status().isInternalServerError()); - } - } - } - - @Nested - class GivenRequestWithNoContent { - @Nested - class whenCallStaticGenerationAPI { - @Test - void thenResponseIs400() throws Exception { - mockMvc.perform(post(STATIC_DEF_GENERATE_ENDPOINT)) - .andExpect(status().isBadRequest()); - } - } - } - - @Nested - class GivenFileAlreadyExistsException { - @Nested - class whenCallStaticGenerationAPI { - @Test - void thenResponseShouldBe409WithSpecificMessage() throws Exception { - when(staticDefinitionGenerator.generateFile("invalid", "test")).thenThrow( - new FileAlreadyExistsException("Exception") - ); - - mockMvc.perform(post(STATIC_DEF_GENERATE_ENDPOINT).header("Service-Id", "test").content("invalid")) - .andExpect(jsonPath("$.messages", hasSize(1))) - .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) - .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAC709E")) - .andExpect(jsonPath("$.messages[0].messageContent").value("Static definition generation failed, caused by exception: java.nio.file.FileAlreadyExistsException: Exception")) - .andExpect(jsonPath("$.messages[0].messageKey").value("org.zowe.apiml.apicatalog.StaticDefinitionGenerationFailed")) - .andExpect(status().isConflict()); - } - } - } - - @Nested - class GivenRequestWithValidContent { - @Nested - class whenCallStaticGenerationAPI { - @Test - void thenResponseIs201() throws Exception { - String payload = "\"services:\\n - serviceId: service\\n title: a\\n description: description\\n instanceBaseUrls:\\n - a\\n routes:\\n "; - when(staticDefinitionGenerator.generateFile(payload, "service")).thenReturn( - new StaticAPIResponse(201, "This is body") - ); - - mockMvc.perform(post(STATIC_DEF_GENERATE_ENDPOINT).content(payload).header("Service-Id", "service")) - .andExpect(status().is2xxSuccessful()); - } - } - - @Nested - class whenCallStaticOverrideAPI { - @Test - void thenResponseIs201() throws Exception { - String payload = "\"services:\\n - serviceId: service\\n title: a\\n description: description\\n instanceBaseUrls:\\n - a\\n routes:\\n "; - when(staticDefinitionGenerator.overrideFile(payload, "service")).thenReturn( - new StaticAPIResponse(201, "This is body") - ); - - mockMvc.perform(post(STATIC_DEF_OVERRIDE_ENDPOINT).content(payload).header("Service-Id", "service")) - .andExpect(status().is2xxSuccessful()); - } - } - - @Nested - class WhenCallDelete { - - @Test - void givenValidId_thenResponseIsOK() throws Exception { - when(staticDefinitionGenerator.deleteFile("test-service")).thenReturn(new StaticAPIResponse(201, "OK")); - mockMvc.perform(delete(STATIC_DEF_DELETE_ENDPOINT).header("Service-Id", "test-service")) - .andExpect(status().is2xxSuccessful()); - } - } - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocalTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocalTest.java new file mode 100644 index 0000000000..a16bf83c30 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocalTest.java @@ -0,0 +1,138 @@ +/* + * 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.apicatalog.swagger; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.netflix.appinfo.InstanceInfo; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.servers.Server; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springdoc.webflux.api.OpenApiWebfluxResource; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.config.ApiInfo; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class ApiDocRetrievalServiceLocalTest { + + private ApiDocRetrievalServiceLocal service; + + @BeforeEach + void init() { + service = new ApiDocRetrievalServiceLocal(Collections.emptyList(), null, null, null, null, null, null, null); + } + + @Test + void givenUnknownServiceId_whenGetApiDoc_thenThrowException() { + var instance = new EurekaServiceInstance(InstanceInfo.Builder.newBuilder().setAppName("unknownService").build()); + var apiInfo = ApiInfo.builder().build(); + var exception = assertThrows(ApiDocNotFoundException.class, () -> service.retrieveApiDoc(instance, apiInfo)); + + assertEquals("Cannot obtain API doc for service unknownservice", exception.getMessage()); + } + + private OpenApiWebfluxResource mockApiDocResource() { + var apiDocResource = mock(OpenApiWebfluxResource.class); + ((Map) ReflectionTestUtils.getField(service, "apiDocResource")).put("service", apiDocResource); + return apiDocResource; + } + + @Test + void givenInvalidApiDoc_whenGetApiDoc_thenThrowException() throws JsonProcessingException { + var apiDocResource = mockApiDocResource(); + doThrow(new JsonProcessingException("an error") {}).when(apiDocResource).openapiJson(any(), eq("/"), any()); + + var instance = new EurekaServiceInstance(InstanceInfo.Builder.newBuilder().setAppName("service").build()); + var apiInfo = ApiInfo.builder().build(); + + var exception = assertThrows(ApiDocNotFoundException.class, () -> service.retrieveApiDoc(instance, apiInfo)); + assertEquals("Cannot obtain API doc for service", exception.getMessage()); + assertInstanceOf(JsonProcessingException.class, exception.getCause()); + assertEquals("an error", exception.getCause().getMessage()); + } + + @Test + void givenValidApiDoc_whenGetApiDoc_thenReturnApiDoc() throws JsonProcessingException { + var apiDocResource = mockApiDocResource(); + doReturn(Mono.just("Api doc".getBytes(StandardCharsets.UTF_8))).when(apiDocResource).openapiJson(any(), eq("/"), any()); + + var instance = new EurekaServiceInstance(InstanceInfo.Builder.newBuilder().setAppName("service").build()); + var apiInfo = ApiInfo.builder().build(); + + StepVerifier.create(service.retrieveApiDoc(instance, apiInfo)) + .expectNextMatches(apiDocInfo -> { + assertEquals("Api doc", apiDocInfo.getApiDocContent()); + assertSame(apiInfo, apiDocInfo.getApiInfo()); + return true; + }) + .verifyComplete(); + } + + @Nested + class NormalizePathsCustomizer { + + @ParameterizedTest + @CsvSource({ + "/api1/a,/api1/b/c,/api1", + "/a/start*,/a/start,/a", + "/a/start/*,/a/start,/a/start", + "/a/b/c,/a/b/c,/a/b/c", + "/a/b/c,/a/b/c/,/a/b/c", + "/a/b/c/,/a/b/c/,/a/b/c", + "/a/b*/c/**,/a/b*/c**,/a" + }) + void givenTwoPatterns_whenGetCommonBasePath_thenGetBasePath(String pattern1, String pattern2, String basePath) { + assertEquals(basePath, service.getCommonBasePath(Arrays.asList(pattern1, pattern2))); + } + + @Test + void givenOpenApiAndPath_whenNormalizePaths_thenModifyOpenApi() { + OpenAPI openApi = new OpenAPI(); + + var server = new Server(); + server.setUrl("https://localhost:10014"); + openApi.setServers(Collections.singletonList(server)); + + var paths = new Paths(); + paths.addPathItem("/apicatalog/api/v1/endpoint", new PathItem()); + openApi.setPaths(paths); + + service.normalizePathsCustomizer(Collections.singletonList("/apicatalog/api/v1/")) + .customise(openApi); + + assertEquals(1, openApi.getServers().size()); + assertEquals("https://localhost:10014/apicatalog/api/v1", server.getUrl()); + + assertEquals(1, openApi.getPaths().size()); + assertNotNull(openApi.getPaths().get("/endpoint")); + } + + } + +} 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 new file mode 100644 index 0000000000..db94d967d3 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocServiceTest.java @@ -0,0 +1,580 @@ +/* + * 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.apicatalog.swagger; + +import com.netflix.appinfo.InstanceInfo; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; +import org.zowe.apiml.apicatalog.exceptions.ApiVersionNotFoundException; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; +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.gateway.GatewayClient; +import org.zowe.apiml.product.instance.ServiceAddress; +import org.zowe.apiml.util.HttpClientMockHelper; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ApiDocServiceTest { + + private static final String SERVICE_ID = "service"; + private static final String SERVICE_HOST = "service"; + private static final int SERVICE_PORT = 8080; + private static final String SERVICE_VERSION = "1.0.0"; + private static final String HIGHER_SERVICE_VERSION = "2.0.0"; + private static final String SERVICE_VERSION_V = "test.app v1.0.0"; + private static final String HIGHER_SERVICE_VERSION_V = "test.app v2.0.0"; + private static final String GATEWAY_SCHEME = "http"; + private static final String GATEWAY_HOST = "gateway:10000"; + private static final String GATEWAY_URL = "api/v1"; + private static final String API_ID = "test.app"; + private static final String SWAGGER_URL = "https://service:8080/service/api-doc"; + + private static final ServiceAddress GW_SERVICE_ADDRESS = ServiceAddress.builder().scheme(GATEWAY_SCHEME).hostname(GATEWAY_HOST).build(); + + @Nested + class ViaRestCall { + + @Mock + private DiscoveryClient discoveryClient; + + private ApiDocService apiDocService; + + @Mock + private ApimlLogger apimlLogger; + + @Mock + private CloseableHttpClient httpClient; + + @Mock + private CloseableHttpResponse response; + + private AtomicReference lastApiInfo = new AtomicReference<>(); + + @BeforeEach + void setup() { + lastApiInfo.set(null); + + HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); + var apiDocRetrievalServiceRest = new ApiDocRetrievalServiceRest(httpClient); + apiDocService = new ApiDocService( + discoveryClient, + new GatewayClient(GW_SERVICE_ADDRESS), + new TransformApiDocService(null) { + @Override + public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { + return apiDocInfo.getApiDocContent(); + } + }, + mock(ApiDocRetrievalServiceLocal.class), + apiDocRetrievalServiceRest + ) { + @Override + Mono retrieveApiDoc(ServiceInstance serviceInstance, ApiInfo apiInfo) { + lastApiInfo.set(apiInfo); + return super.retrieveApiDoc(serviceInstance, apiInfo); + } + }; + + ReflectionTestUtils.setField(apiDocRetrievalServiceRest, "apimlLogger", apimlLogger); + } + + @Nested + class WhenGetApiDoc { + + @Test + void givenValidApiInfo_thenReturnApiDoc() { + var responseBody = "api-doc body"; + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) + .assertNext(actualApiDoc -> { + assertNotNull(lastApiInfo.get()); + assertEquals(API_ID, lastApiInfo.get().getApiId()); + assertEquals(GATEWAY_URL, lastApiInfo.get().getGatewayUrl()); + assertEquals(SERVICE_VERSION, lastApiInfo.get().getVersion()); + assertEquals(SWAGGER_URL, lastApiInfo.get().getSwaggerUrl()); + + assertNotNull(actualApiDoc); + assertEquals(responseBody, actualApiDoc); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Nested + class ThenThrowException { + + @Test + void givenNoApiDocFoundForService() { + Exception exception = assertThrows(ApiDocNotFoundException.class, () -> apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)); + assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); + } + + @Test + void givenServerErrorWhenRequestingSwaggerUrl() { + String responseBody = "Server not found"; + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_INTERNAL_SERVER_ERROR, responseBody); + + Mono apiDocMono = apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V); + Exception exception = assertThrows(ApiDocNotFoundException.class, apiDocMono::block); + assertEquals("No API Documentation was retrieved due to " + SERVICE_ID + " server error: 500 " + responseBody, exception.getMessage()); + } + + } + + @Test + void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() { + //language=JSON + String generatedResponseBody = """ + { + "swagger": "2.0", + "info": { + "title": "Test service" + , "description": "Test service description" + , "version": "1.0.0" + }, + "host": "gateway:10000", + "basePath": "/service/api/v1", + "schemes": ["http"], + "tags": [ + { + "name": "apimlHidden" + } + ], + "paths": { + "/apimlHidden": { + "get": { + "tags": ["apimlHidden"], + "responses": { + "200": { + "description": "OK" + } + } + } + } + } + } + """.replaceAll("\\s+", ""); + String responseBody = "api-doc body"; + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutSwaggerUrl(), true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) + .assertNext(actualApiDoc -> { + assertNotNull(lastApiInfo.get()); + assertEquals(API_ID, lastApiInfo.get().getApiId()); + assertEquals(GATEWAY_URL, lastApiInfo.get().getGatewayUrl()); + assertEquals(SERVICE_VERSION, lastApiInfo.get().getVersion()); + assertNull(lastApiInfo.get().getSwaggerUrl()); + + assertNotNull(actualApiDoc); + assertEquals(generatedResponseBody, actualApiDoc.replaceAll("\\s+", "")); + }) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() { + var responseBody = "api-doc body"; + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) + .assertNext(actualApiDoc -> { + assertNotNull(actualApiDoc); + assertEquals(responseBody, actualApiDoc); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void shouldCreateApiDocUrlFromRoutingAndUseHttp() { + var responseBody = "api-doc body"; + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), false))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) + .assertNext(actualApiDoc -> { + assertNotNull(actualApiDoc); + assertEquals(responseBody, actualApiDoc); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() { + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); + + var exception = new IOException("Unable to reach the host"); + HttpClientMockHelper.whenExecuteThenThrow(httpClient, exception); + Mono apiDocMono = apiDocService.retrieveDefaultApiDoc(SERVICE_ID); + assertThrows(ApiDocNotFoundException.class, apiDocMono::block); + + assertNotNull(lastApiInfo.get()); + assertEquals(API_ID, lastApiInfo.get().getApiId()); + assertEquals(GATEWAY_URL, lastApiInfo.get().getGatewayUrl()); + assertEquals(SERVICE_VERSION, lastApiInfo.get().getVersion()); + assertEquals(SWAGGER_URL, lastApiInfo.get().getSwaggerUrl()); + + verify(apimlLogger, times(1)).log("org.zowe.apiml.apicatalog.apiDocHostCommunication", SERVICE_ID, exception.getMessage()); + } + } + + @Nested + class WhenGetDefaultApiDoc { + + @Test + void givenDefaultApiDoc_thenReturnIt() { + var responseBody = "api-doc body"; + var metadata = getMetadataWithMultipleApiInfo(); + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(metadata, true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) + .assertNext(actualApiDoc -> { + assertNotNull(lastApiInfo.get()); + assertEquals(API_ID, lastApiInfo.get().getApiId()); + assertEquals(GATEWAY_URL, lastApiInfo.get().getGatewayUrl()); + assertEquals(SERVICE_VERSION, lastApiInfo.get().getVersion()); + assertEquals(SWAGGER_URL, lastApiInfo.get().getSwaggerUrl()); + + assertNotNull(actualApiDoc); + assertEquals(responseBody, actualApiDoc); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void givenNoDefaultApiDoc_thenReturnHighestVersion() { + var responseBody = "api-doc body"; + var metadata = getMetadataWithMultipleApiInfo(); + 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, true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) + .assertNext(actualApiDoc -> { + assertNotNull(lastApiInfo.get()); + assertEquals(API_ID, lastApiInfo.get().getApiId()); + assertEquals(GATEWAY_URL, lastApiInfo.get().getGatewayUrl()); + assertEquals(HIGHER_SERVICE_VERSION, lastApiInfo.get().getVersion()); + assertEquals(SWAGGER_URL, lastApiInfo.get().getSwaggerUrl()); + + assertNotNull(actualApiDoc); + assertEquals(responseBody, actualApiDoc); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() { + var responseBody = "api-doc body"; + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithMultipleApiInfoWithDifferentVersionFormat(), true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) + .assertNext(actualApiDoc -> { + assertNotNull(lastApiInfo.get()); + assertEquals(API_ID, lastApiInfo.get().getApiId()); + assertEquals(GATEWAY_URL, lastApiInfo.get().getGatewayUrl()); + assertEquals(HIGHER_SERVICE_VERSION_V, lastApiInfo.get().getVersion()); + assertEquals(SWAGGER_URL, lastApiInfo.get().getSwaggerUrl()); + + assertNotNull(actualApiDoc); + assertEquals(responseBody, actualApiDoc); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + + @Test + void givenNoApiDocs_thenReturnNull() { + var responseBody = "api-doc body"; + + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), true))); + + HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + + var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) + .assertNext(actualApiDoc -> { + assertNotNull(actualApiDoc); + assertEquals(responseBody, actualApiDoc); + }) + .verifyComplete(); + + assertEquals(0L, elapsed.toSeconds()); + } + } + + @Nested + class WhenGetApiVersions { + @Test + void givenApiVersions_thenReturnThem() { + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), false))); + + List actualVersions = apiDocService.retrieveApiVersions(SERVICE_ID); + assertEquals(Collections.singletonList(SERVICE_VERSION_V), actualVersions); + } + + @Test + void givenNoApiVersions_thenThrowException() { + when(discoveryClient.getInstances(SERVICE_ID)).thenReturn(Collections.emptyList()); + + Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> + apiDocService.retrieveApiVersions(SERVICE_ID) + ); + assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); + } + } + + @Nested + class WhenGetDefaultApiVersion { + @Test + void givenDefaultApiVersion_thenReturnIt() { + when(discoveryClient.getInstances(SERVICE_ID)) + .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithMultipleApiInfo(), false))); + + String defaultVersion = apiDocService.retrieveDefaultApiVersion(SERVICE_ID); + assertEquals(SERVICE_VERSION_V, defaultVersion); + } + + @Test + void givenNoDefaultApiVersion_thenReturnHighestVersion() { + Map metadata = getMetadataWithMultipleApiInfo(); + 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))); + + String defaultVersion = apiDocService.retrieveDefaultApiVersion(SERVICE_ID); + assertEquals(HIGHER_SERVICE_VERSION_V, defaultVersion); + } + + @Test + void givenNoApiInfo_thenThrowException() { + when(discoveryClient.getInstances(SERVICE_ID)).thenReturn(Collections.emptyList()); + + Exception exception = assertThrows(ApiVersionNotFoundException.class, () -> + apiDocService.retrieveDefaultApiVersion(SERVICE_ID) + ); + assertEquals("Could not load instance information for service " + SERVICE_ID + ".", exception.getMessage()); + } + } + + private ServiceInstance getStandardInstance(Map metadata, Boolean isPortSecure) { + InstanceInfo instance = InstanceInfo.Builder.newBuilder() + .setAppName(SERVICE_ID) + .setHostName(SERVICE_HOST) + .setPort(SERVICE_PORT) + .setSecurePort(SERVICE_PORT) + .enablePort(InstanceInfo.PortType.SECURE, isPortSecure) + .setStatus(InstanceInfo.InstanceStatus.UP) + .setMetadata(metadata) + .build(); + return new EurekaServiceInstance(instance); + } + + private Map getStandardMetadata() { + Map metadata = new HashMap<>(); + metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); + metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); + metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION); + metadata.put(API_INFO + ".1." + API_INFO_SWAGGER_URL, SWAGGER_URL); + metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); + metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); + metadata.put(SERVICE_TITLE, "Test service"); + metadata.put(SERVICE_DESCRIPTION, "Test service description"); + + return metadata; + } + + private Map getMetadataWithoutSwaggerUrl() { + Map metadata = new HashMap<>(); + metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); + metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); + metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION); + metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); + metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); + metadata.put(SERVICE_TITLE, "Test service"); + metadata.put(SERVICE_DESCRIPTION, "Test service description"); + + return metadata; + } + + private Map getMetadataWithMultipleApiInfo() { + Map metadata = new HashMap<>(); + metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); + metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); + metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION); + metadata.put(API_INFO + ".1." + API_INFO_SWAGGER_URL, SWAGGER_URL); + metadata.put(API_INFO + ".1." + API_INFO_IS_DEFAULT, "true"); + + metadata.put(API_INFO + ".2." + API_INFO_API_ID, API_ID); + metadata.put(API_INFO + ".2." + API_INFO_GATEWAY_URL, GATEWAY_URL); + metadata.put(API_INFO + ".2." + API_INFO_VERSION, HIGHER_SERVICE_VERSION); + metadata.put(API_INFO + ".2." + API_INFO_SWAGGER_URL, SWAGGER_URL); + + metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); + metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); + metadata.put(SERVICE_TITLE, "Test service"); + metadata.put(SERVICE_DESCRIPTION, "Test service description"); + + return metadata; + } + + private Map getMetadataWithMultipleApiInfoWithDifferentVersionFormat() { + Map metadata = new HashMap<>(); + metadata.put(API_INFO + ".1." + API_INFO_API_ID, API_ID); + metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, GATEWAY_URL); + metadata.put(API_INFO + ".1." + API_INFO_VERSION, SERVICE_VERSION_V); + metadata.put(API_INFO + ".1." + API_INFO_SWAGGER_URL, SWAGGER_URL); + + metadata.put(API_INFO + ".2." + API_INFO_API_ID, API_ID); + metadata.put(API_INFO + ".2." + API_INFO_GATEWAY_URL, GATEWAY_URL); + metadata.put(API_INFO + ".2." + API_INFO_VERSION, HIGHER_SERVICE_VERSION_V); + metadata.put(API_INFO + ".2." + API_INFO_SWAGGER_URL, SWAGGER_URL); + + metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); + metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); + metadata.put(SERVICE_TITLE, "Test service"); + metadata.put(SERVICE_DESCRIPTION, "Test service description"); + + return metadata; + } + + private Map getMetadataWithoutApiInfo() { + Map metadata = new HashMap<>(); + metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api"); + metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); + metadata.put(ROUTES + ".apidoc." + ROUTES_GATEWAY_URL, "api/v1/api-doc"); + metadata.put(ROUTES + ".apidoc." + ROUTES_SERVICE_URL, SERVICE_ID + "/api-doc"); + metadata.put(SERVICE_TITLE, "Test service"); + metadata.put(SERVICE_DESCRIPTION, "Test service description"); + + return metadata; + } + + } + + @Nested + @SpringBootTest + class ViaApiCall { + + @MockitoSpyBean + private ApiDocService apiDocService; + + @MockitoSpyBean + private ApiDocRetrievalServiceLocal apiDocRetrievalServiceLocal; + + @Autowired + private GatewayClient gatewayClient; + + @BeforeEach + void onboardCatalog() { + InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() + .setAppName("apicatalog") + .setMetadata(Map.of( + "apiml.apiInfo.0.apiId", "zowe.apiml.apicatalog", + "apiml.apiInfo.0.gatewayUrl", "api/v1", + "apiml.apiInfo.0.swaggerUrl", "https://localhost:10010/v3/api-doc", + "apiml.apiInfo.0.version", "1.0.0" + )) + .build(); + doReturn(new EurekaServiceInstance(instanceInfo)).when(apiDocService).getInstanceInfo("apicatalog"); + + gatewayClient.setGatewayConfigProperties(GW_SERVICE_ADDRESS); + } + + @Test + void givenApiCatalogId_whenRetrieveApiDoc_thenCallLocally() { + StepVerifier.create(apiDocService.retrieveApiDoc(CoreService.API_CATALOG.getServiceId(), "zowe.apiml.apicatalog v1.0.0")) + .expectNextMatches(apiDoc -> apiDoc.contains("/containers/{id}")) + .verifyComplete(); + + verify(apiDocRetrievalServiceLocal).retrieveApiDoc(any(), any()); + } + + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocTransformationExceptionTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocTransformationExceptionTest.java index 36b7b55beb..b03a8db074 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocTransformationExceptionTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ApiDocTransformationExceptionTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.zowe.apiml.apicatalog.exceptions.ApiDocTransformationException; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ContainerServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ContainerServiceTest.java new file mode 100644 index 0000000000..4c2b7390e8 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/ContainerServiceTest.java @@ -0,0 +1,276 @@ +/* + * 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.apicatalog.swagger; + +import com.netflix.appinfo.InstanceInfo; +import org.apache.commons.lang3.tuple.Pair; +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.junit.jupiter.MockitoExtension; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.apicatalog.model.APIContainer; +import org.zowe.apiml.apicatalog.model.APIService; +import org.zowe.apiml.apicatalog.model.CustomStyleConfig; +import org.zowe.apiml.apicatalog.util.ServicesBuilder; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.gateway.GatewayClient; +import org.zowe.apiml.product.instance.ServiceAddress; +import org.zowe.apiml.product.routing.transform.TransformService; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; + +@ExtendWith(MockitoExtension.class) +class ContainerServiceTest { + + @Nested + class WhenCalculatingContainerTotals { + + private static final String SERVICE_ID = "service_test_id"; + + private DiscoveryClient discoveryClient = mock(DiscoveryClient.class); + private TransformService transformService = new TransformService(new GatewayClient(ServiceAddress.builder().scheme("https").hostname("localhost").build())); + private CustomStyleConfig customStyleConfig = new CustomStyleConfig(); + + private ServiceInstance serviceInstance1; + private ServiceInstance serviceInstance2; + private ContainerService containerService; + + @BeforeEach + void prepareApplications() { + serviceInstance1 = ServicesBuilder.createInstance("service1", "demoapp"); + serviceInstance2 = ServicesBuilder.createInstance("service2", "demoapp"); + + when(discoveryClient.getInstances("service1")).thenReturn(Collections.singletonList(serviceInstance1)); + when(discoveryClient.getInstances("service2")).thenReturn(Collections.singletonList(serviceInstance2)); + when(discoveryClient.getServices()).thenReturn(Arrays.asList("service1", "service2")); + containerService = new ContainerService( + discoveryClient, + transformService, + customStyleConfig + ); + } + + @Nested + class AndStatusIsInvolved { + + void assertThatContainerHasValidState(APIContainer container, String state, int activeServices) { + assertNotNull(container); + + assertEquals(state, container.getStatus()); + assertEquals(2, container.getTotalServices().intValue()); + assertEquals(activeServices, container.getActiveServices().intValue()); + } + + @Test + void givenNoServiceId_getGetContainerById_thenReturnNull() { + assertNull(containerService.getContainerById(null)); + } + + @Nested + class GivenAllServicesAreUp { + + @Test + void containerStatusIsUp() { + APIContainer container = containerService.getContainerById("demoapp"); + assertNotNull(container); + + assertThatContainerHasValidState(container, "UP", 2); + } + + } + + @Nested + class GivenAllServicesAreDown { + + @Test + void containerStatusIsDown() { + ((EurekaServiceInstance) serviceInstance1).getInstanceInfo().setStatus(InstanceInfo.InstanceStatus.DOWN); + ((EurekaServiceInstance) serviceInstance2).getInstanceInfo().setStatus(InstanceInfo.InstanceStatus.DOWN); + + APIContainer container = containerService.getContainerById("demoapp"); + assertNotNull(container); + + assertThatContainerHasValidState(container, "DOWN", 0); + } + + } + + @Nested + class GivenSomeServicesAreDown { + @Test + void containerStatusIsWarning() { + ((EurekaServiceInstance) serviceInstance2).getInstanceInfo().setStatus(InstanceInfo.InstanceStatus.DOWN); + + APIContainer container = containerService.getContainerById("demoapp"); + assertNotNull(container); + + assertThatContainerHasValidState(container, "WARNING", 1); + } + } + + } + + @Nested + class GivenMultipleApiIds { + + @Test + void groupThem() { + var serviceInstance = ServicesBuilder.createInstance(SERVICE_ID, SERVICE_ID, + Pair.of("apiml.apiInfo.api-v1.apiId", "api1"), + Pair.of("apiml.apiInfo.api-v1.version", "1.0.0"), + Pair.of("apiml.apiInfo.api-v2.apiId", "api2"), + Pair.of("apiml.apiInfo.api-v2.version", "2"), + Pair.of("apiml.apiInfo.api-v3.apiId", "api3")); + doReturn(Collections.singletonList(serviceInstance)).when(discoveryClient).getInstances(SERVICE_ID); + doReturn(Collections.singletonList(SERVICE_ID)).when(discoveryClient).getServices(); + APIContainer apiContainer = containerService.getContainerById(SERVICE_ID); + + APIService apiService = apiContainer.getServices().iterator().next(); + assertNotNull(apiService.getApis()); + assertEquals(3, apiService.getApis().size()); + assertNotNull(apiService.getApis().get("api1 v1.0.0")); + assertNotNull(apiService.getApis().get("api2 v2")); + assertNotNull(apiService.getApis().get("default")); + } + + } + + @Nested + class AndSsoInvolved { + + @Nested + class GivenSsoAndNonSsoInstances { + + @Test + void returnNonSso() { + doReturn(Arrays.asList( + ServicesBuilder.createInstance(SERVICE_ID, SERVICE_ID, Pair.of(AUTHENTICATION_SCHEME, "bypass")), + ServicesBuilder.createInstance(SERVICE_ID, SERVICE_ID, Pair.of(AUTHENTICATION_SCHEME, "zoweJwt")) + )).when(discoveryClient).getInstances(SERVICE_ID); + doReturn(Collections.singletonList(SERVICE_ID)).when(discoveryClient).getServices(); + + APIContainer apiContainer = containerService.getContainerById(SERVICE_ID); + + assertFalse(apiContainer.isSso()); + for (APIService apiService : apiContainer.getServices()) { + assertFalse(apiService.isSsoAllInstances()); + } + } + + } + + @Nested + class GivenAllInstancesAreSso { + + @Test + void returnSso() { + ServiceInstance serviceInstance = ServicesBuilder.createInstance(SERVICE_ID, SERVICE_ID, Pair.of(AUTHENTICATION_SCHEME, "zoweJwt")); + doReturn(Collections.singletonList(serviceInstance)).when(discoveryClient).getInstances(SERVICE_ID); + doReturn(Collections.singletonList(SERVICE_ID)).when(discoveryClient).getServices(); + APIContainer apiContainer = containerService.getContainerById(SERVICE_ID); + + assertTrue(apiContainer.isSso()); + for (APIService apiService : apiContainer.getServices()) { + assertTrue(apiService.isSso()); + assertTrue(apiService.isSsoAllInstances()); + } + } + + @Test + void whenGetService_thenUpdateIt() { + containerService = spy(containerService); + ServiceInstance serviceInstance = ServicesBuilder.createInstance(SERVICE_ID, SERVICE_ID, Pair.of(AUTHENTICATION_SCHEME, "zoweJwt")); + doReturn(Collections.singletonList(serviceInstance)).when(discoveryClient).getInstances(SERVICE_ID); + var apiService = containerService.getService(SERVICE_ID); + assertNotNull(apiService); + assertTrue(apiService.isSso()); + verify(containerService, times(1)).update(apiService); + } + + } + + } + + @Nested + class GivenHideServiceInfo { + + @Test + void thenSetToApiService() { + ServiceInstance serviceInstance = ServicesBuilder.createInstance(SERVICE_ID, SERVICE_ID, Pair.of(AUTHENTICATION_SCHEME, "zoweJwt")); + doReturn(Collections.singletonList(SERVICE_ID)).when(discoveryClient).getServices(); + doReturn(Collections.singletonList(serviceInstance)).when(discoveryClient).getInstances(SERVICE_ID); + ReflectionTestUtils.setField(containerService, "hideServiceInfo", true); + APIContainer apiContainer = containerService.getContainerById(SERVICE_ID); + assertTrue(apiContainer.isHideServiceInfo()); + } + + } + + } + + @Nested + class MultiTenancy { + + private ContainerService containerService; + + @BeforeEach + void init() { + containerService = new ContainerService( + mock(DiscoveryClient.class), + new TransformService(new GatewayClient(ServiceAddress.builder().scheme("https").hostname("localhost").build())), + new CustomStyleConfig() + ); + } + + private APIService createDto(RegistrationType registrationType) { + Map metadata = new HashMap<>(); + metadata.put(APIML_ID, "apimlId"); + metadata.put(SERVICE_TITLE, "title"); + metadata.put(REGISTRATION_TYPE, registrationType.getValue()); + var service = new EurekaServiceInstance(InstanceInfo.Builder.newBuilder() + .setAppName(CoreService.GATEWAY.getServiceId()) + .setMetadata(metadata) + .build() + ); + return containerService.createAPIServiceFromInstance(service); + } + + @Test + void givenPrimaryInstance_whenCreateDto_thenDoNotUpdateTitle() { + var dto = createDto(RegistrationType.ADDITIONAL); + assertEquals("title (apimlId)", dto.getTitle()); + assertEquals("apimlid", dto.getServiceId()); + assertEquals("/apimlid", dto.getBasePath()); + } + + @Test + void givenPrimaryInstance_whenCreateDto_thenAddApimlIdIntoTitle() { + var dto = createDto(RegistrationType.PRIMARY); + assertEquals("title", dto.getTitle()); + assertEquals("gateway", dto.getServiceId()); + assertEquals("/", dto.getBasePath()); + } + + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SecuritySchemeSerializerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SecuritySchemeSerializerTest.java index 825b7c54ab..8e45dd94b5 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SecuritySchemeSerializerTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SecuritySchemeSerializerTest.java @@ -23,7 +23,7 @@ import java.io.Writer; import java.util.Collections; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class SecuritySchemeSerializerTest { diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGeneratorTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGeneratorTest.java index 5c2b1e300d..468d45b14b 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGeneratorTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/SubstituteSwaggerGeneratorTest.java @@ -10,18 +10,19 @@ package org.zowe.apiml.apicatalog.swagger; -import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; -import org.zowe.apiml.config.ApiInfo; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.InstanceInfo.PortType; import org.junit.jupiter.api.Test; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; class SubstituteSwaggerGeneratorTest { private static final String GATEWAY_SCHEME = "https"; @@ -43,7 +44,7 @@ void testParseApiInfo() { InstanceInfo service = InstanceInfo.Builder.newBuilder().setAppName(APP_NAME).setHostName(HOST_NAME) .setSecurePort(8080).enablePort(PortType.SECURE, true).setMetadata(metadata).build(); - String result = swaggerGenerator.generateSubstituteSwaggerForService(service, + String result = swaggerGenerator.generateSubstituteSwaggerForService(new EurekaServiceInstance(service), info.get(0), GATEWAY_SCHEME, GATEWAY_HOST); assertTrue(result.contains(DOC_URL)); } @@ -59,7 +60,7 @@ void testParseApiInfoWithGatewayUrlSlashes() { InstanceInfo service = InstanceInfo.Builder.newBuilder().setAppName(APP_NAME).setHostName(HOST_NAME) .setSecurePort(8080).enablePort(PortType.SECURE, true).setMetadata(metadata).build(); - String result = swaggerGenerator.generateSubstituteSwaggerForService(service, + String result = swaggerGenerator.generateSubstituteSwaggerForService(new EurekaServiceInstance(service), info.get(0), GATEWAY_SCHEME, GATEWAY_HOST); assertTrue(result.contains(DOC_URL)); } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocServiceTest.java index 8bb21b1775..02eb5844be 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/TransformApiDocServiceTest.java @@ -10,15 +10,15 @@ package org.zowe.apiml.apicatalog.swagger; +import jakarta.validation.UnexpectedTypeException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.apicatalog.swagger.api.AbstractApiDocService; import org.zowe.apiml.apicatalog.swagger.api.ApiDocV2Service; import org.zowe.apiml.apicatalog.swagger.api.ApiDocV3Service; -import jakarta.validation.UnexpectedTypeException; import java.util.function.Function; import static org.mockito.Mockito.*; @@ -50,7 +50,7 @@ void setup() { @Test void testTransformApiDoc_whenThereIsNotApiDocMatch() { - ApiDocInfo apiDocInfo = new ApiDocInfo(null, "DOC4", null); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiDocContent("DOC4").build(); Exception exception = Assertions.assertThrows(UnexpectedTypeException.class, () -> { transformApiDocService.transformApiDoc(SERVICE_ID, apiDocInfo); }); @@ -59,7 +59,7 @@ void testTransformApiDoc_whenThereIsNotApiDocMatch() { @Test void testTransformApiDoc_whenSwaggerDocIsPresent() { - ApiDocInfo apiDocInfo = new ApiDocInfo(null, "DOC2", null); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiDocContent("DOC2").build(); transformApiDocService.transformApiDoc(SERVICE_ID, apiDocInfo); @@ -74,7 +74,7 @@ void testTransformApiDoc_whenSwaggerDocIsPresent() { @Test void testTransformApiDoc_whenOpenDocIsPresent() { - ApiDocInfo apiDocInfo = new ApiDocInfo(null, "DOC3", null); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiDocContent("DOC3").build(); transformApiDocService.transformApiDoc(SERVICE_ID, apiDocInfo); diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocServiceTest.java index d037a74c7e..9cc5043705 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/AbstractApiDocServiceTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.swagger.api.dummy.DummyApiDocService; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.routing.RoutedService; @@ -49,7 +49,7 @@ class AbstractApiDocServiceTest { @BeforeEach void setUp() { GatewayClient gatewayClient = new GatewayClient(getProperties()); - abstractApiDocService = new DummyApiDocService(gatewayClient); + abstractApiDocService = new DummyApiDocService(ApplicationInfo.builder().build(), gatewayClient); } @Test @@ -89,7 +89,7 @@ void givenRoutedEndpoint_whenGetEndpointPairs_thenReturnRoutedPair() { @Test void givenNullApiInfo_whenGetRoutedServiceByApiInfo_thenReturnNull() { - ApiDocInfo apiDocInfo = new ApiDocInfo(null, null, null); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().build(); assertNull(abstractApiDocService.getRoutedServiceByApiInfo(apiDocInfo, "/")); } @@ -109,7 +109,7 @@ void givenSwaggerDoc_whenPreparePaths_thenSetpathsInSwaggerDoc() { routedServices.addRoutedService(routedService2); ApiInfo apiInfo = new ApiInfo("org.zowe.apicatalog", "api/v1", null, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); abstractApiDocService.preparePath(openAPI.getPaths(), apiDocPath, apiDocInfo, "/api/v1/api-doc", "/", "apicatalog"); diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2ServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2ServiceTest.java index 13fe22759c..52e8556063 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2ServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV2ServiceTest.java @@ -14,6 +14,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.swagger.models.*; +import io.swagger.parser.SwaggerParser; +import io.swagger.util.Json; +import jakarta.validation.UnexpectedTypeException; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.hamcrest.collection.IsMapContaining; import org.junit.jupiter.api.BeforeEach; @@ -25,15 +29,15 @@ import org.mockito.quality.Strictness; import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.RoutedServices; -import jakarta.validation.UnexpectedTypeException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -48,6 +52,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) +@Slf4j class ApiDocV2ServiceTest { private static final String SERVICE_ID = "serviceId"; @@ -69,7 +74,7 @@ class ApiDocV2ServiceTest { void setUp() { gatewayConfigProperties = getProperties(); gatewayClient = new GatewayClient(gatewayConfigProperties); - apiDocV2Service = new ApiDocV2Service(gatewayClient); + apiDocV2Service = new ApiDocV2Service(ApplicationInfo.builder().build(), gatewayClient); ReflectionTestUtils.setField(apiDocV2Service, "scheme", "https"); } @@ -77,7 +82,7 @@ void setUp() { void givenSwaggerJsonNotAsExpectedFormat_whenConvertToSwagger_thenThrowIOException() { String apiDocContent = "Failed content"; - ApiDocInfo apiDocInfo = new ApiDocInfo(null, apiDocContent, null); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiDocContent(apiDocContent).build(); Exception exception = assertThrows(UnexpectedTypeException.class, () -> apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo)); assertEquals("The Swagger definition for service 'serviceId' was retrieved but was not a valid JSON document.", exception.getMessage()); @@ -85,8 +90,10 @@ void givenSwaggerJsonNotAsExpectedFormat_whenConvertToSwagger_thenThrowIOExcepti @Nested class WhenApiDocTransform { + @Nested class ThenCheckUpdatedValues { + @Test void givenSwaggerValidJson() { Swagger dummySwaggerObject = getDummySwaggerObject("/apicatalog", false); @@ -102,7 +109,7 @@ void givenSwaggerValidJson() { routedServices.addRoutedService(routedService3); ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo); Swagger actualSwagger = convertJsonToSwagger(actualContent); @@ -153,7 +160,7 @@ void givenSwaggerValidYaml() { routedServices.addRoutedService(routedService3); ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo); Swagger actualSwagger = convertYamlToSwagger(actualContent); @@ -201,7 +208,7 @@ void givenApiInfoNullAndBasePathAsNotRoot() { routedServices.addRoutedService(routedService); routedServices.addRoutedService(routedService2); - ApiDocInfo apiDocInfo = new ApiDocInfo(null, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo); Swagger actualSwagger = convertJsonToSwagger(actualContent); @@ -223,7 +230,7 @@ void givenApiInfoNullAndBasePathAsRoot() { routedServices.addRoutedService(routedService); routedServices.addRoutedService(routedService2); - ApiDocInfo apiDocInfo = new ApiDocInfo(null, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo); Swagger actualSwagger = convertJsonToSwagger(actualContent); @@ -248,7 +255,7 @@ void givenMultipleRoutedService() { routedServices.addRoutedService(routedService3); ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc",null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo); Swagger actualSwagger = convertJsonToSwagger(actualContent); @@ -298,7 +305,7 @@ void givenServiceUrlAsRoot() { routedServices.addRoutedService(routedService2); ApiInfo apiInfo = new ApiInfo(API_VERSION, "api/v1", API_ID, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo); Swagger actualSwagger = convertJsonToSwagger(actualContent); @@ -310,6 +317,21 @@ void givenServiceUrlAsRoot() { assertThat(actualSwagger.getPaths(), IsMapContaining.hasKey(dummySwaggerObject.getBasePath() + k)) ); } + + @Test + void givenOpenApiWithoutVersion() throws JsonProcessingException { + SwaggerParser swaggerParser = new SwaggerParser(); + Swagger swagger = new Swagger(); + String transformedOpenApi = apiDocV2Service.transformApiDoc("serviceId", ApiDocInfo.builder() + .apiInfo(ApiInfo.builder().version("1.2.3").build()) + .apiDocContent(Json.mapper().writeValueAsString(swagger)) + .build() + ); + swagger = swaggerParser.readWithInfo(transformedOpenApi).getSwagger(); + + assertEquals("1.2.3", swagger.getInfo().getVersion()); + } + } @Test @@ -325,7 +347,7 @@ void givenApimlHiddenTag_thenShouldBeSameDescriptionAndPaths() { routedServices.addRoutedService(routedService2); ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc", null, null); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV2Service.transformApiDoc(SERVICE_ID, apiDocInfo); Swagger actualSwagger = convertJsonToSwagger(actualContent); @@ -349,18 +371,19 @@ void givenInputFile_thenParseItCorrectly() throws IOException { GatewayClient gatewayClient = new GatewayClient(gatewayConfigProperties); AtomicReference swaggerHolder = new AtomicReference<>(); - ApiDocV2Service apiDocV2Service = new ApiDocV2Service(gatewayClient) { + apiDocV2Service = new ApiDocV2Service(ApplicationInfo.builder().build(), gatewayClient) { @Override protected void updateExternalDoc(Swagger swagger, ApiDocInfo apiDocInfo) { super.updateExternalDoc(swagger, apiDocInfo); swaggerHolder.set(swagger); } }; - String transformed = apiDocV2Service.transformApiDoc("serviceId", new ApiDocInfo( - mock(ApiInfo.class), - IOUtils.toString(new ClassPathResource("swagger/swagger2.json").getInputStream(), StandardCharsets.UTF_8), - mock(RoutedServices.class) - )); + String transformed = apiDocV2Service.transformApiDoc("serviceId", ApiDocInfo.builder() + .apiInfo(mock(ApiInfo.class)) + .apiDocContent(IOUtils.toString(new ClassPathResource("swagger/swagger2.json").getInputStream(), StandardCharsets.UTF_8)) + .routes(mock(RoutedServices.class)) + .build() + ); assertNotNull(transformed); verifySwagger2(swaggerHolder.get()); } @@ -381,7 +404,7 @@ private String writeOpenApiAsString(Swagger swagger, ObjectMapper objectMapper) try { return objectMapper.writeValueAsString(swagger); } catch (JsonProcessingException e) { - e.printStackTrace(); + log.error("Cannot serialize swagger", e); return null; } } @@ -401,7 +424,7 @@ private Swagger readStringToOpenApi(String content, ObjectMapper objectMapper) { try { swagger = objectMapper.readValue(content, Swagger.class); } catch (IOException e) { - e.printStackTrace(); + log.error("Cannot read swagger content", e); } return swagger; @@ -443,4 +466,5 @@ private ServiceAddress getProperties() { .hostname("localhost:10010") .build(); } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java index 26c13ee806..de5b4b66e5 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiDocV3ServiceTest.java @@ -19,22 +19,24 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.oas.models.tags.Tag; +import io.swagger.v3.parser.OpenAPIV3Parser; +import jakarta.validation.UnexpectedTypeException; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.RoutedServices; -import jakarta.validation.UnexpectedTypeException; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -46,6 +48,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; +@Slf4j class ApiDocV3ServiceTest { private static final String HIDDEN_TAG = "apimlHidden"; @@ -66,14 +69,16 @@ class ApiDocV3ServiceTest { void setUp() { ServiceAddress gatewayConfigProperties = getProperties(); gatewayClient = new GatewayClient(gatewayConfigProperties); - apiDocV3Service = new ApiDocV3Service(gatewayClient); + apiDocV3Service = new ApiDocV3Service(ApplicationInfo.builder().build(), gatewayClient); ReflectionTestUtils.setField(apiDocV3Service, "scheme", "https"); } @Nested class WhenApiDocTransform { + @Nested class ThenCheckUpdatedValues { + @Test void givenOpenApiValidJson() { List servers = new ArrayList<>(); @@ -90,7 +95,7 @@ void givenOpenApiValidJson() { routedServices.addRoutedService(routedService); routedServices.addRoutedService(routedService2); ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV3Service.transformApiDoc(SERVICE_ID, apiDocInfo); OpenAPI actualSwagger = convertJsonToOpenApi(actualContent); @@ -138,7 +143,7 @@ void givenOpenApiValidYaml() { routedServices.addRoutedService(routedService); routedServices.addRoutedService(routedService2); ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, apiDocContent, routedServices); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(apiDocContent).routes(routedServices).build(); String actualContent = apiDocV3Service.transformApiDoc(SERVICE_ID, apiDocInfo); OpenAPI actualSwagger = convertYamlToOpenApi(actualContent); @@ -169,15 +174,31 @@ void givenOpenApiValidYaml() { assertEquals(EXTERNAL_DOCUMENTATION, actualSwagger.getExternalDocs().getDescription()); assertEquals(apiDocInfo.getApiInfo().getDocumentationUrl(), actualSwagger.getExternalDocs().getUrl()); } + + @Test + void givenOpenApiWithoutVersion() throws JsonProcessingException { + OpenAPIV3Parser openAPIV3Parser = new OpenAPIV3Parser(); + OpenAPI openAPI = new OpenAPI(); + String transformedOpenApi = apiDocV3Service.transformApiDoc("serviceId", ApiDocInfo.builder() + .apiInfo(ApiInfo.builder().version("1.2.3").build()) + .apiDocContent(apiDocV3Service.objectMapper().writeValueAsString(openAPI)) + .build() + ); + openAPI = openAPIV3Parser.readContents(transformedOpenApi).getOpenAPI(); + + assertEquals("1.2.3", openAPI.getInfo().getVersion()); + } + } @Nested class ThenThrowException { + @Test void givenEmptyJson() { String invalidJson = ""; ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, invalidJson, null); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(invalidJson).build(); Exception exception = assertThrows(UnexpectedTypeException.class, () -> apiDocV3Service.transformApiDoc(SERVICE_ID, apiDocInfo)); assertEquals("The OpenAPI for service 'serviceId' was retrieved but was not a valid JSON document. '[Null or empty definition]'", exception.getMessage()); @@ -189,11 +210,12 @@ void givenInvalidJson() { String error = "The OpenAPI for service 'serviceId' was retrieved but was not a valid JSON document. '[Cannot construct instance of `java.util.LinkedHashMap` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('nonsense')\n" + " at [Source: UNKNOWN; byte offset: #UNKNOWN]]'"; ApiInfo apiInfo = new ApiInfo(API_ID, "api/v1", API_VERSION, "https://localhost:10014/apicatalog/api-doc", null, "https://www.zowe.org"); - ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, invalidJson, null); + ApiDocInfo apiDocInfo = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(invalidJson).build(); Exception exception = assertThrows(UnexpectedTypeException.class, () -> apiDocV3Service.transformApiDoc(SERVICE_ID, apiDocInfo)); assertEquals(error, exception.getMessage()); } + } /** @@ -208,7 +230,7 @@ void givenValidApiDoc_thenDontCapitalizeEnums() { RoutedServices routedServices = new RoutedServices(); routedServices.addRoutedService(routedService); - ApiDocInfo info = new ApiDocInfo(apiInfo, content, routedServices); + ApiDocInfo info = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(content).routes(routedServices).build(); assertThat(content, not(containsString("\"style\":\"form\""))); assertThat(content, not(containsString("\"style\":\"FORM\""))); @@ -229,21 +251,22 @@ private void verifyOpenApi3(OpenAPI openAPI) { @Test void givenInputFile_thenParseItCorrectly() throws IOException { ServiceAddress gatewayConfigProperties = ServiceAddress.builder().scheme("https").hostname("localhost").build(); - GatewayClient gatewayClient = new GatewayClient(gatewayConfigProperties); + gatewayClient.setGatewayConfigProperties(gatewayConfigProperties); AtomicReference openApiHolder = new AtomicReference<>(); - ApiDocV3Service apiDocV3Service = new ApiDocV3Service(gatewayClient) { + apiDocV3Service = new ApiDocV3Service(ApplicationInfo.builder().build(), gatewayClient) { @Override protected void updateExternalDoc(OpenAPI openAPI, ApiDocInfo apiDocInfo) { super.updateExternalDoc(openAPI, apiDocInfo); openApiHolder.set(openAPI); } }; - String transformed = apiDocV3Service.transformApiDoc("serviceId", new ApiDocInfo( - mock(ApiInfo.class), - IOUtils.toString(new ClassPathResource("swagger/openapi3.json").getInputStream(), StandardCharsets.UTF_8), - mock(RoutedServices.class) - )); + String transformed = apiDocV3Service.transformApiDoc("serviceId", ApiDocInfo.builder() + .apiInfo(mock(ApiInfo.class)) + .apiDocContent(IOUtils.toString(new ClassPathResource("swagger/openapi3.json").getInputStream(), StandardCharsets.UTF_8)) + .routes(mock(RoutedServices.class)) + .build() + ); assertNotNull(transformed); verifyOpenApi3(openApiHolder.get()); } @@ -256,12 +279,13 @@ void givenValidApiDoc_thenDoNotLeakExampleSetFlag() throws IOException { RoutedServices routedServices = new RoutedServices(); routedServices.addRoutedService(new RoutedService("api-v1", "api/v1", "/apicatalog")); - ApiDocInfo info = new ApiDocInfo(apiInfo, content, routedServices); + ApiDocInfo info = ApiDocInfo.builder().apiInfo(apiInfo).apiDocContent(content).routes(routedServices).build(); assertThat(content, containsString("\"exampleSetFlag\":")); String actualContent = apiDocV3Service.transformApiDoc(SERVICE_ID, info); assertThat(actualContent, not(containsString("\"exampleSetFlag\":"))); } + } private String convertOpenApiToJson(OpenAPI openApi) { @@ -278,7 +302,7 @@ private String writeOpenApiAsString(OpenAPI openApi, ObjectMapper objectMapper) try { return objectMapper.writeValueAsString(openApi); } catch (JsonProcessingException e) { - e.printStackTrace(); + log.error("Cannot serializable openApi", e); return null; } } @@ -298,7 +322,7 @@ private OpenAPI readStringToOpenApi(String content, ObjectMapper objectMapper) { try { openAPI = objectMapper.readValue(content, OpenAPI.class); } catch (IOException e) { - e.printStackTrace(); + log.error("Cannnot parse OpenAPI content", e); } return openAPI; @@ -342,4 +366,5 @@ private ServiceAddress getProperties() { .hostname("localhost:10010") .build(); } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiTransformationConfigTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiTransformationConfigTest.java index d31a65e5c4..67d74ba72f 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiTransformationConfigTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/ApiTransformationConfigTest.java @@ -10,61 +10,68 @@ package org.zowe.apiml.apicatalog.swagger.api; -import org.junit.jupiter.api.BeforeEach; +import com.fasterxml.jackson.core.JsonParseException; +import jakarta.validation.UnexpectedTypeException; import org.junit.jupiter.api.Test; +import org.zowe.apiml.apicatalog.config.ApiTransformationConfig; +import org.zowe.apiml.config.ApplicationInfo; -import jakarta.validation.UnexpectedTypeException; import java.util.function.Function; import static org.junit.jupiter.api.Assertions.*; class ApiTransformationConfigTest { - private AbstractApiDocService abstractApiDocService; - private final ApiTransformationConfig apiTransformationConfig = new ApiTransformationConfig(null); + private final ApiTransformationConfig apiTransformationConfig = new ApiTransformationConfig(ApplicationInfo.builder().build(), null); private final Function> beanApiDocFactory = apiTransformationConfig.beanApiDocFactory(); - @BeforeEach - void setUp() { - abstractApiDocService = null; - } - @Test void givenSwaggerJson_whenGetApiDocService_thenReturnApiDocV2Service() { - abstractApiDocService = beanApiDocFactory.apply("{\"swagger\": \"2.0\"}"); + var abstractApiDocService = beanApiDocFactory.apply("{\"swagger\": \"2.0\"}"); assertTrue(abstractApiDocService instanceof ApiDocV2Service, "AbstractApiDocService is not ApiDocV2Service"); } @Test void givenOpenApiJson_whenGetApiDocService_thenReturnApiDocV3Service() { - abstractApiDocService = beanApiDocFactory.apply("{\"openapi\": \"3.0\"}"); + var abstractApiDocService = beanApiDocFactory.apply("{\"openapi\": \"3.0\"}"); assertTrue(abstractApiDocService instanceof ApiDocV3Service, "AbstractApiDocService is not ApiDocV3Service"); } @Test void givenSwaggerYml_whenGetApiDocService_thenReturnApiDocV2Service() { - abstractApiDocService = beanApiDocFactory.apply("swagger: 2.0"); + var abstractApiDocService = beanApiDocFactory.apply("swagger: 2.0"); assertTrue(abstractApiDocService instanceof ApiDocV2Service, "AbstractApiDocService is not ApiDocV2Service"); } @Test void givenOpenApiYml_whenGetApiDocService_thenReturnApiDocV3Service() { - abstractApiDocService = beanApiDocFactory.apply("openapi: 3.0"); + var abstractApiDocService = beanApiDocFactory.apply("openapi: 3.0"); assertTrue(abstractApiDocService instanceof ApiDocV3Service, "AbstractApiDocService is not ApiDocV3Service"); } @Test void givenApiDocNotInOpenApiNorSwagger_whenGetApiDocService_thenReturnNull() { - abstractApiDocService = beanApiDocFactory.apply("{\"superapi\": \"3.0\"}"); + var abstractApiDocService = beanApiDocFactory.apply("{\"superapi\": \"3.0\"}"); assertNull(abstractApiDocService, "abstractApiDocService is not null"); } @Test void givenApDocVersionIsNotAsExpectedFormat_whenGetApiDocService_thenThrowException() { - Exception exception = assertThrows(UnexpectedTypeException.class, () -> { - abstractApiDocService = beanApiDocFactory.apply("FAILED FORMAT"); - }); - assertNull(abstractApiDocService); + Exception exception = assertThrows(UnexpectedTypeException.class, () -> beanApiDocFactory.apply("FAILED FORMAT")); assertEquals("Response is not a Swagger or OpenAPI type object.", exception.getMessage()); } + + @Test + void givenJsonWithTabs_whenGetApiDocService_thenIsParsed() { + var abstractApiDocService = beanApiDocFactory.apply("{\t\"openapi\": \"3.0\"}"); + assertTrue(abstractApiDocService instanceof ApiDocV3Service, "Parser doesn't support tabulators even it is JSON"); + } + + @Test + void givenYamlWithTabs_whenGetApiDocService_thenItIsUnparseable() { + var e = assertThrows(UnexpectedTypeException.class, () -> beanApiDocFactory.apply("\tswagger: 2.0")); + assertInstanceOf(JsonParseException.class, e.getCause()); + assertTrue(e.getCause().getMessage().contains("Do not use \\t(TAB) for indentation")); + } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/dummy/DummyApiDocService.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/DummyApiDocService.java similarity index 69% rename from api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/dummy/DummyApiDocService.java rename to api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/DummyApiDocService.java index a27787bc27..8c54dc531f 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/dummy/DummyApiDocService.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/swagger/api/DummyApiDocService.java @@ -8,16 +8,16 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.apicatalog.swagger.api.dummy; +package org.zowe.apiml.apicatalog.swagger.api; -import org.zowe.apiml.apicatalog.services.cached.model.ApiDocInfo; -import org.zowe.apiml.apicatalog.swagger.api.AbstractApiDocService; +import org.zowe.apiml.apicatalog.model.ApiDocInfo; +import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.product.gateway.GatewayClient; public class DummyApiDocService extends AbstractApiDocService { - public DummyApiDocService(GatewayClient gatewayClient) { - super(gatewayClient); + public DummyApiDocService(ApplicationInfo applicationInfo, GatewayClient gatewayClient) { + super(applicationInfo, gatewayClient); } @Override @@ -27,11 +27,12 @@ public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { @Override protected void updatePaths(Object swaggerAPI, String serviceId, ApiDocInfo apiDocInfo, boolean hidden) { - + // dummy implementation } @Override protected void updateExternalDoc(Object swaggerAPI, ApiDocInfo apiDocInfo) { - + // dummy implementation } + } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApplicationsWrapper.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApplicationsWrapper.java deleted file mode 100644 index f36d7e1d05..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApplicationsWrapper.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.util; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.netflix.discovery.shared.Applications; -import lombok.Data; - -@JsonDeserialize(as = ApplicationsWrapper.class) -@Data -public class ApplicationsWrapper { - - private Applications applications; - - public ApplicationsWrapper() { - } - - public ApplicationsWrapper(Applications applications) { - this.applications = applications; - } -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java deleted file mode 100644 index 7757c67dcf..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.util; - -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.model.APIService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; - -import java.util.*; -import java.util.stream.Stream; - -import static org.mockito.Mockito.when; - -public class ContainerServiceMockUtil { - - public ContainerServiceState createContainersServicesAndInstances() { - ContainerServiceState containerServiceState = new ContainerServiceState(); - containerServiceState.setApplications(new ArrayList<>()); - containerServiceState.setServices(new ArrayList<>()); - containerServiceState.setInstances(new ArrayList<>()); - containerServiceState.setContainers(new ArrayList<>()); - - List services = new ArrayList<>(); - - int index = 1000; - - index = generateInstancesAndServices(containerServiceState, services, "service1", index, 3); - index = generateInstancesAndServices(containerServiceState, services, "service2", index, 1); - - // two containers same services - APIContainer container = new APIContainer("api-one", "API One", "This is API One", new HashSet<>(services)); - APIContainer container1 = new APIContainer("api-two", "API Two", "This is API Two", new HashSet<>(services)); - - // one extra service - index = generateInstancesAndServices(containerServiceState, services, "service3", index, 1); - - APIContainer container2 = new APIContainer("api-three", "API Three", "This is API Three", new HashSet<>(services)); - - // unique service - services.clear(); - - index = generateInstancesAndServices(containerServiceState, services, "service4", index, 1); - - APIContainer container4 = new APIContainer("api-four", "API Four", "This is API Four", new HashSet<>(services)); - - containerServiceState.setContainers(Arrays.asList(container, container1, container2, container4)); - - return containerServiceState; - } - - public void mockServiceRetrievalFromCache(CachedServicesService cachedServicesService, - List applications) { - applications.forEach(application -> - when(cachedServicesService.getService(application.getName())).thenReturn(application)); - } - - public InstanceInfo createInstance(String serviceId, String instanceId, - InstanceInfo.InstanceStatus status, - InstanceInfo.ActionType actionType, - HashMap metadata) { - return new InstanceInfo(instanceId, serviceId.toUpperCase(), null, "192.168.0.1", null, - new InstanceInfo.PortWrapper(true, 9090), null, null, null, null, null, null, null, 0, null, "hostname", - status, null, null, null, null, metadata, null, null, actionType, null); - } - - private int generateInstancesAndServices(ContainerServiceState containerServiceState, - List services, - String serviceId, - int index, - int limit) { - List generatedInstances = Stream.iterate(index, i -> i + 1) - .limit(limit) - .map(mIndex -> getInstance(mIndex, serviceId)) - .toList(); - - addApiService(serviceId, containerServiceState.getServices(), services); - addInstancesToApplications( - generatedInstances, - containerServiceState.getApplications(), - containerServiceState.getInstances(), - serviceId); - - return index + limit; - } - - private void addInstancesToApplications(List instanceCollection, - List applications, - List instances, - String serviceId) { - instances.addAll(instanceCollection); - applications.add(new Application(serviceId, instanceCollection)); - } - - - private APIService addApiService(String serviceId, - List allServices, - List services) { - APIService service = new APIService.Builder(serviceId) - .title(serviceId + "-title") - .description(serviceId + "-desc") - .secured(false) - .baseUrl("base") - .homePageUrl("home") - .basePath("base") - .sso(false) - .apis(Collections.emptyMap()) - .build(); - services.add(service); - allServices.add(service); - return service; - } - - private InstanceInfo getInstance(int index, String serviceId) { - InstanceInfo instance = createInstance( - serviceId, - serviceId + ":" + index, - InstanceInfo.InstanceStatus.UP, - InstanceInfo.ActionType.ADDED, - new HashMap<>()); - return instance; - } - -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceState.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceState.java deleted file mode 100644 index 8e58df8d99..0000000000 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceState.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.apicatalog.util; - -import org.zowe.apiml.apicatalog.model.APIContainer; -import org.zowe.apiml.apicatalog.model.APIService; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; -import lombok.Data; - -import java.util.List; - -@Data -public class ContainerServiceState { - private List containers; - private List services; - private List instances; - private List applications; -} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ServicesBuilder.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ServicesBuilder.java index 5e703c00c0..1e724fbdf7 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ServicesBuilder.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ServicesBuilder.java @@ -10,43 +10,25 @@ package org.zowe.apiml.apicatalog.util; -import java.util.HashMap; -import java.util.Map; - import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.shared.Application; +import lombok.experimental.UtilityClass; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; +import java.util.HashMap; +import java.util.Map; import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; +@UtilityClass public class ServicesBuilder { - private int id = 0; - private CachedProductFamilyService service; - - public InstanceInfo instance1; - public InstanceInfo instance2; - - public ServicesBuilder(CachedProductFamilyService service) { - this.service = service; - - instance1 = createInstance("service1", "demoapp"); - instance2 = createInstance("service2", "demoapp2"); - } - public Application createApp(String serviceId, InstanceInfo...instanceInfos) { - Application application = new Application(serviceId); - for (InstanceInfo instanceInfo : instanceInfos) { - application.addInstance(instanceInfo); - service.saveContainerFromInstance(serviceId, instanceInfo); - } - return application; - } + private int id = 0; - public InstanceInfo createInstance(String serviceId, + public ServiceInstance createInstance(String serviceId, InstanceInfo.InstanceStatus status, Map metadata) { - return InstanceInfo.Builder.newBuilder() + return new EurekaServiceInstance(InstanceInfo.Builder.newBuilder() .setInstanceId(serviceId + (id++)) .setAppName(serviceId) .setStatus(status) @@ -54,14 +36,14 @@ public InstanceInfo createInstance(String serviceId, .setHomePageUrl(null, "https://localhost:8080/") .setVIPAddress(serviceId) .setMetadata(metadata) - .build(); + .build()); } - public InstanceInfo createInstance(String serviceId, String catalogId, Map.Entry...otherMetadata) { + public ServiceInstance createInstance(String serviceId, String catalogId, Map.Entry...otherMetadata) { return createInstance(serviceId, catalogId, InstanceInfo.InstanceStatus.UP, otherMetadata); } - public InstanceInfo createInstance( + public ServiceInstance createInstance( String serviceId, String catalogId, InstanceInfo.InstanceStatus status, Map.Entry...otherMetadata ) { @@ -70,16 +52,7 @@ public InstanceInfo createInstance( otherMetadata); } - public InstanceInfo createInstance( - String serviceId, String catalogId, String catalogVersion, String title, - Map.Entry...otherMetadata - ) { - return createInstance( - serviceId, catalogId, title, "Description", catalogVersion, InstanceInfo.InstanceStatus.UP, - otherMetadata); - } - - public InstanceInfo createInstance(String serviceId, + public ServiceInstance createInstance(String serviceId, String catalogId, String catalogTitle, String catalogDescription, @@ -97,4 +70,5 @@ public InstanceInfo createInstance(String serviceId, return createInstance(serviceId, status, metadata); } + } diff --git a/api-catalog-services/src/test/resources/application.yml b/api-catalog-services/src/test/resources/application.yml index 47439542ba..f5bed0e151 100644 --- a/api-catalog-services/src/test/resources/application.yml +++ b/api-catalog-services/src/test/resources/application.yml @@ -16,6 +16,15 @@ spring: main: banner-mode: ${apiml.banner:"off"} allow-circular-references: true + web-application-type: reactive + +springdoc: + packagesToScan: org.zowe.apiml.apicatalog.controllers.api + api-docs: + path: /apicatalog/v3/api-docs + group-configs: + - group: apicatalog + pathsToMatch: /apicatalog/** logging: level: @@ -44,7 +53,6 @@ apiml: hostname: localhost ipAddress: 0.0.0.0 port: 10014 - contextPath: /apicatalog scheme: https discoveryServiceUrls: https://localhost2:10021/eureka/,https://localhost:10011/eureka/ @@ -84,8 +92,6 @@ apiml: server: address: ${apiml.service.ipAddress} port: ${apiml.service.port} - servlet: - contextPath: ${apiml.service.contextPath} ssl: enabled: ${apiml.security.ssl.sslEnabled} protocol: ${apiml.security.ssl.protocol} @@ -110,10 +116,10 @@ eureka: instance: instanceId: ${apiml.service.hostname}:${apiml.service.id}:${apiml.service.port} hostname: ${apiml.service.hostname} - statusPageUrlPath: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/info - healthCheckUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/health - secureHealthCheckUrl: https://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/health - homePageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath} + statusPageUrlPath: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/info + healthCheckUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health + secureHealthCheckUrl: https://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health + homePageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/apicatalog port: ${apiml.service.port} securePort: ${apiml.service.port} nonSecurePortEnabled: ${apiml.service.nonSecurePortEnabled} @@ -139,7 +145,7 @@ eureka: - apiId: zowe.apiml.apicatalog version: 1.0.0 gatewayUrl: api/v1 - swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v3/api-docs + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs service: title: API Catalog @@ -158,7 +164,7 @@ management: endpoints: migrate-legacy-ids: true web: - base-path: /application + base-path: /apicatalog/application exposure: include: health,info health: @@ -176,7 +182,6 @@ management: endpoints: migrate-legacy-ids: true web: - base-path: /application exposure: include: health,info,loggers @@ -197,7 +202,6 @@ management: endpoints: migrate-legacy-ids: true web: - base-path: /application exposure: include: "*" @@ -217,7 +221,7 @@ eureka: securePort: 0 nonSecurePortEnabled: true securePortEnabled: false - secureHealthCheckUrl: http://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/application/health + secureHealthCheckUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health metadata-map: apiml: corsEnabled: true @@ -225,7 +229,7 @@ eureka: - apiId: zowe.apiml.apicatalog version: 1.0.0 gatewayUrl: api/v1 - swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v3/api-docs + swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs apiml: service: scheme: http diff --git a/api-catalog-services/src/test/resources/localhost_truststore.jks b/api-catalog-services/src/test/resources/localhost_truststore.jks deleted file mode 100644 index c68683eab45e4e9e4c46fc04d0484c7ffd4424d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1109 zcmezO_TO6u1_mY|W(3o`Ir+(nIq}Jf@kROhB|y=X`&H&z46G4)rUsS_3@rQxO)T67 zO-!E`Ff%bSF>x~d60GRhZD6;}fR~L^tIebBJ1-+6H!Fid<7`8215P&PP!={}Cg&(a zVFN)Bhl7VJI6pU4H@GCRB-M}~s0%2_&chZ^l$c(cYRGHA4HDquVTsBwPc@V=kOFbJ zd4wGUJQaLXQ!*1vGV}8kd=e{DiwrFd%t5luJO&`e3NEQ-sX6%txv6<23Z8kzC5d^- zsl^J;sYNB3X_?7D>l7SIOEU6{GD|8AjfZ{A|mT4~JWY{am4?$S59tCH?b z+bb-0)MgQrRJmDaL&nz$^3%8aUEg#pW!AdXcZsTElkeT)H<&N1*($NxNWZ7yj@p5H z8Fjhe*QF(9@927Erk{{jjV%Ow+549#U?&O>6ubxq1{wGW9_+lY1ScqThNP)oaZ_1({%eBfiMfJ0W%}xf8;O+CUIbx zGcvGMe+hirnDl&xWDawc&h+b^UVYD7-n=WF#Qj2Z-LqfQf}iPL*UWcW{eShI?qljX z<|W>ar}auXsuDv3CPdliy+86_?0KvE0byZ3^P8tnty%jqkUz{LszmUv&1;`+uSKh- zG@p;G`tye^-`e3#;5ywq3MacCFL!oPn{!)!wu-oZ$GsEhHTdiJZ6%W*)NAHn+xX8% zwr2ho=|kR4?%zx789h=@n9BUwxRzB|s&xA5^ZQmjos!M0e6=52^HtpoqEdO$8%wmgwJG+TQBVOx0NKNsO+zpAZU{|owKuS@_lK(eH>QG l;#SV;d_7z5p55Em`QqN!fd7lyTzES4_!*l`*Pn<#4*<`inBV{a diff --git a/api-catalog-services/src/test/resources/standalone/invalid-apiDoc/apiDocs/service1-instance1_zowe v1.0.0_default.json b/api-catalog-services/src/test/resources/standalone/invalid-apiDoc/apiDocs/service1-instance1_zowe v1.0.0_default.json deleted file mode 100644 index fa4a2e51a2..0000000000 --- a/api-catalog-services/src/test/resources/standalone/invalid-apiDoc/apiDocs/service1-instance1_zowe v1.0.0_default.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "negative": "testing" -} diff --git a/api-catalog-services/src/test/resources/standalone/invalid-apiDocName/apiDocs/service2.json b/api-catalog-services/src/test/resources/standalone/invalid-apiDocName/apiDocs/service2.json deleted file mode 100644 index c46874e76a..0000000000 --- a/api-catalog-services/src/test/resources/standalone/invalid-apiDocName/apiDocs/service2.json +++ /dev/null @@ -1 +0,0 @@ -negative testing diff --git a/api-catalog-services/src/test/resources/standalone/invalid-app/apps/service2.json b/api-catalog-services/src/test/resources/standalone/invalid-app/apps/service2.json deleted file mode 100644 index fa4a2e51a2..0000000000 --- a/api-catalog-services/src/test/resources/standalone/invalid-app/apps/service2.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "negative": "testing" -} diff --git a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance1_zowe v1.0.0_default.json b/api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance1_zowe v1.0.0_default.json deleted file mode 100644 index b130e5443c..0000000000 --- a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance1_zowe v1.0.0_default.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Swagger Petstore - OpenAPI 3.0", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.", - "version": "1.0.0" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://zowe.org" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - } - ], - "paths": { - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - } - }, - "components": { - "schemas": { - "Pet": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - } - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance2_zowe v1.0.0_default.json b/api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance2_zowe v1.0.0_default.json deleted file mode 100644 index b130e5443c..0000000000 --- a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service1-instance2_zowe v1.0.0_default.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Swagger Petstore - OpenAPI 3.0", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.", - "version": "1.0.0" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://zowe.org" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - } - ], - "paths": { - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - } - }, - "components": { - "schemas": { - "Pet": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - } - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v1.0.0_default.json b/api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v1.0.0_default.json deleted file mode 100644 index b130e5443c..0000000000 --- a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v1.0.0_default.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Swagger Petstore - OpenAPI 3.0", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.", - "version": "1.0.0" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://zowe.org" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - } - ], - "paths": { - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - } - }, - "components": { - "schemas": { - "Pet": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - } - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v2.0.0.json b/api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v2.0.0.json deleted file mode 100644 index b130e5443c..0000000000 --- a/api-catalog-services/src/test/resources/standalone/services/apiDocs/service2_zowe v2.0.0.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Swagger Petstore - OpenAPI 3.0", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.", - "version": "1.0.0" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://zowe.org" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - } - ], - "paths": { - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - } - }, - "components": { - "schemas": { - "Pet": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - } - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/api-catalog-services/src/test/resources/standalone/services/apps/service1.json b/api-catalog-services/src/test/resources/standalone/services/apps/service1.json deleted file mode 100644 index afe54d072e..0000000000 --- a/api-catalog-services/src/test/resources/standalone/services/apps/service1.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "application": [ - { - "name": "SERVICE1-INSTANCE1", - "instance": [ - { - "hostName": "zowe.org", - "app": "service1-instance1", - "ipAddr": "1.1.1.1", - "status": "UP", - "port": { - "$": 1000, - "@enabled": "false" - }, - "securePort": { - "$": 1000, - "@enabled": "true" - }, - "vipAddress": "service1", - "metadata": { - "apiml.service.description": "Test mock application 1 / instance 1 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.api.version": "1.0.0", - "apiml.apiInfo.api.defaultApi": "false", - "apiml.apiInfo.api.gatewayUrl": "api", - "apiml.apiInfo.api.swaggerUrl": "/standAloneApps/swagger/openapi.json", - "apiml.apiInfo.api.apiId": "zowe", - "apiml.routes.api.serviceUrl": "", - "apiml.apiInfo.api.documentationUrl": "https://www.zowe.org", - "apiml.service.title": "Test mock application 1", - "apiml.catalog.tile.description": "Mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock1", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:1000" - } - ] - }, - { - "name": "SERVICE1-INSTANCE2", - "instance": [ - { - "hostName": "zowe.org", - "app": "service1-instance2", - "ipAddr": "1.1.1.2", - "status": "UP", - "port": { - "$": 2000, - "@enabled": "false" - }, - "securePort": { - "$": 2000, - "@enabled": "true" - }, - "vipAddress": "service1", - "metadata": { - "apiml.service.description": "Test mock application 1 / instance 2 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.api.version": "1.0.0", - "apiml.apiInfo.api.defaultApi": "true", - "apiml.apiInfo.api.gatewayUrl": "api", - "apiml.apiInfo.api.apiId": "zowe", - "apiml.routes.api.serviceUrl": "", - "apiml.apiInfo.api.documentationUrl": "https://www.zowe.org", - "apiml.service.title": "Test mock application 1", - "apiml.catalog.tile.description": "Mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock1", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:2000" - } - ] - } - ] -} diff --git a/api-catalog-services/src/test/resources/standalone/services/apps/service2.json b/api-catalog-services/src/test/resources/standalone/services/apps/service2.json deleted file mode 100644 index fa19c89212..0000000000 --- a/api-catalog-services/src/test/resources/standalone/services/apps/service2.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "application": [ - { - "name": "SERVICE2", - "instance": [ - { - "hostName": "zowe.org", - "app": "service2", - "ipAddr": "1.1.2.1", - "status": "UP", - "port": { - "$": 3000, - "@enabled": "false" - }, - "securePort": { - "$": 3000, - "@enabled": "true" - }, - "vipAddress": "service2", - "metadata": { - "apiml.service.description": "Test mock application 2 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.v1.version": "1.0.0", - "apiml.apiInfo.v1.defaultApi": "true", - "apiml.apiInfo.v1.gatewayUrl": "api", - "apiml.apiInfo.v1.swaggerUrl": "/services/swagger/openapi.json", - "apiml.apiInfo.v1.apiId": "zowe", - "apiml.apiInfo.v1.documentationUrl": "https://www.zowe.org", - "apiml.apiInfo.v2.version": "2.0.0", - "apiml.apiInfo.v2.defaultApi": "false", - "apiml.apiInfo.v2.gatewayUrl": "api", - "apiml.apiInfo.v2.swaggerUrl": "/services/swagger/openapi.json", - "apiml.apiInfo.v2.apiId": "zowe", - "apiml.apiInfo.v2.documentationUrl": "https://www.zowe.org", - "apiml.routes.api.serviceUrl": "", - "apiml.service.title": "Test mock application 2", - "apiml.catalog.tile.description": "Another mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock2", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Another mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:1000" - } - ] - } - ] -} diff --git a/api-catalog-ui/frontend/cypress.modulith.config.js b/api-catalog-ui/frontend/cypress.modulith.config.js index d4d5941b4a..24ac4fd172 100644 --- a/api-catalog-ui/frontend/cypress.modulith.config.js +++ b/api-catalog-ui/frontend/cypress.modulith.config.js @@ -13,7 +13,7 @@ const { defineConfig } = require('cypress'); module.exports = defineConfig({ env: { catalogHomePage: 'https://localhost:10010/apicatalog/ui/v1', - loginUrl: 'https://localhost:10010/apicatalog/api/v1/auth/login', + loginUrl: 'https://localhost:10010/gateway/api/v1/auth/login', gatewayOktaRedirect: 'https://localhost:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Flocalhost%3A10010%2Fapplication', username: 'USER', diff --git a/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js b/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js index e37e3ce4c5..8ff652ec02 100644 --- a/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js @@ -92,7 +92,7 @@ describe('>>> Dashboard test', () => { // Set the cookie in the Cypress browser cy.setCookie('apimlAuthenticationToken', cookieValue); - cy.visit(`${Cypress.env('catalogHomePage')}/#/dashboard`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/dashboard`); cy.get('.header').should('exist'); cy.url().should('contain', '/dashboard'); diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js index 038a9c640a..1d80758b91 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js @@ -38,7 +38,7 @@ describe('>>> Detail page test', () => { cy.contains('Version: '); cy.get('#grid-container').contains('API Catalog').click(); - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/apicatalog`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/apicatalog`); const baseUrl = `${Cypress.env('catalogHomePage')}`; @@ -72,7 +72,7 @@ describe('>>> Detail page test', () => { cy.contains('Version: '); cy.contains('API Gateway').click(); - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/gateway`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/gateway`); const baseUrl = `${Cypress.env('catalogHomePage')}`; diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js index 4c946e2809..9b8ffe51b6 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js @@ -37,7 +37,7 @@ describe('>>> Multi-tenancy deployment test', () => { cy.contains('Version: '); cy.get('#grid-container').contains('API Gateway (apiml2)').click(); - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/apiml2`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/apiml2`); const baseUrl = `${Cypress.env('catalogHomePage')}`; @@ -63,7 +63,7 @@ describe('>>> Multi-tenancy deployment test', () => { cy.contains('Version: '); cy.contains('API Gateway').click(); - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/gateway`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/gateway`); const baseUrl = `${Cypress.env('catalogHomePage')}`; diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index 76df837b19..8ed9cfb62f 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -30,7 +30,7 @@ describe('>>> Service version compare Test', () => { cy.contains('Service Spring Onboarding Enabler sample application API').click(); // discoverable client - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/discoverableclient`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/discoverableclient`); }); it('Should show compare tab', () => { diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js index df329447cc..cc941e901a 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-switch.cy.js @@ -18,7 +18,7 @@ describe('>>> Service version change Test', () => { cy.contains('Service Spring Onboarding Enabler sample application API').click(); - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/discoverableclient`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/discoverableclient`); }); it('Should contain version tabs', () => { diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js index a36279fc00..d9de5d1aea 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-try-out-and-code-snippets.cy.js @@ -27,7 +27,7 @@ describe('>>> Swagger Try Out and Code Snippets Test', () => { it('Should contain try-out button', () => { cy.log(`Visiting ${test.tile}, ${test.id}`); cy.contains(test.tile).click(); - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/${test.id}`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/${test.id}`); cy.get('.opblock-summary').eq(0).click(); cy.get('.try-out').should('exist'); }); diff --git a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js index 58f6f2252e..47641e12d6 100644 --- a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js @@ -35,7 +35,7 @@ const PATH_TO_PLAYGROUND_INPUT_DATA = '#graphiql-session > div:nth-child(1) > div > div:nth-child(1) > section > div.graphiql-editor > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code'; function login() { - cy.visit(`${Cypress.env('catalogHomePage')}/#/login`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/login`); const username = Cypress.env('username'); const password = Cypress.env('password'); @@ -66,7 +66,7 @@ describe('>>> GraphiQL Playground page test', () => { cy.contains('Discoverable client with GraphQL').click(); - cy.visit(`${Cypress.env('catalogHomePage')}/#/service/graphqlclient`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/service/graphqlclient`); cy.get('.tabs-container').should('not.exist'); diff --git a/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js b/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js index e5009a1f21..6cf2aee00b 100644 --- a/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/login/login-bad.cy.js @@ -12,7 +12,7 @@ describe('>>> Login bad test', () => { it('should not display header', () => { - cy.visit(`${Cypress.env('catalogHomePage')}/`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html`); cy.get('.header').should('not.exist'); }); diff --git a/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js b/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js index e0cd67e280..749bbbfa91 100644 --- a/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js @@ -12,7 +12,7 @@ describe('>>> Login ok page test', () => { it('should not display header', () => { - cy.visit(`${Cypress.env('catalogHomePage')}/#/`); + cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/`); cy.get('.header').should('not.exist'); }); diff --git a/api-catalog-ui/frontend/mocked-backend/assets/containers.json b/api-catalog-ui/frontend/mocked-backend/assets/containers.json index 03a7352aa8..50fabf3591 100644 --- a/api-catalog-ui/frontend/mocked-backend/assets/containers.json +++ b/api-catalog-ui/frontend/mocked-backend/assets/containers.json @@ -189,7 +189,7 @@ "apiId":"zowe.apiml.discoverableclient", "gatewayUrl":"api/v1", "version":null, - "swaggerUrl":"https://localhost:10012/discoverableclient/v2/api-docs", + "swaggerUrl":"https://localhost:10012/discoverableclient/v3/api-docs", "documentationUrl":null, "codeSnippet":[ diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 68a01c05ec..ee10ddf9fe 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -63,7 +63,7 @@ "test": "react-app-rewired test --runInBand --silent --watchAll=false --env=jsdom components/* utils/* reducers/* epics/* actions/* selectors/* ErrorBoundary/* helpers/* --reporters=default --reporters=jest-html-reporter --coverage", "cy:open": "cypress open", "cy:e2e:ci": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --env moudlith=false,catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/apicatalog/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", - "cy:e2e:ci:modulith": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --config-file cypress.modulith.config.js --env modulith=true,catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/apicatalog/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", + "cy:e2e:ci:modulith": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --config-file cypress.modulith.config.js --env modulith=true,catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/gateway/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", "cy:e2e:localhost": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headless", "cy:e2e:localhost-debug": "DEBUG=cypress:net* cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headless", "cy:e2e:localhost-headful": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headed", diff --git a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx index 8cb26d37ae..039253b2c8 100644 --- a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx +++ b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx @@ -34,7 +34,7 @@ function transformSwaggerToCurrentHost(swagger, service) { } } catch (e) { // not a proper url, assume it is an endpoint - server.url = location + server; + server.url = location + server.url; } }); } diff --git a/api-catalog-ui/frontend/src/error-messages.json b/api-catalog-ui/frontend/src/error-messages.json index aba6b1346f..a5c9a7392e 100644 --- a/api-catalog-ui/frontend/src/error-messages.json +++ b/api-catalog-ui/frontend/src/error-messages.json @@ -12,6 +12,10 @@ "messageKey": "ZWEAS120E", "messageText": "Invalid Credentials" }, + { + "messageKey": "ZWEAG120E", + "messageText": "Invalid Credentials" + }, { "messageKey": "ZWEAT604E", "messageText": "Passwords do not match" diff --git a/api-catalog-ui/frontend/src/services/user.service.jsx b/api-catalog-ui/frontend/src/services/user.service.jsx index a15282a2da..f436d5375a 100644 --- a/api-catalog-ui/frontend/src/services/user.service.jsx +++ b/api-catalog-ui/frontend/src/services/user.service.jsx @@ -69,7 +69,7 @@ function query() { } }; - return fetch(`/gateway/api/v1/auth/query`, requestOptions) + return fetch(`/apicatalog/api/v1/auth/query`, requestOptions) .then(async (response) => { const data = await response.json(); return { diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index 045bee35ee..b441305e69 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -11,6 +11,7 @@ package org.zowe.apiml.product.routing.transform; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; @@ -52,7 +53,6 @@ public String transformURL(ServiceType type, String serviceUrl, RoutedServices routes, boolean httpsScheme) throws URLTransformationException { - if (!gatewayClient.isInitialized()) { apimlLog.log("org.zowe.apiml.common.gatewayNotFoundForTransformRequest"); throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); @@ -71,14 +71,27 @@ public String transformURL(ServiceType type, throw new URLTransformationException(message); } - if (serviceUri.getQuery() != null) { serviceUriPath += "?" + serviceUri.getQuery(); } + return transformURL(serviceId, serviceUriPath, route, httpsScheme, serviceUri); + } + + public String transformURL(String serviceId, + String serviceUriPath, + RoutedService route, + boolean httpsScheme, + URI originalUri + ) throws URLTransformationException { + if (!gatewayClient.isInitialized()) { + apimlLog.log("org.zowe.apiml.common.gatewayNotFoundForTransformRequest"); + throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); + } + String endPoint = getShortEndPoint(route.getServiceUrl(), serviceUriPath); if (!endPoint.isEmpty() && !endPoint.startsWith("/")) { - throw new URLTransformationException("The path " + serviceUri.getPath() + " of the service URL " + serviceUri + " is not valid."); + throw new URLTransformationException("The path " + originalUri.getPath() + " of the service URL " + originalUri + " is not valid."); } ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); @@ -135,9 +148,10 @@ public String retrieveApiBasePath(String serviceId, */ private String getShortEndPoint(String routeServiceUrl, String endPoint) { String shortEndPoint = endPoint; - if (!routeServiceUrl.equals(SEPARATOR)) { + if (!SEPARATOR.equals(routeServiceUrl) && StringUtils.isNotBlank(routeServiceUrl)) { shortEndPoint = shortEndPoint.replaceFirst(UrlUtils.removeLastSlash(routeServiceUrl), ""); } return shortEndPoint; } + } diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java index 98b6af9ff7..c98866d014 100644 --- a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java +++ b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java @@ -158,7 +158,7 @@ void givenInvalidHomePage_thenThrowException() { @Test void givenEmptyGatewayClient_thenThrowException() { - String url = "https:localhost:8080/wss"; + String url = "https://localhost:8080/wss"; GatewayClient emptyGatewayClient = new GatewayClient(null); TransformService transformService = new TransformService(emptyGatewayClient); diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index e329101a5c..a34cb19f02 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -328,7 +328,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.discovery.allPeersUrls=${ZWE_DISCOVERY_SERVICES_LIST} \ -Dapiml.discovery.password=password \ -Dapiml.discovery.serviceIdPrefixReplacer=${ZWE_configs_apiml_discovery_serviceIdPrefixReplacer:-${ZWE_components_discovery_apiml_discovery_serviceIdPrefixReplacer}} \ - -Dapiml.discovery.staticApiDefinitionsDirectories=${ZWE_STATIC_DEFINITIONS_DIR} \ + -Dapiml.discovery.staticApiDefinitionsDirectories=${ZWE_STATIC_DEFINITIONS_DIR:-} \ -Dapiml.discovery.userid=eureka \ -Dapiml.gateway.cachePeriodSec=${ZWE_configs_apiml_gateway_registry_cachePeriodSec:-${ZWE_components_gateway_apiml_gateway_registry_cachePeriodSec:-120}} \ -Dapiml.gateway.cookieNameForRateLimit=${cookieName:-apimlAuthenticationToken} \ @@ -404,6 +404,14 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dserver.webSocket.maxIdleTimeout=${ZWE_configs_server_webSocket_maxIdleTimeout:-${ZWE_components_gateway_server_webSocket_maxIdleTimeout:-3600000}} \ -Dserver.webSocket.requestBufferSize=${ZWE_configs_server_webSocket_requestBufferSize:-${ZWE_components_gateway_server_webSocket_requestBufferSize:-8192}} \ -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-https} \ + -Dapiml.catalog.hide.serviceInfo=${ZWE_configs_apiml_catalog_hide_serviceInfo:-${ZWE_components_apicatalog_apiml_catalog_hide_serviceInfo:-false}} \ + -Dapiml.catalog.customStyle.logo=${ZWE_configs_apiml_catalog_customStyle_logo:-${ZWE_components_apicatalog_apiml_catalog_customStyle_logo:-}} \ + -Dapiml.catalog.customStyle.fontFamily=${ZWE_configs_apiml_catalog_customStyle_fontFamily:-${ZWE_components_apicatalog_apiml_catalog_customStyle_fontFamily:-}} \ + -Dapiml.catalog.customStyle.backgroundColor=${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_backgroundColor:-}} \ + -Dapiml.catalog.customStyle.titlesColor=${ZWE_configs_apiml_catalog_customStyle_titlesColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_titlesColor:-}} \ + -Dapiml.catalog.customStyle.headerColor=${ZWE_configs_apiml_catalog_customStyle_headerColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_headerColor:-}} \ + -Dapiml.catalog.customStyle.textColor=${ZWE_configs_apiml_catalog_customStyle_textColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_textColor:-}} \ + -Dapiml.catalog.customStyle.docLink=${ZWE_configs_apiml_catalog_customStyle_docLink:-${ZWE_components_apicatalog_apiml_catalog_customStyle_docLink:-}} \ -jar "${JAR_FILE}" & pid=$! diff --git a/apiml-security-common/build.gradle b/apiml-security-common/build.gradle index 11ea7dd0d0..857804ec41 100644 --- a/apiml-security-common/build.gradle +++ b/apiml-security-common/build.gradle @@ -4,6 +4,7 @@ dependencies { implementation libs.spring.boot.starter.web implementation libs.spring.boot.starter.security implementation libs.reactor + implementation libs.spring.webflux implementation libs.apache.commons.lang3 implementation libs.http.client5 diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java index d0c74586d0..9510325b1b 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AbstractExceptionHandler.java @@ -38,7 +38,7 @@ public abstract class AbstractExceptionHandler { * @param ex Exception to be handled * @throws ServletException Fallback exception if exception cannot be handled */ - public abstract void handleException(String requestUri, BiConsumer function, BiConsumer addHeader, RuntimeException ex) throws ServletException; + public abstract void handleException(String requestUri, BiConsumer function, BiConsumer addHeader, Exception ex) throws ServletException; /** * Write message (by message key) to http response diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java index 4b04dd65d4..7ae4bfc832 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java @@ -14,30 +14,27 @@ import jakarta.servlet.ServletException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; -import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import org.springframework.web.reactive.resource.NoResourceFoundException; import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.security.common.token.InvalidTokenTypeException; -import org.zowe.apiml.security.common.token.NoMainframeIdentityException; -import org.zowe.apiml.security.common.token.TokenExpireException; -import org.zowe.apiml.security.common.token.TokenFormatNotValidException; -import org.zowe.apiml.security.common.token.TokenNotProvidedException; -import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.product.gateway.GatewayNotAvailableException; +import org.zowe.apiml.security.common.token.*; import java.util.Map; import java.util.function.BiConsumer; -import static java.util.Map.entry; - /** * Exception handler deals with exceptions (methods listed below) that are thrown during the authentication process */ @@ -64,11 +61,15 @@ private static class HandlerContext { } @FunctionalInterface - private interface ExceptionHandler { - void handle(RuntimeException ex, HandlerContext ctx); + private interface ExceptionHandler { + void handle(E ex, HandlerContext ctx); + } + + private Map.Entry, ExceptionHandler> entry(Class clazz, ExceptionHandler handler) { + return Map.entry(clazz, handler); } - private final Map, ExceptionHandler> exceptionHandlers = Map.ofEntries( + private final Map, ExceptionHandler> exceptionHandlers = Map.ofEntries( entry(InsufficientAuthenticationException.class, (ex, ctx) -> handleAuthenticationRequired(ctx.requestUri, ctx.function, ctx.addHeader, ex)), entry(BadCredentialsException.class, @@ -80,7 +81,7 @@ private interface ExceptionHandler { entry(TokenNotValidException.class, (ex, ctx) -> handleTokenNotValid(ctx.requestUri, ctx.function, ctx.addHeader, ex)), entry(NoMainframeIdentityException.class, - (ex, ctx) -> handleNoMainframeIdentity(ctx.requestUri, ctx.function, ctx.addHeader, (NoMainframeIdentityException) ex)), + (ex, ctx) -> handleNoMainframeIdentity(ctx.requestUri, ctx.function, ctx.addHeader, ex)), entry(TokenNotProvidedException.class, (ex, ctx) -> handleTokenNotProvided(ctx.requestUri, ctx.function, ex)), entry(TokenExpireException.class, @@ -94,21 +95,35 @@ private interface ExceptionHandler { entry(InvalidCertificateException.class, (ex, ctx) -> handleInvalidCertificate(ctx.function, ex)), entry(ZosAuthenticationException.class, - (ex, ctx) -> handleZosAuthenticationException(ctx.function, (ZosAuthenticationException) ex)), + (ex, ctx) -> handleZosAuthenticationException(ctx.function, ex)), entry(InvalidTokenTypeException.class, (ex, ctx) -> handleInvalidTokenTypeException(ctx.requestUri, ctx.function, ex)), - entry(AuthenticationServiceException.class, - (ex, ctx) -> handleAuthenticationException(ctx.requestUri, ctx.function, ex)), entry(AuthenticationException.class, (ex, ctx) -> handleAuthenticationException(ctx.requestUri, ctx.function, ex)), entry(ServiceNotAccessibleException.class, - (ex, ctx) -> handleServiceNotAccessibleException(ctx.requestUri, ctx.function, ex)) + (ex, ctx) -> handleServiceNotAccessibleException(ctx.requestUri, ctx.function, ex)), + entry(NoResourceFoundException.class, + (ex, ctx) -> handleNoResourceFoundException(ctx.function, ex)), + entry(RuntimeException.class, + (ex, ctx) -> handleRuntimeException(ctx.requestUri, ctx.function, ex)), + entry(WebClientResponseException.BadRequest.class, + (ex, ctx) -> handleBadRequest(ctx.requestUri, ctx.function, ex, "org.zowe.apiml.security.login.invalidInput")), + entry(AccessDeniedException.class, + (ex, ctx) -> handleForbidden(ctx.function, ex) + ), + entry(GatewayNotAvailableException.class, + (ex, ctx) -> handleGatewayNotAvailable(ctx.function, ex) + ) ); - private ExceptionHandler resolveHandler(RuntimeException ex) { + private ExceptionHandler resolveHandler(E ex) { Class exClass = ex.getClass(); - while (exClass != null && RuntimeException.class.isAssignableFrom(exClass)) { - ExceptionHandler handler = exceptionHandlers.get(exClass); + while (exClass != null) { + if (!applicationInfo.isModulith() && exClass == RuntimeException.class) { + return null; + } + + ExceptionHandler handler = (ExceptionHandler) exceptionHandlers.get(exClass); if (handler != null) { return handler; } @@ -132,10 +147,10 @@ private ExceptionHandler resolveHandler(RuntimeException ex) { public void handleException(String requestUri, BiConsumer function, BiConsumer addHeader, - RuntimeException ex) throws ServletException { + Exception ex) throws ServletException { HandlerContext ctx = new HandlerContext(requestUri, function, addHeader); - ExceptionHandler handler = resolveHandler(ex); + ExceptionHandler handler = resolveHandler(ex); if (handler != null) { handler.handle(ex, ctx); @@ -146,7 +161,7 @@ public void handleException(String requestUri, throw new ServletException(ex); } - handleAuthenticationException(requestUri, function, ex); + handleUnknownHandler(requestUri, function, ex); } private void handleZosAuthenticationException(BiConsumer function, ZosAuthenticationException ex) { @@ -155,31 +170,31 @@ private void handleZosAuthenticationException(BiConsumer function, BiConsumer addHeader, RuntimeException ex) { + private void handleAuthenticationRequired(String requestUri, BiConsumer function, BiConsumer addHeader, InsufficientAuthenticationException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); String error = this.messageService.createMessage("org.zowe.apiml.zaas.security.schema.missingAuthentication").mapToLogMessage(); addHeader.accept(ApimlConstants.AUTH_FAIL_HEADER, error); writeErrorResponse(ErrorType.AUTH_REQUIRED.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, function, requestUri); } - private void handleBadCredentials(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleBadCredentials(String requestUri, BiConsumer function, BadCredentialsException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); writeErrorResponse(ErrorType.BAD_CREDENTIALS.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, function, requestUri); } - private void handleAuthenticationCredentialsNotFound(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleAuthenticationCredentialsNotFound(String requestUri, BiConsumer function, AuthenticationCredentialsNotFoundException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.BAD_REQUEST.value(), ex.getMessage()); writeErrorResponse(ErrorType.AUTH_CREDENTIALS_NOT_FOUND.getErrorMessageKey(), HttpStatus.BAD_REQUEST, function, requestUri); } - private void handleAuthMethodNotSupported(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleAuthMethodNotSupported(String requestUri, BiConsumer function, AuthMethodNotSupportedException ex) { final HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; log.debug(MESSAGE_FORMAT, status.value(), ex.getMessage()); final ApiMessageView message = messageService.createMessage(ErrorType.METHOD_NOT_ALLOWED.getErrorMessageKey(), ex.getMessage(), requestUri).mapToView(); function.accept(message, status); } - private void handleTokenNotValid(String requestUri, BiConsumer function, BiConsumer addHeader, RuntimeException ex) { + private void handleTokenNotValid(String requestUri, BiConsumer function, BiConsumer addHeader, TokenNotValidException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); String error = this.messageService.createMessage("org.zowe.apiml.common.unauthorized").mapToLogMessage(); addHeader.accept(ApimlConstants.AUTH_FAIL_HEADER, error); @@ -193,27 +208,27 @@ private void handleNoMainframeIdentity(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleTokenNotProvided(String requestUri, BiConsumer function, TokenNotProvidedException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); writeErrorResponse(ErrorType.TOKEN_NOT_PROVIDED.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, function, requestUri); } - private void handleTokenExpire(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleTokenExpire(String requestUri, BiConsumer function, TokenExpireException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); writeErrorResponse(ErrorType.TOKEN_EXPIRED.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, function, requestUri); } - private void handleInvalidCertificate(BiConsumer function, RuntimeException ex) { + private void handleInvalidCertificate(BiConsumer function, InvalidCertificateException ex) { function.accept(null, HttpStatus.FORBIDDEN); log.debug(MESSAGE_FORMAT, HttpStatus.FORBIDDEN.value(), ex.getMessage()); } - private void handleTokenFormatException(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleTokenFormatException(String requestUri, BiConsumer function, TokenFormatNotValidException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.BAD_REQUEST.value(), ex.getMessage()); writeErrorResponse(ErrorType.TOKEN_NOT_VALID.getErrorMessageKey(), HttpStatus.BAD_REQUEST, function, requestUri); } - private void handleInvalidTokenTypeException(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleInvalidTokenTypeException(String requestUri, BiConsumer function, InvalidTokenTypeException ex) { log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); writeErrorResponse(ErrorType.INVALID_TOKEN_TYPE.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, function, requestUri); } @@ -223,18 +238,47 @@ private void handleBadRequest(String requestUri, BiConsumer function, RuntimeException ex) { + private void handleAuthenticationException(String uri, BiConsumer function, AuthenticationException ex) { final ApiMessageView message = messageService.createMessage(ErrorType.AUTH_GENERAL.getErrorMessageKey(), ex.getMessage(), uri).mapToView(); final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; log.debug(MESSAGE_FORMAT, status.value(), ex.getMessage()); function.accept(message, status); } - private void handleServiceNotAccessibleException(String uri, BiConsumer function, RuntimeException ex) { + private void handleUnknownHandler(String uri, BiConsumer function, Exception ex) { + // TODO: it should be a general message, this is just for back-compatibility + final ApiMessageView message = messageService.createMessage(ErrorType.AUTH_GENERAL.getErrorMessageKey(), ex.getMessage(), uri).mapToView(); + final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + log.debug(MESSAGE_FORMAT, status.value(), ex.getMessage()); + function.accept(message, status); + } + + private void handleServiceNotAccessibleException(String uri, BiConsumer function, ServiceNotAccessibleException ex) { final ApiMessageView message = messageService.createMessage(ErrorType.SERVICE_UNAVAILABLE.getErrorMessageKey(), ex.getMessage(), uri).mapToView(); final HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE; log.debug(MESSAGE_FORMAT, status.value(), ex.getMessage()); function.accept(message, status); } + private void handleNoResourceFoundException(BiConsumer function, NoResourceFoundException ex) { + log.debug(MESSAGE_FORMAT, HttpStatus.NOT_FOUND.value(), ex.getMessage()); + writeErrorResponse("org.zowe.apiml.common.notFound", HttpStatus.NOT_FOUND, function); + } + + private void handleRuntimeException(String uri, BiConsumer function, RuntimeException ex) { + log.debug(MESSAGE_FORMAT, HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage()); + writeErrorResponse("org.zowe.apiml.common.internalRequestError", HttpStatus.INTERNAL_SERVER_ERROR, function, uri, ExceptionUtils.getMessage(ex), ExceptionUtils.getRootCauseMessage(ex)); + } + + private void handleForbidden(BiConsumer function, AccessDeniedException ex) { + log.debug(MESSAGE_FORMAT, HttpStatus.FORBIDDEN.value(), ex.getMessage()); + writeErrorResponse("org.zowe.apiml.security.forbidden", HttpStatus.FORBIDDEN, function); + } + + private void handleGatewayNotAvailable(BiConsumer function, GatewayNotAvailableException ex) { + log.debug(MESSAGE_FORMAT, HttpStatus.SERVICE_UNAVAILABLE.value(), ex.getMessage()); + writeErrorResponse("org.zowe.apiml.security.gatewayNotAvailable", HttpStatus.SERVICE_UNAVAILABLE, function); + + } + } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ResourceAccessExceptionHandler.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ResourceAccessExceptionHandler.java index 16784d58dd..dd9322cc7f 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ResourceAccessExceptionHandler.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ResourceAccessExceptionHandler.java @@ -41,14 +41,16 @@ public ResourceAccessExceptionHandler(MessageService messageService, ObjectMappe * @throws RuntimeException Fallback exception if exception cannot be handled */ @Override - public void handleException(String requestUri, BiConsumer function, BiConsumer addHeader, RuntimeException ex) { + public void handleException(String requestUri, BiConsumer function, BiConsumer addHeader, Exception ex) { ErrorType errorType; if (ex instanceof GatewayNotAvailableException) { errorType = ErrorType.GATEWAY_NOT_AVAILABLE; } else if (ex instanceof ServiceNotAccessibleException) { errorType = ErrorType.SERVICE_UNAVAILABLE; + } else if (ex instanceof RuntimeException re) { + throw re; } else { - throw ex; + throw new RuntimeException(ex); } log.debug(MESSAGE_FORMAT, HttpStatus.SERVICE_UNAVAILABLE.value(), ex.getMessage()); diff --git a/apiml/.gitignore b/apiml/.gitignore index c2065bc262..4936402ee1 100644 --- a/apiml/.gitignore +++ b/apiml/.gitignore @@ -35,3 +35,12 @@ out/ ### VS Code ### .vscode/ + +## Caching service infinispan artifacts + + +# created by infinispan during build +index +*.lck +ispn12.* +*.state diff --git a/apiml/build.gradle b/apiml/build.gradle index 0700251866..10bf995687 100644 --- a/apiml/build.gradle +++ b/apiml/build.gradle @@ -59,6 +59,7 @@ dependencies { api project(":discovery-service") api project(":zaas-service") api project(":caching-service") + api project(":api-catalog-services") implementation libs.bundles.modulith implementation libs.spring.boot.starter.web diff --git a/apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java b/apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java index d766fe80cc..79d396265f 100644 --- a/apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java +++ b/apiml/src/main/java/org/zowe/apiml/CachingServiceEurekaInstanceConfigBean.java @@ -10,17 +10,22 @@ package org.zowe.apiml; +import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.commons.util.InetUtils; -import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; import org.springframework.stereotype.Component; +import java.util.HashMap; +import java.util.Map; + @Component @ConfigurationProperties("apiml.caching.eureka.instance") -public class CachingServiceEurekaInstanceConfigBean extends EurekaInstanceConfigBean { +@Data +public class CachingServiceEurekaInstanceConfigBean { - public CachingServiceEurekaInstanceConfigBean(InetUtils inetUtils) { - super(inetUtils); - } + /** + * Gets the metadata name/value pairs associated with this instance. This information + * is sent to eureka server and can be used by other instances. + */ + private Map metadataMap = new HashMap<>(); } diff --git a/apiml/src/main/java/org/zowe/apiml/CatalogEurekaInstanceConfigBean.java b/apiml/src/main/java/org/zowe/apiml/CatalogEurekaInstanceConfigBean.java new file mode 100644 index 0000000000..e7c6169ddd --- /dev/null +++ b/apiml/src/main/java/org/zowe/apiml/CatalogEurekaInstanceConfigBean.java @@ -0,0 +1,31 @@ +/* + * 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; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@ConfigurationProperties("apiml.catalog.eureka.instance") +@Data +public class CatalogEurekaInstanceConfigBean { + + /** + * Gets the metadata name/value pairs associated with this instance. This information + * is sent to eureka server and can be used by other instances. + */ + private Map metadataMap = new HashMap<>(); + +} diff --git a/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java b/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java index c03cb7f680..dded0e7b94 100644 --- a/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java +++ b/apiml/src/main/java/org/zowe/apiml/GatewayEurekaInstanceConfigBean.java @@ -9,6 +9,7 @@ */ package org.zowe.apiml; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.commons.util.InetUtils; import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; diff --git a/apiml/src/main/java/org/zowe/apiml/GatewaySecurityApi.java b/apiml/src/main/java/org/zowe/apiml/GatewaySecurityApi.java new file mode 100644 index 0000000000..a611ca8596 --- /dev/null +++ b/apiml/src/main/java/org/zowe/apiml/GatewaySecurityApi.java @@ -0,0 +1,76 @@ +/* + * 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; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Primary; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.zowe.apiml.security.client.service.GatewaySecurity; +import org.zowe.apiml.security.common.login.LoginRequest; +import org.zowe.apiml.security.common.token.OIDCProvider; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.zaas.security.config.CompoundAuthProvider; +import org.zowe.apiml.zaas.security.service.AuthenticationService; + +import java.util.Optional; + +import static org.zowe.apiml.security.common.error.ErrorType.TOKEN_NOT_VALID; + +@Service +@Primary +@RequiredArgsConstructor +public class GatewaySecurityApi implements GatewaySecurity { + + private final CompoundAuthProvider compoundAuthProvider; + private final AuthenticationService authenticationService; + @Nullable + private final OIDCProvider oidcProvider; + + @Override + public Optional login(String username, char[] password, char[] newPassword) { + if (StringUtils.isBlank(username) || ArrayUtils.isEmpty(password)) { + throw new AuthenticationCredentialsNotFoundException("Username or password not provided."); + } + + var loginRequest = new LoginRequest(username, password, newPassword); + Authentication authentication = new UsernamePasswordAuthenticationToken(username, loginRequest); + authentication = compoundAuthProvider.authenticate(authentication); + if (authentication.isAuthenticated() ) { + return Optional.ofNullable((String) authentication.getCredentials()); + } + return Optional.empty(); + } + + @Override + public QueryResponse query(String token) { + var authentication = authenticationService.validateJwtToken(token); + if (authentication.isAuthenticated()) { + return authenticationService.parseJwtToken(token); + } + throw new TokenNotValidException(TOKEN_NOT_VALID.getDefaultMessage()); + } + + @Override + public QueryResponse verifyOidc(String token) { + if (oidcProvider != null && oidcProvider.isValid(token)) { + return new QueryResponse(); + } + throw new TokenNotValidException(TOKEN_NOT_VALID.getDefaultMessage()); + } + +} diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index f54cad1585..a2f35e4414 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -70,8 +70,9 @@ public class ModulithConfig { private final ApplicationContext applicationContext; private final Map instances = new HashMap<>(); private final GatewayEurekaInstanceConfigBean eurekaInstanceGw; + private final CatalogEurekaInstanceConfigBean catalogEurekaInstanceConfigBean; private final EurekaClientConfig eurekaConfig; - private final CachingServiceEurekaInstanceConfigBean eurekaInstanceCaching; + private final CachingServiceEurekaInstanceConfigBean cachingServiceEurekaInstanceConfigBean; private final Timer timer = new Timer("PeerReplicated-StaticServices"); @@ -105,23 +106,20 @@ private InstanceInfo getInstanceInfo(String serviceId) { var scheme = https ? "https" : "http"; - Map metadata = new HashMap<>(); - switch (serviceId) { - case "gateway": - metadata = eurekaInstanceGw.getMetadataMap(); - metadata.put("management.port", "10010"); - break; - case "cachingservice": - metadata = eurekaInstanceCaching.getMetadataMap(); - break; - default: - } + Map metadata = switch (serviceId) { + case "gateway" -> eurekaInstanceGw.getMetadataMap(); + case "cachingservice" -> cachingServiceEurekaInstanceConfigBean.getMetadataMap(); + case "apicatalog" -> catalogEurekaInstanceConfigBean.getMetadataMap(); + default -> new HashMap<>(); + }; + + String homePagePath = metadata.getOrDefault("apiml.homePagePath", "/"); return InstanceInfo.Builder.newBuilder() .setInstanceId(String.format("%s:%s:%d", hostname, serviceId, port)) .setAppName(serviceId) .setHostName(hostname) - .setHomePageUrl(null, String.format("%s://%s:%d", scheme, hostname, port)) + .setHomePageUrl(null, String.format("%s://%s:%d%s", scheme, hostname, port, homePagePath)) .setStatus(InstanceInfo.InstanceStatus.UP) .setIPAddr(ipAddress) .setPort(port) @@ -150,14 +148,15 @@ void createLocalInstances() { instances.put(CoreService.GATEWAY.getServiceId(), getInstanceInfo(CoreService.GATEWAY.getServiceId())); instances.put(CoreService.DISCOVERY.getServiceId(), getInstanceInfo(CoreService.DISCOVERY.getServiceId())); instances.put(CoreService.CACHING.getServiceId(), getInstanceInfo(CoreService.CACHING.getServiceId())); + instances.put(CoreService.API_CATALOG.getServiceId(), getInstanceInfo(CoreService.API_CATALOG.getServiceId())); EurekaServerContextHolder.initialize(applicationContext.getBean(EurekaServerContext.class)); - } - @EventListener(ApplicationReadyEvent.class) - public void onApplicationStart() { ApimlInstanceRegistry registry = getRegistry(); instances.forEach((key, value) -> registry.registerStatically(instances.get(key), false, CoreService.GATEWAY.getServiceId().equalsIgnoreCase(key))); + } + @EventListener(ApplicationReadyEvent.class) + public void onApplicationStart() { log.info("Initialize timer for static services peer-replicated heartbeats"); // This timer calls Eureka registry's peerReplicate method to accumulate all heartbeats of statically-onboarded services once @@ -165,7 +164,12 @@ public void onApplicationStart() { @Override public void run() { - registry.peerAwareHeartbeat(instances.get(CoreService.GATEWAY.getServiceId())); + var registry = getRegistry(); + if (registry != null) { + registry.peerAwareHeartbeat(instances.get(CoreService.GATEWAY.getServiceId())); + } else { + log.debug("Eureka registry is not available yet."); + } } }, eurekaConfig.getInstanceInfoReplicationIntervalSeconds() * 1000L, eurekaConfig.getInstanceInfoReplicationIntervalSeconds() * 1000L); @@ -252,6 +256,7 @@ MessageService messageService() { messageService.loadMessages("/discovery-log-messages.yml"); messageService.loadMessages("/gateway-log-messages.yml"); + messageService.loadMessages("/apicatalog-log-messages.yml"); messageService.loadMessages("/zaas-log-messages.yml"); 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 388938edbc..2ece9a3272 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ApimlExceptionHandler.java @@ -100,7 +100,7 @@ public Mono handlePassTicketException(ServerWebExchange exchange, PassTick @ExceptionHandler(BadCredentialsException.class) public Mono handleBadCredentialsException(ServerWebExchange exchange, BadCredentialsException ex) { log.debug("Bad credentials: {}", ex.getMessage()); - return setBodyResponse(exchange, SC_UNAUTHORIZED, "org.zowe.apiml.security.login.invalidCredentials"); + return setBodyResponse(exchange, SC_UNAUTHORIZED, "org.zowe.apiml.security.login.invalidCredentials", String.valueOf(exchange.getRequest().getPath())); } @ExceptionHandler(StorageException.class) diff --git a/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java b/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java index b22e2c139b..62db72bde5 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ReactiveAuthenticationController.java @@ -34,12 +34,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ServerWebExchange; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.security.common.login.LoginRequest; @@ -55,10 +50,7 @@ import java.io.IOException; import java.util.Objects; -import static org.apache.http.HttpStatus.SC_BAD_REQUEST; -import static org.apache.http.HttpStatus.SC_OK; -import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; -import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.http.HttpStatus.*; @@ -115,6 +107,7 @@ public Mono> login(ServerWebExchange exchange, ServerHttp .map(SecurityContext::getAuthentication) .filter(Objects::nonNull) .filter(Authentication::isAuthenticated) + .filter(TokenAuthentication.class::isInstance) .map(authentication -> replyWithJwt(exchange, authentication)) .switchIfEmpty(Mono.>defer(() -> this.authWithBody(exchange, request))) .switchIfEmpty(Mono.just(ResponseEntity.status(HttpStatusCode.valueOf(401)).build())); diff --git a/apiml/src/main/java/org/zowe/apiml/filter/LogoutHandler.java b/apiml/src/main/java/org/zowe/apiml/filter/LogoutHandler.java index 1e38a792b9..083969cd82 100644 --- a/apiml/src/main/java/org/zowe/apiml/filter/LogoutHandler.java +++ b/apiml/src/main/java/org/zowe/apiml/filter/LogoutHandler.java @@ -28,6 +28,7 @@ import reactor.core.publisher.Mono; import static org.zowe.apiml.constants.ApimlConstants.BEARER_AUTHENTICATION_PREFIX; +import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; @RequiredArgsConstructor @Component @@ -44,6 +45,12 @@ public Mono logout(WebFilterExchange exchange, Authentication authenticati } private Mono invalidateJwtToken(String token, WebFilterExchange exchange) { + if (exchange.getExchange().getRequest().getCookies().getFirst(COOKIE_AUTH_NAME) != null) { + exchange.getExchange().getResponse().addCookie( + httpUtils.createResponseCookieRemoval() + ); + } + if (Boolean.TRUE.equals(authenticationService.isInvalidated(token))) { return failure.onAuthenticationFailure(exchange,new TokenNotValidException("The token you are trying to logout is not valid")); } else { diff --git a/apiml/src/main/java/org/zowe/apiml/util/HttpUtils.java b/apiml/src/main/java/org/zowe/apiml/util/HttpUtils.java index 07a160c2ab..4dc7e68457 100644 --- a/apiml/src/main/java/org/zowe/apiml/util/HttpUtils.java +++ b/apiml/src/main/java/org/zowe/apiml/util/HttpUtils.java @@ -20,8 +20,8 @@ import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import reactor.core.publisher.Mono; -import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; import static org.zowe.apiml.constants.ApimlConstants.BEARER_AUTHENTICATION_PREFIX; +import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; @Component @RequiredArgsConstructor @@ -33,7 +33,7 @@ public class HttpUtils { private int cookieMaxAge = -1; @PostConstruct - void readConfig() { + protected void readConfig() { cp = authConfigurationProperties.getCookieProperties(); if (cp.getCookieMaxAge() != null) { cookieMaxAge = cp.getCookieMaxAge(); @@ -50,6 +50,16 @@ public ResponseCookie createResponseCookie(String jwt) { .build(); } + public ResponseCookie createResponseCookieRemoval() { + return ResponseCookie.from(cp.getCookieName()) + .path(cp.getCookiePath()) + .sameSite(cp.getCookieSameSite().getValue()) + .maxAge(0L) + .httpOnly(true) + .secure(cp.isCookieSecure()) + .build(); + } + public Mono getTokenFromRequest(ServerWebExchange exchange) { return getCookieValue(exchange, COOKIE_AUTH_NAME) .switchIfEmpty(getBearerTokenFromHeaderReactive(exchange)); diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 43c54fd390..6f1dcfe54d 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -57,10 +57,47 @@ springdoc: paths-to-match: /discovery/api/v1/** - group: zaas paths-to-match: /zaas/api/v1/** + - group: apicatalog + paths-to-match: /apicatalog/api/v1/** + apiml: catalog: serviceId: apicatalog + eureka: + instance: + metadata-map: + apiml: + apiBasePath: /apicatalog/api/v1 + homePagePath: /apicatalog + catalog: + tile: + id: apimediationlayer + title: API Mediation Layer API + description: The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation. + version: 1.0.0 + + routes: + ui-v1: + gatewayUrl: "/ui/v1" + serviceUrl: /apicatalog + api-v1: + gatewayUrl: "/api/v1" + serviceUrl: /apicatalog + + apiInfo: + - apiId: zowe.apiml.apicatalog + version: 1.0.0 + gatewayUrl: api/v1 + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/v3/api-docs/apicatalog + + service: + title: API Catalog + description: API Catalog service to display service details and API documentation for discovered API services. + + authentication: + sso: true + scheme: zoweJwt discovery: staticApiDefinitionsDirectories: config/local/api-defs # Not used in HTTPS mode and not applicable for Zowe @@ -112,6 +149,7 @@ apiml: externalUrl: ${apiml.service.externalUrl:${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}} authentication: sso: true + management.port: 10010 registry: enabled: false metadata-key-allow-list: zos.sysname,zos.system,zos.sysplex,zos.cpcName,zos.zosName,zos.lpar @@ -150,7 +188,7 @@ apiml: timeToLive: 10000 routing: instanceIdHeader: false - ignoredServices: discovery,zaas # to disable routing to the Discovery and ZAAS service + ignoredServices: discovery,zaas,apicatalog,cachingservice # to disable routing to the Discovery and ZAAS service + local services on Modulith service: apimlId: apiml1 corsEnabled: true diff --git a/apiml/src/test/java/org/zowe/apiml/GatewaySecurityApiTest.java b/apiml/src/test/java/org/zowe/apiml/GatewaySecurityApiTest.java new file mode 100644 index 0000000000..b74eba8446 --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/GatewaySecurityApiTest.java @@ -0,0 +1,180 @@ +/* + * 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; + +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.zowe.apiml.security.common.token.OIDCProvider; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.zaas.security.config.CompoundAuthProvider; +import org.zowe.apiml.zaas.security.service.AuthenticationService; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GatewaySecurityApiTest { + + @Mock + private CompoundAuthProvider compoundAuthProvider; + + @Mock + private AuthenticationService authenticationService; + + @Mock + private OIDCProvider oidcProvider; + + @InjectMocks + private GatewaySecurityApi gatewaySecurityApi; + + @Nested + class GiveValidAuthentication { + + Authentication authenticated; + + @BeforeEach + void setup() { + authenticated = mock(Authentication.class); + lenient().when(authenticated.isAuthenticated()).thenReturn(true); + } + + @Test + void whenLogin_andWhenCredentialsAreValid_thenShouldReturnToken() { + String expectedToken = "a-valid-jwt-token"; + when(authenticated.getCredentials()).thenReturn(expectedToken); + when(compoundAuthProvider.authenticate(any(Authentication.class))).thenReturn(authenticated); + + Optional tokenOptional = gatewaySecurityApi.login("user", "password".toCharArray(), null); + + assertTrue(tokenOptional.isPresent(), "Token should be present for a successful login"); + assertEquals(expectedToken, tokenOptional.get(), "The returned token does not match the expected token"); + verify(compoundAuthProvider).authenticate(any(UsernamePasswordAuthenticationToken.class)); + } + + @Test + void whenLogin_andWhenAuthenticationSucceedsButNoCredentials_thenShouldReturnEmpty() { + when(authenticated.getCredentials()).thenReturn(null); + when(compoundAuthProvider.authenticate(any(Authentication.class))).thenReturn(authenticated); + + Optional tokenOptional = gatewaySecurityApi.login("user", "password".toCharArray(), null); + + assertFalse(tokenOptional.isPresent(), "Token should not be present if credentials are not returned"); + } + } + + + @Test + void givenUsernameIsBlank_thenThrowException() { + var password = "password".toCharArray(); + assertThrows(AuthenticationCredentialsNotFoundException.class, () -> + gatewaySecurityApi.login(" ", password, null), + "Should throw AuthenticationCredentialsNotFoundException for blank username" + ); + } + + @Test + void givenPasswordIsEmpty_thenThrowException() { + assertThrows(AuthenticationCredentialsNotFoundException.class, () -> { + gatewaySecurityApi.login("user", new char[0], null); + }, "Should throw AuthenticationCredentialsNotFoundException for empty password"); + } + + @Nested + class GivenValidTokenAuthentication { + @Test + void thenReturnQueryResponse() { + var tokenAuthenticated = mock(TokenAuthentication.class); + when(tokenAuthenticated.isAuthenticated()).thenReturn(true); + + String validToken = "valid-jwt"; + QueryResponse expectedResponse = new QueryResponse(); // Assuming a default or populated response + when(authenticationService.validateJwtToken(validToken)).thenReturn(tokenAuthenticated); + when(authenticationService.parseJwtToken(validToken)).thenReturn(expectedResponse); + + QueryResponse actualResponse = gatewaySecurityApi.query(validToken); + + assertNotNull(actualResponse, "QueryResponse should not be null for a valid token"); + assertEquals(expectedResponse, actualResponse, "The returned QueryResponse does not match the expected one"); + verify(authenticationService).validateJwtToken(validToken); + verify(authenticationService).parseJwtToken(validToken); + } + } + + @Nested + class GivenInvalidTokenAuthentication { + @Test + void whenQuery_andTokenIsInvalid_thenThrowException() { + var tokenUnAuthenticated = mock(TokenAuthentication.class); + lenient().when(tokenUnAuthenticated.isAuthenticated()).thenReturn(false); + String invalidToken = "invalid-jwt"; + when(authenticationService.validateJwtToken(invalidToken)).thenReturn(tokenUnAuthenticated); + + assertThrows(TokenNotValidException.class, () -> { + gatewaySecurityApi.query(invalidToken); + }, "Should throw TokenNotValidException for an invalid token"); + verify(authenticationService, never()).parseJwtToken(anyString()); + } + + @Test + void whenLogin_andAuthenticationFails_thenDoNotReturnAuthentication() { + var unauthenticated = mock(Authentication.class); + lenient().when(unauthenticated.isAuthenticated()).thenReturn(false); + when(compoundAuthProvider.authenticate(any(Authentication.class))).thenReturn(unauthenticated); + + Optional tokenOptional = gatewaySecurityApi.login("user", "password".toCharArray(), null); + + assertFalse(tokenOptional.isPresent(), "Token should not be present for a failed authentication"); + } + } + + @Test + void whenVerifyOidc_andTokenIsValid_thenReturnResponse() { + String validOidcToken = "valid-oidc-token"; + when(oidcProvider.isValid(validOidcToken)).thenReturn(true); + + QueryResponse response = gatewaySecurityApi.verifyOidc(validOidcToken); + + assertNotNull(response, "QueryResponse should not be null for a valid OIDC token"); + } + + @Test + void whenVerifyOidc_andTokenIsInvalid_thenThrowException() { + String invalidOidcToken = "invalid-oidc-token"; + when(oidcProvider.isValid(invalidOidcToken)).thenReturn(false); + + assertThrows(TokenNotValidException.class, () -> { + gatewaySecurityApi.verifyOidc(invalidOidcToken); + }, "Should throw TokenNotValidException for an invalid OIDC token"); + } + + @Test + void whenVerifyOidc_andProviderIsNull_thenThrowException() { + GatewaySecurityApi noOidcApi = new GatewaySecurityApi(compoundAuthProvider, authenticationService, null); + String token = "any-token"; + + assertThrows(TokenNotValidException.class, () -> { + noOidcApi.verifyOidc(token); + }, "Should throw TokenNotValidException when OIDC provider is not configured"); + } +} diff --git a/apiml/src/test/java/org/zowe/apiml/filter/LogoutHandlerTest.java b/apiml/src/test/java/org/zowe/apiml/filter/LogoutHandlerTest.java index 4c19afb082..476273e13b 100644 --- a/apiml/src/test/java/org/zowe/apiml/filter/LogoutHandlerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/filter/LogoutHandlerTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; @@ -24,6 +25,7 @@ import org.springframework.security.web.server.WebFilterExchange; import org.springframework.web.server.WebFilterChain; import org.zowe.apiml.handler.FailedAuthenticationWebHandler; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.token.TokenFormatNotValidException; import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.util.HttpUtils; @@ -31,11 +33,12 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; @ExtendWith(MockitoExtension.class) class LogoutHandlerTest { @@ -43,7 +46,11 @@ class LogoutHandlerTest { @Mock private AuthenticationService authenticationService; @Mock private FailedAuthenticationWebHandler failureHandler; @Mock private PeerAwareInstanceRegistryImpl registry; - private HttpUtils httpUtils = new HttpUtils(null); + private HttpUtils httpUtils = new HttpUtils(new AuthConfigurationProperties()) { + { + readConfig(); + } + }; private LogoutHandler logoutHandler; @@ -130,4 +137,24 @@ void shouldInvalidateValidTokenSuccessfully() { verify(authenticationService).invalidateJwtTokenGateway(eq("token123"), eq(true), any()); } + @Test + void givenCookie_whenLogout_thenRemoveCookie() { + var request = MockServerHttpRequest.get("/logout") + .cookie(new HttpCookie(COOKIE_AUTH_NAME, "invalidated.jwt.token")) + .build(); + var exchange = MockServerWebExchange.from(request); + WebFilterChain mockChain = mock(WebFilterChain.class); + var webFilterExchange = new WebFilterExchange(exchange, mockChain); + + when(authenticationService.isInvalidated("invalidated.jwt.token")).thenReturn(false); + Applications mockApplications = mock(Applications.class); + when(registry.getApplications()).thenReturn(mockApplications); + + StepVerifier.create(logoutHandler.logout(webFilterExchange, mock(Authentication.class))).verifyComplete(); + + var cookie = webFilterExchange.getExchange().getResponse().getCookies().getFirst(COOKIE_AUTH_NAME); + assertNotNull(cookie); + assertEquals(0L, cookie.getMaxAge().getSeconds()); + } + } diff --git a/apiml/src/test/java/org/zowe/apiml/util/HttpUtilsTest.java b/apiml/src/test/java/org/zowe/apiml/util/HttpUtilsTest.java index dddaa287c9..6fb7e14805 100644 --- a/apiml/src/test/java/org/zowe/apiml/util/HttpUtilsTest.java +++ b/apiml/src/test/java/org/zowe/apiml/util/HttpUtilsTest.java @@ -103,4 +103,18 @@ void testCreateResponseCookie() { // test defaults assertTrue(cookie.isSecure()); } + @Test + void testCreateResponseCookieRemoval() { // test defaults + when(authProperties.getCookieProperties()).thenReturn(new CookieProperties()); + httpUtils.readConfig(); + ResponseCookie cookie = httpUtils.createResponseCookieRemoval(); + assertEquals("apimlAuthenticationToken", cookie.getName()); + assertEquals("", cookie.getValue()); + assertEquals("/", cookie.getPath()); + assertEquals("Strict", cookie.getSameSite()); + assertEquals(0, cookie.getMaxAge().getSeconds()); + assertTrue(cookie.isHttpOnly()); + assertTrue(cookie.isSecure()); + } + } diff --git a/common-service-core/build.gradle b/common-service-core/build.gradle index 5468ef7860..835d7efb3a 100644 --- a/common-service-core/build.gradle +++ b/common-service-core/build.gradle @@ -4,6 +4,7 @@ dependencies { implementation libs.eureka.jersey.client implementation libs.http.client5 implementation libs.guava + implementation libs.spring.cloud.commons compileOnly libs.eh.cache compileOnly libs.spring.boot.starter.web @@ -11,6 +12,7 @@ dependencies { testImplementation libs.eh.cache testImplementation libs.spring.boot.starter.test testImplementation libs.spring.boot.starter.web + testImplementation libs.spring.cloud.starter.eureka.client compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java index 741deeab35..771d30ff1a 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java +++ b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java @@ -15,6 +15,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.UserTokenHandler; import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.LaxRedirectStrategy; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; @@ -35,11 +36,7 @@ import java.io.File; import java.io.IOException; import java.net.URL; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; +import java.security.*; import java.security.cert.CertificateException; @@ -69,7 +66,9 @@ public CloseableHttpClient buildHttpClient(HttpClientConnectionManager connectio .setKeepAliveStrategy(ApimlKeepAliveStrategy.INSTANCE) .evictExpiredConnections() .evictIdleConnections(Timeout.ofSeconds(config.getIdleConnTimeoutSeconds())) - .disableAuthCaching().build(); + .disableAuthCaching() + .setRedirectStrategy(new LaxRedirectStrategy()) + .build(); } diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java index 1c687b1313..7b83e47217 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/EurekaUtils.java @@ -11,23 +11,29 @@ package org.zowe.apiml.util; import com.netflix.appinfo.InstanceInfo; +import lombok.experimental.UtilityClass; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.zowe.apiml.constants.EurekaMetadataDefinition; + +import java.util.Optional; + +import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; +import static org.zowe.apiml.product.constants.CoreService.GATEWAY; /** * This util offer basic operation with eureka, like: extraction serviceId from instanceId, construct URL by * InstanceInfo etc. */ -public final class EurekaUtils { - - private EurekaUtils() { - - } +@UtilityClass +public class EurekaUtils { /** * Extract serviceId from instanceId * @param instanceId input, instanceId in format "host:service:random number to unique instanceId" * @return second part, it means serviceId. If it doesn't exist return null; */ - public static final String getServiceIdFromInstanceId(String instanceId) { + public String getServiceIdFromInstanceId(String instanceId) { final int startIndex = instanceId.indexOf(':'); if (startIndex < 0) return null; @@ -42,7 +48,7 @@ public static final String getServiceIdFromInstanceId(String instanceId) { * @param instanceInfo Instance of service, for which we want to get an URL * @return URL to the instance */ - public static final String getUrl(InstanceInfo instanceInfo) { + public String getUrl(InstanceInfo instanceInfo) { if (instanceInfo.getSecurePort() == 0 || !instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE)) { return "http://" + instanceInfo.getHostName() + ":" + instanceInfo.getPort(); } else { @@ -50,4 +56,38 @@ public static final String getUrl(InstanceInfo instanceInfo) { } } + private Optional getPrimaryInstanceInfo(DiscoveryClient discoveryClient, String serviceId) { + return Optional.ofNullable(discoveryClient.getInstances(serviceId)) + .map(instances -> instances.stream() + .filter(instance -> EurekaMetadataDefinition.RegistrationType.of(instance.getMetadata()).isPrimary()) + .findFirst() + .orElse(null) + ); + } + + private Optional getSecondaryInstanceInfo(DiscoveryClient discoveryClient, String apimlId) { + return Optional.ofNullable(discoveryClient.getInstances(GATEWAY.getServiceId())) + .map(instances -> instances.stream() + .filter(instance -> EurekaMetadataDefinition.RegistrationType.of(instance.getMetadata()).isAdditional()) + .filter(instance -> apimlId.equals(instance.getMetadata().get(APIML_ID))) + .findFirst() + .orElse(null) + ); + } + + /** + * It tries to find service with primary registration, if it does not exist it looks also + * for a gateway with secondary registration and id is used as apimlId + * @param discoveryClient eureka client instance for look up + * @param id serviceId for primary or apimlId for secondary registration + * @return instance or empty Optional object + */ + public Optional getInstanceInfo(DiscoveryClient discoveryClient, String id) { + if (id == null) { + return Optional.empty(); + } + return getPrimaryInstanceInfo(discoveryClient, id) + .or(() -> getSecondaryInstanceInfo(discoveryClient, id)); + } + } diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java index b89ebbed9c..e5be5b03b6 100644 --- a/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java +++ b/common-service-core/src/test/java/org/zowe/apiml/util/EurekaUtilsTest.java @@ -11,12 +11,22 @@ package org.zowe.apiml.util; import com.netflix.appinfo.InstanceInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.REGISTRATION_TYPE; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.RegistrationType.ADDITIONAL; +import static org.zowe.apiml.product.constants.CoreService.GATEWAY; class EurekaUtilsTest { @@ -49,4 +59,57 @@ void testGetUrl() { assertEquals("https://locahost:443", EurekaUtils.getUrl(ii2)); } + @Nested + class PrimaryAndSecondaryRegistration { + + private static final String PRIMARY = "primary"; + private static final String SECONDARY = "secondary"; + + private DiscoveryClient discoveryClient; + + @BeforeEach + void init() { + discoveryClient = mock(DiscoveryClient.class); + + InstanceInfo instanceInfoPrimary = InstanceInfo.Builder.newBuilder() + .setAppName(PRIMARY) + .setInstanceId(String.format("x:%s:1", PRIMARY)) + .build(); + ServiceInstance serviceInstancePrimary = new EurekaServiceInstance(instanceInfoPrimary); + doReturn(Collections.singletonList(serviceInstancePrimary)).when(discoveryClient).getInstances(PRIMARY); + + InstanceInfo instanceInfoSecondary = InstanceInfo.Builder.newBuilder() + .setAppName(GATEWAY.getServiceId()) + .setInstanceId(String.format("x:%s:1", GATEWAY.getServiceId())) + .setMetadata(Map.of( + APIML_ID, SECONDARY, + REGISTRATION_TYPE, ADDITIONAL.getValue() + )) + .build(); + ServiceInstance serviceInstanceSecondary = new EurekaServiceInstance(instanceInfoSecondary); + doReturn(Collections.singletonList(serviceInstanceSecondary)).when(discoveryClient).getInstances(GATEWAY.getServiceId()); + } + + @Test + void givenPrimaryRegistration_whenGetInstanceInfo_thenReturnInstanceInfo() { + var instance = EurekaUtils.getInstanceInfo(discoveryClient, PRIMARY); + assertTrue(instance.isPresent()); + assertEquals(PRIMARY, instance.get().getServiceId().toLowerCase()); + } + + @Test + void givenSecondaryRegistration_whenGetInstanceInfo_thenReturnInstanceInfo() { + var instance = EurekaUtils.getInstanceInfo(discoveryClient, SECONDARY); + assertTrue(instance.isPresent()); + assertEquals(GATEWAY.getServiceId(), instance.get().getServiceId().toLowerCase()); + } + + @Test + void givenUnknownServiceId_whenGetInstanceInfo_thenReturnEmptyOptional() { + var instance = EurekaUtils.getInstanceInfo(discoveryClient, "unknown"); + assertTrue(instance.isEmpty()); + } + + } + } diff --git a/config/docker/api-catalog-services-standalone.yml b/config/docker/api-catalog-services-standalone.yml deleted file mode 100644 index 642e618722..0000000000 --- a/config/docker/api-catalog-services-standalone.yml +++ /dev/null @@ -1,41 +0,0 @@ -spring.profiles.include: diag,standalone - -logging: - level: - org.apache.http.impl.conn: INFO - -eureka: - client: - instanceInfoReplicationIntervalSeconds: 30 - registryFetchIntervalSeconds: 30 - instance: - leaseExpirationDurationInSeconds: 6 - leaseRenewalIntervalInSeconds: 1 - -server: - address: 0.0.0.0 - port: 10015 - ssl: - keyAlias: localhost - keyPassword: password - keyStore: /docker/all-services.keystore.p12 - keyStorePassword: password - keyStoreType: PKCS12 - clientAuth: want - trustStore: /docker/all-services.truststore.p12 - trustStorePassword: password - trustStoreType: PKCS12 - -apiml: - enabled: false - catalog: - standalone: - enabled: true - servicesDirectory: /docker/catalog-standalone-defs - banner: console - discovery: - staticApiDefinitionsDirectories: /api-defs;/docker/api-defs -spring: - output: - ansi: - enabled: always diff --git a/config/docker/api-defs/staticclient.yml b/config/docker/api-defs/staticclient.yml index 695a81788f..7df25b9083 100644 --- a/config/docker/api-defs/staticclient.yml +++ b/config/docker/api-defs/staticclient.yml @@ -25,7 +25,7 @@ services: apiInfo: - apiId: zowe.apiml.discoverableclient gatewayUrl: api/v1 - swaggerUrl: https://discoverable-client:10012/discoverableclient/v2/api-docs + swaggerUrl: https://discoverable-client:10012/discoverableclient/v3/api-docs customMetadata: apiml: okToRetryOnAllOperations: true diff --git a/config/docker/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json b/config/docker/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json deleted file mode 100644 index 7909654be6..0000000000 --- a/config/docker/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 1 - Instance 1", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/docker/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json b/config/docker/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json deleted file mode 100644 index 48e2b0c34a..0000000000 --- a/config/docker/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 1 - Instance 2", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json b/config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json deleted file mode 100644 index 960e09ffca..0000000000 --- a/config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 2 - v1 (default)", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json b/config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json deleted file mode 100644 index 6ad09d0a99..0000000000 --- a/config/docker/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 2 - v2", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/docker/catalog-standalone-defs/apiDocs/service3_instance1_org.zowe v1.0.0_default.json b/config/docker/catalog-standalone-defs/apiDocs/service3_instance1_org.zowe v1.0.0_default.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/config/docker/catalog-standalone-defs/apps/service1.json b/config/docker/catalog-standalone-defs/apps/service1.json deleted file mode 100644 index eac67cd1a7..0000000000 --- a/config/docker/catalog-standalone-defs/apps/service1.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "application": [ - { - "name": "SERVICE1-INSTANCE1", - "instance": [ - { - "hostName": "zowe.org", - "app": "service1-instance1", - "ipAddr": "1.1.1.1", - "status": "UP", - "port": { - "$": 1000, - "@enabled": "false" - }, - "securePort": { - "$": 1000, - "@enabled": "true" - }, - "metadata": { - "apiml.service.description": "Test mock application 1 / instance 1 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.api.version": "1.0.0", - "apiml.apiInfo.api.defaultApi": "false", - "apiml.apiInfo.api.gatewayUrl": "api", - "apiml.apiInfo.api.swaggerUrl": "/standAloneApps/swagger/openapi.json", - "apiml.apiInfo.api.apiId": "org.zowe", - "apiml.routes.api.serviceUrl": "", - "apiml.apiInfo.api.documentationUrl": "https://www.zowe.org", - "apiml.service.title": "Test mock application 1", - "apiml.catalog.tile.description": "Mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock1", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:1000" - } - ] - }, - { - "name": "SERVICE1-INSTANCE2", - "instance": [ - { - "hostName": "zowe.org", - "app": "service1-instance2", - "ipAddr": "1.1.1.2", - "status": "UP", - "port": { - "$": 2000, - "@enabled": "false" - }, - "securePort": { - "$": 2000, - "@enabled": "true" - }, - "metadata": { - "apiml.service.description": "Test mock application 1 / instance 2 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.api.version": "1.0.0", - "apiml.apiInfo.api.defaultApi": "true", - "apiml.apiInfo.api.gatewayUrl": "api", - "apiml.apiInfo.api.apiId": "org.zowe", - "apiml.routes.api.serviceUrl": "", - "apiml.apiInfo.api.documentationUrl": "https://www.zowe.org", - "apiml.service.title": "Test mock application 1", - "apiml.catalog.tile.description": "Mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock1", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:2000" - } - ] - } - ] -} diff --git a/config/docker/catalog-standalone-defs/apps/service2.json b/config/docker/catalog-standalone-defs/apps/service2.json deleted file mode 100644 index 625ba43e81..0000000000 --- a/config/docker/catalog-standalone-defs/apps/service2.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "application": [ - { - "name": "SERVICE2", - "instance": [ - { - "hostName": "zowe.org", - "app": "service2", - "ipAddr": "1.1.2.1", - "status": "UP", - "port": { - "$": 3000, - "@enabled": "false" - }, - "securePort": { - "$": 3000, - "@enabled": "true" - }, - "metadata": { - "apiml.service.description": "Test mock application 2 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.v1.version": "1.0.0", - "apiml.apiInfo.v1.defaultApi": "true", - "apiml.apiInfo.v1.gatewayUrl": "api", - "apiml.apiInfo.v1.swaggerUrl": "/services/swagger/openapi.json", - "apiml.apiInfo.v1.apiId": "org.zowe", - "apiml.apiInfo.v1.documentationUrl": "https://www.zowe.org", - "apiml.apiInfo.v2.version": "2.0.0", - "apiml.apiInfo.v2.defaultApi": "false", - "apiml.apiInfo.v2.gatewayUrl": "api", - "apiml.apiInfo.v2.swaggerUrl": "/services/swagger/openapi.json", - "apiml.apiInfo.v2.apiId": "org.zowe", - "apiml.apiInfo.v2.documentationUrl": "https://www.zowe.org", - "apiml.routes.api.serviceUrl": "", - "apiml.service.title": "Test mock application 2", - "apiml.catalog.tile.description": "Another mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock2", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Another mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:1000" - } - ] - } - ] -} diff --git a/config/docker/catalog-standalone-defs/apps/service3.json b/config/docker/catalog-standalone-defs/apps/service3.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/config/local/api-catalog-service.yml b/config/local/api-catalog-service.yml index f3a0360600..35501aaf96 100644 --- a/config/local/api-catalog-service.yml +++ b/config/local/api-catalog-service.yml @@ -1,5 +1,9 @@ spring.profiles.active: debug,diag +logging: + level: + web: debug + eureka: client: instanceInfoReplicationIntervalSeconds: 30 diff --git a/config/local/api-catalog-services-standalone.yml b/config/local/api-catalog-services-standalone.yml deleted file mode 100644 index 642e618722..0000000000 --- a/config/local/api-catalog-services-standalone.yml +++ /dev/null @@ -1,41 +0,0 @@ -spring.profiles.include: diag,standalone - -logging: - level: - org.apache.http.impl.conn: INFO - -eureka: - client: - instanceInfoReplicationIntervalSeconds: 30 - registryFetchIntervalSeconds: 30 - instance: - leaseExpirationDurationInSeconds: 6 - leaseRenewalIntervalInSeconds: 1 - -server: - address: 0.0.0.0 - port: 10015 - ssl: - keyAlias: localhost - keyPassword: password - keyStore: /docker/all-services.keystore.p12 - keyStorePassword: password - keyStoreType: PKCS12 - clientAuth: want - trustStore: /docker/all-services.truststore.p12 - trustStorePassword: password - trustStoreType: PKCS12 - -apiml: - enabled: false - catalog: - standalone: - enabled: true - servicesDirectory: /docker/catalog-standalone-defs - banner: console - discovery: - staticApiDefinitionsDirectories: /api-defs;/docker/api-defs -spring: - output: - ansi: - enabled: always diff --git a/config/local/api-defs-http/staticclient.yml b/config/local/api-defs-http/staticclient.yml index 22a5c0ad86..0f87493414 100644 --- a/config/local/api-defs-http/staticclient.yml +++ b/config/local/api-defs-http/staticclient.yml @@ -25,7 +25,7 @@ services: apiInfo: - apiId: zowe.apiml.discoverableclient gatewayUrl: api/v1 - swaggerUrl: http://localhost:10012/discoverableclient/v2/api-docs + swaggerUrl: http://localhost:10012/discoverableclient/v3/api-docs customMetadata: apiml: okToRetryOnAllOperations: true diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 15047cbce0..5f16df1b5f 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -25,7 +25,7 @@ services: apiInfo: - apiId: zowe.apiml.discoverableclient gatewayUrl: api/v1 - swaggerUrl: https://localhost:10012/discoverableclient/v2/api-docs + swaggerUrl: https://localhost:10012/discoverableclient/v3/api-docs customMetadata: apiml: okToRetryOnAllOperations: true diff --git a/config/local/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json b/config/local/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json deleted file mode 100644 index 7909654be6..0000000000 --- a/config/local/catalog-standalone-defs/apiDocs/service1-instance1_org.zowe v1.0.0_default.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 1 - Instance 1", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/local/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json b/config/local/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json deleted file mode 100644 index 48e2b0c34a..0000000000 --- a/config/local/catalog-standalone-defs/apiDocs/service1-instance2_org.zowe v1.0.0_default.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 1 - Instance 2", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json b/config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json deleted file mode 100644 index 960e09ffca..0000000000 --- a/config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v1.0.0_default.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 2 - v1 (default)", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json b/config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json deleted file mode 100644 index 6ad09d0a99..0000000000 --- a/config/local/catalog-standalone-defs/apiDocs/service2_org.zowe v2.0.0.json +++ /dev/null @@ -1,667 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Service 2 - v2", - "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "email": "apiteam@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.11" - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" - }, - "servers": [ - { - "url": "https://petstore3.swagger.io/api/v3" - } - ], - "tags": [ - { - "name": "pet", - "description": "Everything about your Pets", - "externalDocs": { - "description": "Find out more", - "url": "http://swagger.io" - } - }, - { - "name": "store", - "description": "Access to Petstore orders", - "externalDocs": { - "description": "Find out more about our store", - "url": "http://swagger.io" - } - }, - { - "name": "user", - "description": "Operations about user" - } - ], - "paths": { - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "Update an existing pet by Id", - "operationId": "updatePet", - "requestBody": { - "description": "Update an existent pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "Add a new pet to the store", - "operationId": "addPet", - "requestBody": { - "description": "Create a new pet in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "explode": true, - "schema": { - "type": "string", - "default": "available", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - }, - "application/xml": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "Place a new order in the store", - "operationId": "placeOrder", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "405": { - "description": "Invalid input" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", - "operationId": "getOrderById", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of order that needs to be fetched", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Order" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Order" - } - } - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "requestBody": { - "description": "Created user object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Update user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Update an existent user in the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "default": { - "description": "successful operation" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - } - }, - "components": { - "schemas": { - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "petId": { - "type": "integer", - "format": "int64", - "example": 198772 - }, - "quantity": { - "type": "integer", - "format": "int32", - "example": 7 - }, - "status": { - "type": "string", - "description": "Order Status", - "example": "approved", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "order" - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "name": { - "type": "string", - "example": "Dogs" - } - }, - "xml": { - "name": "category" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "username": { - "type": "string", - "example": "theUser" - }, - "password": { - "type": "string", - "example": "12345" - } - }, - "xml": { - "name": "user" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 10 - }, - "name": { - "type": "string", - "example": "doggie" - }, - "category": { - "$ref": "#/components/schemas/Category" - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "pet" - } - } - }, - "requestBodies": { - "Pet": { - "description": "Pet object that needs to be added to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "UserArray": { - "description": "List of user object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - }, - "securitySchemes": { - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - } -} diff --git a/config/local/catalog-standalone-defs/apps/service1.json b/config/local/catalog-standalone-defs/apps/service1.json deleted file mode 100644 index 3cdf3bf81f..0000000000 --- a/config/local/catalog-standalone-defs/apps/service1.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "application": [ - { - "name": "SERVICE1-INSTANCE1", - "instance": [ - { - "hostName": "zowe.org", - "app": "service1-instance1", - "ipAddr": "1.1.1.1", - "status": "UP", - "port": { - "$": 1000, - "@enabled": "false" - }, - "securePort": { - "$": 1000, - "@enabled": "true" - }, - "vipAddress": "service1", - "metadata": { - "apiml.service.description": "Test mock application 1 / instance 1 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.api.version": "1.0.0", - "apiml.apiInfo.api.defaultApi": "false", - "apiml.apiInfo.api.gatewayUrl": "api", - "apiml.apiInfo.api.swaggerUrl": "/standAloneApps/swagger/openapi.json", - "apiml.apiInfo.api.apiId": "org.zowe", - "apiml.routes.api.serviceUrl": "", - "apiml.apiInfo.api.documentationUrl": "https://www.zowe.org", - "apiml.service.title": "Test mock application 1", - "apiml.catalog.tile.description": "Mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock1", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:1000" - } - ] - }, - { - "name": "SERVICE1-INSTANCE2", - "instance": [ - { - "hostName": "zowe.org", - "app": "service1-instance2", - "ipAddr": "1.1.1.2", - "status": "UP", - "port": { - "$": 2000, - "@enabled": "false" - }, - "securePort": { - "$": 2000, - "@enabled": "true" - }, - "vipAddress": "service1", - "metadata": { - "apiml.service.description": "Test mock application 1 / instance 2 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.api.version": "1.0.0", - "apiml.apiInfo.api.defaultApi": "true", - "apiml.apiInfo.api.gatewayUrl": "api", - "apiml.apiInfo.api.apiId": "org.zowe", - "apiml.routes.api.serviceUrl": "", - "apiml.apiInfo.api.documentationUrl": "https://www.zowe.org", - "apiml.service.title": "Test mock application 1", - "apiml.catalog.tile.description": "Mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock1", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:2000" - } - ] - } - ] -} diff --git a/config/local/catalog-standalone-defs/apps/service2.json b/config/local/catalog-standalone-defs/apps/service2.json deleted file mode 100644 index 5cdc66e710..0000000000 --- a/config/local/catalog-standalone-defs/apps/service2.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "application": [ - { - "name": "SERVICE2", - "instance": [ - { - "hostName": "zowe.org", - "app": "service2", - "ipAddr": "1.1.2.1", - "status": "UP", - "port": { - "$": 3000, - "@enabled": "false" - }, - "securePort": { - "$": 3000, - "@enabled": "true" - }, - "vipAddress": "service2", - "metadata": { - "apiml.service.description": "Test mock application 2 - just for testing purposes", - "apiml.catalog.tile.version": "1.0.0", - "apiml.apiInfo.v1.version": "1.0.0", - "apiml.apiInfo.v1.defaultApi": "true", - "apiml.apiInfo.v1.gatewayUrl": "api", - "apiml.apiInfo.v1.swaggerUrl": "/services/swagger/openapi.json", - "apiml.apiInfo.v1.apiId": "org.zowe", - "apiml.apiInfo.v1.documentationUrl": "https://www.zowe.org", - "apiml.apiInfo.v2.version": "2.0.0", - "apiml.apiInfo.v2.defaultApi": "false", - "apiml.apiInfo.v2.gatewayUrl": "api", - "apiml.apiInfo.v2.swaggerUrl": "/services/swagger/openapi.json", - "apiml.apiInfo.v2.apiId": "org.zowe", - "apiml.apiInfo.v2.documentationUrl": "https://www.zowe.org", - "apiml.routes.api.serviceUrl": "", - "apiml.service.title": "Test mock application 2", - "apiml.catalog.tile.description": "Another mock apps from file", - "version": "1.0.0", - "apiml.routes.api.gatewayUrl": "api", - "apiml.routes.ui.gatewayUrl": "ui", - "apiml.catalog.tile.id": "mock2", - "apiml.authentication.scheme": "zosmf", - "apiml.catalog.tile.title": "Another mocked Services from file", - "apiml.routes.ui.serviceUrl": "" - }, - "homePageUrl": "https://www.zowe.org:1000" - } - ] - } - ] -} diff --git a/gateway-service/build.gradle b/gateway-service/build.gradle index 57de5f3203..5e2ccbcdb7 100644 --- a/gateway-service/build.gradle +++ b/gateway-service/build.gradle @@ -75,7 +75,9 @@ dependencies { implementation libs.spring.boot.starter.actuator implementation libs.spring.boot.starter.oauth2.client implementation libs.spring.boot.starter.thymeleaf - implementation libs.spring.doc.webflux + implementation(libs.spring.doc.webflux) { + exclude group: "jakarta.xml.bind", module: "jakarta.xml.bind-api" + } implementation libs.netty.reactor.http implementation libs.google.gson implementation libs.jjwt @@ -104,6 +106,7 @@ dependencies { testFixturesImplementation libs.lombok testFixturesImplementation libs.spring.cloud.starter.eureka.client testFixturesImplementation libs.guava + testFixturesImplementation libs.jaxbApi testFixturesAnnotationProcessor libs.lombok compileOnly libs.lombok diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java index ec722b756f..731d38d2b0 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java @@ -136,7 +136,7 @@ void updatePaths(OpenAPI openApi, String pathToMatch) { String download(URI uri) { return webClient - .get().uri(uri) + .get().uri(uri) .retrieve() .bodyToMono(String.class).share().block(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java index 7bb9f568ee..4b63fcc0bf 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java @@ -154,9 +154,13 @@ private int authorizationServiceCount() { } private String getCatalogLink(ServiceInstance catalogInstance) { - String gatewayUrl = catalogInstance.getMetadata().get(String.format(UI_V1_ROUTE, ROUTES, ROUTES_GATEWAY_URL)); - String serviceUrl = catalogInstance.getMetadata().get(String.format(UI_V1_ROUTE, ROUTES, ROUTES_SERVICE_URL)); - return serviceUrl + gatewayUrl; + if (applicationInfo.isModulith()) { + return "/apicatalog/ui/v1"; + } else { + String gatewayUrl = catalogInstance.getMetadata().get(String.format(UI_V1_ROUTE, ROUTES, ROUTES_GATEWAY_URL)); + String serviceUrl = catalogInstance.getMetadata().get(String.format(UI_V1_ROUTE, ROUTES, ROUTES_SERVICE_URL)); + return serviceUrl + gatewayUrl; + } } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 434494af3f..ac97301582 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -13,28 +13,28 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.product.gateway.GatewayClient; -import org.zowe.apiml.product.routing.RoutedServices; +import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.ServiceType; import org.zowe.apiml.product.routing.transform.TransformService; import org.zowe.apiml.product.routing.transform.URLTransformationException; import reactor.core.publisher.Mono; -import java.util.Map; -import java.util.stream.Stream; - -import static reactor.core.publisher.Mono.empty; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; /** * PageRedirectionFilterFactory is a Spring Cloud Gateway Filter Factory that adapts a response from a routed service @@ -45,68 +45,144 @@ @Slf4j public class PageRedirectionFilterFactory extends AbstractGatewayFilterFactory { + private static final String SLASH = "/"; + @Value("${server.attls.enabled:false}") private boolean isAttlsEnabled; + private static final EurekaMetadataParser EUREKA_METADATA_PARSER = new EurekaMetadataParser(); + + private final TransformService transformService; private final DiscoveryClient discoveryClient; - private final EurekaMetadataParser metadataParser; - private TransformService transformService; public PageRedirectionFilterFactory( - DiscoveryClient discoveryClient, - @Qualifier("getEurekaMetadataParser") EurekaMetadataParser metadataParser, - GatewayClient gatewayClient) { + GatewayClient gatewayClient, + DiscoveryClient discoveryClient + ) { super(Config.class); - this.discoveryClient = discoveryClient; - this.metadataParser = metadataParser; this.transformService = new TransformService(gatewayClient); + this.discoveryClient = discoveryClient; + } + + @Override + public GatewayFilter apply(Config config) { + Optional instance = discoveryClient.getInstances(config.serviceId).stream() + .filter(i -> config.getInstanceId().equalsIgnoreCase(i.getInstanceId())) + .findFirst(); + return (exchange, chain) -> chain.filter(exchange) + .then(Mono.defer(() -> processNewLocationUrl(exchange, config, instance))); } - private String getNewLocationUrl(Config config, String location) { - if (location == null) { - return ""; + private URI getHostUri(ServiceInstance instance) { + return UriComponentsBuilder.newInstance() + .host(instance.getHost()) + .port(instance.getPort()) + .build().toUri(); + } + + private URI getHostUri(URI uri) { + return UriComponentsBuilder.newInstance() + .host(uri.getHost()) + .port(uri.getPort()) + .build().toUri(); + } + + private Optional getInstance(URI locationUri, Optional instance) { + if (locationUri.getHost() == null) { + return instance; } - return ((Stream) discoveryClient.getInstances(config.serviceId).stream()) - .findAny() - .map(ServiceInstance.class::cast) - .map(serviceInstance -> { - Map metadata = serviceInstance.getMetadata(); - RoutedServices routes = metadataParser.parseRoutes(metadata); - try { - String newUrl = transformService.transformURL(ServiceType.ALL, StringUtils.toRootLowerCase(config.serviceId), location, routes, false); - if (isAttlsEnabled) { - newUrl = UriComponentsBuilder.fromUriString(newUrl).scheme("https").build().toUriString(); - } - return newUrl; - } catch (URLTransformationException e) { - log.debug("The URL for the redirect {} cannot be transformed: {}", location, e.getMessage()); - return ""; - } - } - ) - .orElse(""); + URI hostUri = getHostUri(locationUri); + if (instance.map(i -> getHostUri(i).equals(hostUri)).orElse(false)) { + return instance; + } + + return discoveryClient.getServices().stream() + .map(discoveryClient::getInstances) + .flatMap(List::stream) + .filter(i -> hostUri.equals(getHostUri(i))) + .findFirst(); } - @Override - public GatewayFilter apply(Config config) { - return (exchange, chain) -> chain.filter(exchange) - .then(processNewLocationUrl(exchange, config)); + private String normalizePath(String path) { + if (!path.startsWith(SLASH)) { + path = SLASH + path; + } + if (!path.endsWith(SLASH)) { + path = path + SLASH; + } + return path; } - private Mono processNewLocationUrl(ServerWebExchange exchange, Config config) { - return Mono.fromCallable(() -> { - var response = exchange.getResponse(); - if (response.getStatusCode().is3xxRedirection()) { - return getNewLocationUrl(config, response.getHeaders().getFirst(HttpHeaders.LOCATION)); + private boolean isMatching(RoutedService route, URI uri) { + var servicePath = normalizePath(route.getServiceUrl()); + var locationPath = normalizePath(uri.getPath()); + return locationPath.startsWith(servicePath); + } + + private Mono processNewLocationUrl(ServerWebExchange exchange, Config config, Optional instance) { + var response = exchange.getResponse(); + var isRedirect = Optional.ofNullable(response.getStatusCode()).map(HttpStatusCode::is3xxRedirection).orElse(false); + if (!isRedirect) { + return Mono.empty(); + } + + var location = response.getHeaders().getFirst(HttpHeaders.LOCATION); + if (StringUtils.isBlank(location)) { + return Mono.empty(); + } + + var locationUri = URI.create(location); + var targetInstance = getInstance(locationUri, instance); + var defaultRoute = config.getRoutedService(); + + AtomicReference newUrl = new AtomicReference<>(); + if (targetInstance == instance && isMatching(defaultRoute, locationUri)) { + // try the preferable route on the same instance (the same as in the original request) + try { + newUrl.set(transformService.transformURL( + StringUtils.toRootLowerCase(config.serviceId), + UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getQuery()).build().toUri().toString(), + defaultRoute, + false, + locationUri + )); + } catch (URLTransformationException e) { + log.debug("Cannot transform URL on the same route", e); + return Mono.empty(); } - return ""; - }).flatMap(newUrl -> { - if (StringUtils.isNotBlank(newUrl)) { - exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION, newUrl); + } + + if (newUrl.get() == null) { + // try to find a matching routing for the service instance + targetInstance.ifPresent(i -> { + var routes = EUREKA_METADATA_PARSER.parseRoutes(i.getMetadata()); + + try { + newUrl.set(transformService.transformURL( + ServiceType.ALL, + StringUtils.toRootLowerCase(config.serviceId), + location, + routes, + false + )); + } catch (URLTransformationException e) { + log.debug("Cannot transform URL", e); + } + }); + } + + if (newUrl.get() != null) { + // if the new URL was defined, decorate (scheme by AT-TLS) and set + if (isAttlsEnabled) { + newUrl.set(UriComponentsBuilder.fromUriString(newUrl.get()).scheme("https").build().toUriString()); } - return empty(); - }); + + exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION, newUrl.toString()); + } + + // in case url was not transformed leave it as it is (routing could be outside the Zowe) + return Mono.empty(); } @Data @@ -115,6 +191,13 @@ public static class Config { private String serviceId; private String instanceId; + private String gatewayUrl; + private String serviceUrl; + + RoutedService getRoutedService() { + return new RoutedService("used", gatewayUrl, serviceUrl); + } + } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509FilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509FilterFactory.java index d8f473c233..3d2a5a3588 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509FilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509FilterFactory.java @@ -78,13 +78,13 @@ private void setHeader(HttpHeaders headers, String[] headerNames, X509Certificat for (String headerName : headerNames) { switch (headerName.trim()) { case COMMON_NAME: - headers.add(COMMON_NAME, getCommonName(new LdapName(certificate.getSubjectDN().getName()))); + headers.add(COMMON_NAME, getCommonName(new LdapName(certificate.getSubjectX500Principal().getName()))); break; case PUBLIC_KEY: headers.add(PUBLIC_KEY, Base64.getEncoder().encodeToString(certificate.getEncoded())); break; case DISTINGUISHED_NAME: - headers.add(DISTINGUISHED_NAME, certificate.getSubjectDN().getName()); + headers.add(DISTINGUISHED_NAME, certificate.getSubjectX500Principal().getName()); break; default: log.debug("Unsupported header specified in service metadata, " + diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java index bdaab76932..482a1e546a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java @@ -98,7 +98,7 @@ static List join(List a, List b) { return output; } - List getPostRoutingFilters(ServiceInstance serviceInstance) { + List getPostRoutingFilters(ServiceInstance serviceInstance, RoutedService routedService) { List serviceRelated = new LinkedList<>(); if (forwardingClientCertEnabled && Optional.ofNullable(serviceInstance.getMetadata().get(SERVICE_SUPPORTING_CLIENT_CERT_FORWARDING)) @@ -137,6 +137,8 @@ List getPostRoutingFilters(ServiceInstance serviceInstance) { pageRedirectionFilter.setName("PageRedirectionFilterFactory"); pageRedirectionFilter.addArg("serviceId", serviceInstance.getServiceId()); pageRedirectionFilter.addArg("instanceId", serviceInstance.getInstanceId()); + pageRedirectionFilter.addArg("gatewayUrl", routedService.getGatewayUrl()); + pageRedirectionFilter.addArg("serviceUrl", routedService.getServiceUrl()); serviceRelated.add(pageRedirectionFilter); return join(commonFilters, serviceRelated); @@ -144,8 +146,7 @@ List getPostRoutingFilters(ServiceInstance serviceInstance) { private List getAuthFilterPerRoute( AtomicInteger orderHolder, - ServiceInstance serviceInstance, - List postRoutingFilters + ServiceInstance serviceInstance ) { Authentication auth = metadataParser.parseAuthentication(serviceInstance.getMetadata()); // iterate over routing definition (ordered from the longest one to match with the most specific) @@ -157,7 +158,7 @@ private List getAuthFilterPerRoute( // generate a new routing rule by a specific produces RouteDefinition routeDefinition = rdp.get(serviceInstance, routedService); routeDefinition.setOrder(orderHolder.getAndIncrement()); - routeDefinition.getFilters().addAll(postRoutingFilters); + routeDefinition.getFilters().addAll(getPostRoutingFilters(serviceInstance, routedService)); setAuth(serviceInstance, routeDefinition, auth); return routeDefinition; @@ -182,13 +183,13 @@ public Flux getRouteDefinitions() { // iterate over services return getServiceInstances().flatMap(Flux::fromIterable).map(serviceInstance -> // generate route definition per services and its routing rules - getAuthFilterPerRoute(order, serviceInstance, getPostRoutingFilters(serviceInstance)) + getAuthFilterPerRoute(order, serviceInstance) ) .flatMapIterable(list -> list); } private boolean filterIgnored(String serviceId) { - return !PatternMatchUtils.simpleMatch(ignoredServices, serviceId); + return !PatternMatchUtils.simpleMatch(ignoredServices, serviceId.toLowerCase()); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java index 40a8bf2970..539166d2b4 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java @@ -17,8 +17,8 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.gateway.MockService; -import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; +import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; import static io.restassured.RestAssured.given; import static org.apache.hc.core5.http.HttpStatus.SC_PERMANENT_REDIRECT; @@ -41,7 +41,7 @@ class ForwardedProxyHeadersTest extends AcceptanceTestWithMockServices { @BeforeEach void setUp() { var responseHeaders = new Headers(); - responseHeaders.add("Location", basePath + "/serviceid1/test2"); + responseHeaders.add("Location", "/serviceid1/test2"); mockService("serviceid1").scope(MockService.Scope.CLASS) .addEndpoint("/serviceid1/test") .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst("X-forwarded-prefix"))) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java index f701a04a15..cf26897c6c 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/caching/LoadBalancerCacheTest.java @@ -231,10 +231,13 @@ class WhenRetrieve { @Test void andSuccess_thenSuccess() { - var cacheRecord = new LoadBalancerCacheRecord("instance1"); + var expectedRecord = new LoadBalancerCacheRecord("instance1"); var key = "lb.anuser:aserviceid"; - when(map.get(key)).thenReturn(cacheRecord); - assertEquals(cacheRecord, loadBalancerCache.retrieve("anuser", "aserviceid").block()); + when(map.get(key)).thenReturn(expectedRecord); + StepVerifier.create(loadBalancerCache.retrieve("anuser", "aserviceid")) + .assertNext(retrievedRecord -> assertEquals(expectedRecord, retrievedRecord)) + .verifyComplete(); + verifyNoInteractions(cachingServiceClient); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/ForbidEncodedCharactersFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/ForbidEncodedCharactersFilterFactoryTest.java index 1b36d85227..c9266d8274 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/ForbidEncodedCharactersFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/ForbidEncodedCharactersFilterFactoryTest.java @@ -13,18 +13,23 @@ 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.junit.jupiter.MockitoExtension; import org.springframework.http.HttpMethod; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.test.util.ReflectionTestUtils; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import java.net.URI; import java.net.URISyntaxException; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@ExtendWith(MockitoExtension.class) class ForbidEncodedCharactersFilterFactoryTest { private static final String ENCODED_REQUEST_URI = "/api/v1/encoded;ch%25rs"; @@ -33,24 +38,26 @@ class ForbidEncodedCharactersFilterFactoryTest { private ForbidEncodedCharactersFilterFactory filter; @BeforeEach - public void setUp() { + void setUp() { filter = new ForbidEncodedCharactersFilterFactory(); } @Nested class Responses { + @Test void givenNormalRequestUri_whenFilterApply_thenSuccess() { - MockServerHttpRequest request = MockServerHttpRequest + var request = MockServerHttpRequest .get(NORMAL_REQUEST_URI) .build(); - MockServerWebExchange exchange = MockServerWebExchange.from(request); + var exchange = MockServerWebExchange.from(request); - var response = filter.apply("").filter(exchange, e -> { + var elapsed = StepVerifier.create(filter.apply("").filter(exchange, e -> { exchange.getResponse().setRawStatusCode(200); return Mono.empty(); - }); - response.block(); + })) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); assertTrue(exchange.getResponse().getStatusCode().is2xxSuccessful()); } @@ -66,6 +73,7 @@ void givenRequestUriWithEncodedCharacters_whenFilterApply_thenReturnBadRequest() var mono = filter.apply(""); assertThrows(ForbidCharacterException.class, () -> mono.filter(exchange, e -> Mono.empty())); } + } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/InMemoryRateLimiterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/InMemoryRateLimiterTest.java index 18b1d2e2d3..72a3b3eef9 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/InMemoryRateLimiterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/InMemoryRateLimiterTest.java @@ -12,21 +12,24 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter; -import reactor.core.publisher.Mono; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.test.StepVerifier; -import java.util.Objects; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.*; - -public class InMemoryRateLimiterTest { +@ExtendWith(MockitoExtension.class) +class InMemoryRateLimiterTest { private InMemoryRateLimiter rateLimiter; String userId = "testUser"; String routeId = "testRoute"; @BeforeEach - public void setUp() { + void setUp() { rateLimiter = new InMemoryRateLimiter(); rateLimiter.capacity = 3; rateLimiter.tokens = 3; @@ -34,53 +37,63 @@ public void setUp() { } @Test - public void isAllowed_shouldReturnTrue_whenTokensAvailable() { + void isAllowed_shouldReturnTrue_whenTokensAvailable() { rateLimiter.capacity = 1; - Mono response = rateLimiter.isAllowed(routeId, userId); - - assertTrue(Objects.requireNonNull(response.block()).isAllowed()); + var elapsed = StepVerifier.create(rateLimiter.isAllowed(routeId, userId)) + .assertNext(response -> assertTrue(response.isAllowed())) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); } @Test - public void isAllowed_shouldReturnFalse_whenTokensExhausted() { + void isAllowed_shouldReturnFalse_whenTokensExhausted() { for (int i = 0; i < rateLimiter.capacity; i++) { - Mono responseMono = rateLimiter.isAllowed(routeId, userId); - InMemoryRateLimiter.Response response = responseMono.block(); - assertTrue(response.isAllowed(), "Request " + (i + 1) + " should be allowed"); + var count = i; + + var elapsed = StepVerifier.create(rateLimiter.isAllowed(routeId, userId)) + .assertNext(response -> assertTrue(response.isAllowed(), "Request " + (count + 1) + " should be allowed")) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); } // Last request should be denied - Mono responseMono = rateLimiter.isAllowed(routeId, userId); - InMemoryRateLimiter.Response response = responseMono.block(); - assertFalse(response.isAllowed(), "Fourth request should not be allowed"); + var elapsed = StepVerifier.create(rateLimiter.isAllowed(routeId, userId)) + .assertNext(response -> assertFalse(response.isAllowed(), "Fourth request should not be allowed")) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); } - @Test - public void testDifferentClientIdHasSeparateBucket() { - String clientId1 = "client1"; - String clientId2 = "client2"; + void testDifferentClientIdHasSeparateBucket() { + var clientId1 = "client1"; + var clientId2 = "client2"; // Allow first three requests for client1 for (int i = 0; i < rateLimiter.capacity; i++) { - Mono responseMono = rateLimiter.isAllowed(routeId, clientId1); - InMemoryRateLimiter.Response response = responseMono.block(); - assertTrue(response.isAllowed(), "Request " + (i + 1) + " for client1 should be allowed"); + var count = i; + var elapsed = StepVerifier.create(rateLimiter.isAllowed(routeId, clientId1)) + .assertNext(response -> { + assertTrue(response.isAllowed(), "Request " + (count + 1) + " for client1 should be allowed"); + }) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); } // Fourth request for client1 should be denied - Mono responseMono = rateLimiter.isAllowed(routeId, clientId1); - InMemoryRateLimiter.Response response = responseMono.block(); - assertFalse(response.isAllowed(), "Fourth request for client1 should not be allowed"); + var elapsed = StepVerifier.create(rateLimiter.isAllowed(routeId, clientId1)) + .assertNext(response -> assertFalse(response.isAllowed(), "Fourth request for client1 should not be allowed")) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); // Allow first request for client2, it should be allowed since it's a separate bucket - Mono responseMono2 = rateLimiter.isAllowed(routeId, clientId2); - InMemoryRateLimiter.Response response2 = responseMono2.block(); - assertTrue(response2.isAllowed(), "First request for client2 should be allowed"); + elapsed = StepVerifier.create(rateLimiter.isAllowed(routeId, clientId2)) + .assertNext(response -> assertTrue(response.isAllowed(), "First request for client2 should be allowed")) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); } @Test - public void testNewConfig() { + void testNewConfig() { InMemoryRateLimiter.Config config = rateLimiter.newConfig(); assertNotNull(config, "Config should not be null"); @@ -90,7 +103,7 @@ public void testNewConfig() { } @Test - public void setNonNullParametersTest() { + void setNonNullParametersTest() { Integer newCapacity = 20; Integer newTokens = 20; Integer newRefillDuration = 2; @@ -101,10 +114,10 @@ public void setNonNullParametersTest() { } @Test - public void setParametersWithNullValuesTest() { + void setParametersWithNullValuesTest() { Integer newCapacity = 30; rateLimiter.setParameters(newCapacity, 0, 0); assertEquals(newCapacity, rateLimiter.capacity); - } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/KeyResolverTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/KeyResolverTest.java index a2ea042844..076695970e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/KeyResolverTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/KeyResolverTest.java @@ -12,51 +12,56 @@ 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.http.HttpCookie; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class KeyResolverTest { +@ExtendWith(MockitoExtension.class) +class KeyResolverTest { + + @Mock private ServerHttpRequest request; private KeyResolver keyResolver; private ServerWebExchange exchange; @BeforeEach - public void setUp() { + void setUp() { keyResolver = new KeyResolver(); exchange = mock(ServerWebExchange.class); ReflectionTestUtils.setField(keyResolver, "cookieName", "apimlAuthenticationToken"); } @Test - public void resolve_shouldReturnCookieValue_whenCookieIsPresent() { - ServerHttpRequest request = mock(ServerHttpRequest.class); + void resolve_shouldReturnCookieValue_whenCookieIsPresent() { HttpCookie cookie = new HttpCookie("apimlAuthenticationToken", "testToken"); when(exchange.getRequest()).thenReturn(request); var cookies = new LinkedMultiValueMap(); cookies.add("apimlAuthenticationToken", cookie); when(request.getCookies()).thenReturn(cookies); - Mono result = keyResolver.resolve(exchange); - assertEquals("testToken", result.block()); + StepVerifier.create(keyResolver.resolve(exchange)) + .assertNext(result -> assertEquals("testToken", result)) + .verifyComplete(); } @Test - public void resolve_shouldReturnNull_whenCookieIsNotPresent() { - ServerHttpRequest request = mock(ServerHttpRequest.class); + void resolve_shouldReturnNull_whenCookieIsNotPresent() { when(exchange.getRequest()).thenReturn(request); when(request.getCookies()).thenReturn(new LinkedMultiValueMap<>()); - Mono result = keyResolver.resolve(exchange); - - assertEquals("", result.block()); + StepVerifier.create(keyResolver.resolve(exchange)) + .assertNext(result -> assertEquals("", result)) + .verifyComplete(); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index e128246b71..a032e2c6b4 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -23,7 +23,6 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.ServerWebExchange; -import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import reactor.core.publisher.Mono; @@ -34,21 +33,17 @@ import java.util.HashMap; import java.util.Map; -import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; 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.when; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; class PageRedirectionFilterFactoryTest { private GatewayClient gatewayClient; private DiscoveryClient discoveryClient; - private final EurekaMetadataParser eurekaMetadataParser = new EurekaMetadataParser(); private static final String GW_HOSTNAME = "gateway"; private static final String GW_PORT = "10010"; private static final String GW_SCHEME = "https"; @@ -75,6 +70,8 @@ private PageRedirectionFilterFactory.Config createConfig() { var config = new PageRedirectionFilterFactory.Config(); config.setInstanceId("instanceId"); config.setServiceId("GATEWAY"); + config.setGatewayUrl("api/v1"); + config.setServiceUrl("/"); return config; } @@ -85,7 +82,10 @@ private void setupInstanceInfo() { metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api/v1"); metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); when(serviceInstance.getMetadata()).thenReturn(metadata); - when(discoveryClient.getInstances("GATEWAY")).thenReturn(new ArrayList<>(asList(serviceInstance))); + when(serviceInstance.getInstanceId()).thenReturn("instanceId"); + when(serviceInstance.getHost()).thenReturn("localhost"); + when(serviceInstance.getPort()).thenReturn(10010); + when(discoveryClient.getInstances("GATEWAY")).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); } @Nested @@ -94,7 +94,7 @@ class GivenValidUrl { @Test void whenNoAttls_thenAddRedirectionUrl() { var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(discoveryClient, eurekaMetadataParser, gatewayClient); + var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); var chain = mock(GatewayFilterChain.class); var exchange = mock(ServerWebExchange.class); @@ -118,13 +118,13 @@ void whenNoAttls_thenAddRedirectionUrl() { @Test void whenAttls_thenAddRedirectionUrl() { - var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(discoveryClient, eurekaMetadataParser, gatewayClient); + var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url?arg=1&arg=2"; + var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); var chain = mock(GatewayFilterChain.class); var exchange = mock(ServerWebExchange.class); var res = mock(ServerHttpResponse.class); var header = new HttpHeaders(); - header.put(HttpHeaders.LOCATION, Collections.singletonList("http://localhost:10010/api/v1/redirected_url")); + header.put(HttpHeaders.LOCATION, Collections.singletonList("http://localhost:10010/api/v1/redirected_url?arg=1&arg=2")); when(res.getHeaders()).thenReturn(header); commonSetup(factory, exchange, res, chain, true); @@ -141,8 +141,8 @@ class GivenMissingGwConfig { @Test void thenDoNotTransform() { - var expectedUrl = GW_BASE_URL + "http://localhost:10010/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(discoveryClient, eurekaMetadataParser, gatewayClient); + var expectedUrl = GW_BASE_URL + "/api/v1/redirected_url"; + var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); var chain = mock(GatewayFilterChain.class); var exchange = mock(ServerWebExchange.class); var res = mock(ServerHttpResponse.class); @@ -163,7 +163,7 @@ void thenDoNotTransform() { class GivenNullUrl { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(discoveryClient, eurekaMetadataParser, gatewayClient); + var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); var chain = mock(GatewayFilterChain.class); var exchange = mock(ServerWebExchange.class); var res = mock(ServerHttpResponse.class); @@ -171,7 +171,6 @@ void thenDoNotTransform() { header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); commonSetup(factory, exchange, res, chain, false); - setupInstanceInfo(); var config = createConfig(); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); @@ -183,7 +182,7 @@ void thenDoNotTransform() { class GivenDifferentResponseStatusCode { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(discoveryClient, eurekaMetadataParser, gatewayClient); + var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); var chain = mock(GatewayFilterChain.class); var exchange = mock(ServerWebExchange.class); var res = mock(ServerHttpResponse.class); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/X509FilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/X509FilterFactoryTest.java index f8940a17f5..3f3e1fe662 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/X509FilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/X509FilterFactoryTest.java @@ -12,6 +12,9 @@ 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.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.http.HttpHeaders; @@ -24,6 +27,7 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import javax.naming.ldap.LdapName; import javax.security.auth.x500.X500Principal; @@ -38,20 +42,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE; +@ExtendWith(MockitoExtension.class) class X509FilterFactoryTest { + public static final String ALL_HEADERS = "X-Certificate-Public,X-Certificate-DistinguishedName,X-Certificate-CommonName"; + private final X509Certificate[] x509Certificates = new X509Certificate[1]; - SslInfo sslInfo = mock(SslInfo.class); - ServerWebExchange exchange = mock(ServerWebExchange.class); - ServerHttpRequest request = mock(ServerHttpRequest.class); - ServerHttpResponse response = mock(ServerHttpResponse.class); - X509Certificate certificate = mock(X509Certificate.class); - GatewayFilterChain chain = mock(GatewayFilterChain.class); + @Mock SslInfo sslInfo; + @Mock ServerWebExchange exchange; + @Mock ServerHttpRequest request; + @Mock ServerHttpResponse response; + @Mock X509Certificate certificate; + @Mock GatewayFilterChain chain; + X509FilterFactory factory; X509FilterFactory.Config config; MessageService messageService = YamlMessageServiceInstance.getInstance(); @@ -70,35 +78,36 @@ void setup() { ServerWebExchange.Builder exchangeBuilder = new ServerWebExchangeBuilderMock(); x509Certificates[0] = certificate; - when(exchange.getRequest()).thenReturn(request); + lenient().when(exchange.getRequest()).thenReturn(request); - when(request.getSslInfo()).thenReturn(sslInfo); - when(request.mutate()).thenReturn(builder); + lenient().when(request.getSslInfo()).thenReturn(sslInfo); + lenient().when(request.mutate()).thenReturn(builder); - when(sslInfo.getPeerCertificates()).thenReturn(x509Certificates); + lenient().when(sslInfo.getPeerCertificates()).thenReturn(x509Certificates); - when(certificate.getSubjectDN()).thenReturn(new X500Principal("CN=user, OU=JavaSoft, O=Sun Microsystems, C=US")); - when(exchange.mutate()).thenReturn(exchangeBuilder); + lenient().when(certificate.getSubjectX500Principal()).thenReturn(new X500Principal("CN=user, OU=JavaSoft, O=Sun Microsystems, C=US")); + lenient().when(exchange.mutate()).thenReturn(exchangeBuilder); Map attributes = new HashMap<>(); - when(exchange.getAttributes()).thenReturn(attributes); + lenient().when(exchange.getAttributes()).thenReturn(attributes); - when(chain.filter(exchange)).thenReturn(Mono.empty()); + lenient().when(chain.filter(exchange)).thenReturn(Mono.empty()); } @Test void givenCertificateInRequest_thenPopulateHeaders() throws Exception { GatewayFilter filter = factory.apply(config); when(certificate.getEncoded()).thenReturn(new byte[2]); - Mono result = filter.filter(exchange, chain); - result.block(); + + StepVerifier.create(filter.filter(exchange, chain)) + .verifyComplete(); + assertEquals("user", exchange.getRequest().getHeaders().get("X-Certificate-CommonName").get(0)); assertEquals(Boolean.TRUE, exchange.getAttributes().get(HTTP_CLIENT_USE_CLIENT_CERTIFICATE)); } @Test void givenCertificateWithIncorrectEncoding_thenProvideInfoInHeader() throws Exception { - GatewayFilter filter = factory.apply(config); when(certificate.getEncoded()).thenThrow(new CertificateEncodingException("incorrect encoding")); Mono result = filter.filter(exchange, chain); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java index 5a313cb703..c606f5ab2b 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java @@ -220,6 +220,7 @@ class PostRoutingFilterDefinition { private final List COMMON_FILTERS = Collections.singletonList(mock(FilterDefinition.class)); private final RouteLocator routeLocator = new RouteLocator(null, COMMON_FILTERS, Collections.emptyList(), null); + private final RoutedService routedService = new RoutedService("test", "api/v1", "/service1"); private ServiceInstance createServiceInstance(Boolean forwardingEnabled, Boolean encodedCharactersEnabled, Boolean rateLimiterEnabled) { Map metadata = new HashMap<>(); @@ -251,7 +252,7 @@ void enableForwarding() { void givenServiceAllowingCertForwarding_whenGetPostRoutingFilters_thenAddClientCertFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(Boolean.TRUE, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertEquals(3, filterDefinitions.size()); // common filters + PageRedirectionFilterFactory assertEquals("ForwardClientCertFilterFactory", filterDefinitions.get(1).getName()); } @@ -260,7 +261,7 @@ void givenServiceAllowingCertForwarding_whenGetPostRoutingFilters_thenAddClientC void givenServiceNotAllowingCertForwarding_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(Boolean.FALSE, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForwardClientCertFilterFactory".equals(filter.getName()))); @@ -271,7 +272,7 @@ void givenServiceNotAllowingCertForwarding_whenGetPostRoutingFilters_thenReturnJ void givenServiceWithoutCertForwardingConfig_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(null, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForwardClientCertFilterFactory".equals(filter.getName()))); @@ -291,7 +292,7 @@ void disableForwarding() { void givenAnyService_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(Boolean.TRUE, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForwardClientCertFilterFactory".equals(filter.getName()))); @@ -305,7 +306,7 @@ class EncodedCharacters { @Test void givenServiceAllowingEncodedCharacters_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(null, Boolean.TRUE, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForbidEncodedCharactersFilterFactory".equals(filter.getName()))); @@ -314,7 +315,7 @@ void givenServiceAllowingEncodedCharacters_whenGetPostRoutingFilters_thenReturnJ @Test void givenServiceNotAllowingEncodedCharacters_whenGetPostRoutingFilters_thenAddEncodedCharacterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, Boolean.FALSE, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertEquals(3, filterDefinitions.size()); assertEquals("ForbidEncodedCharactersFilterFactory", filterDefinitions.get(1).getName()); } @@ -322,7 +323,7 @@ void givenServiceNotAllowingEncodedCharacters_whenGetPostRoutingFilters_thenAddE @Test void givenServiceWithoutAllowingEncodedCharacters_whenGetPostRoutingFilters_thenAddEncodedCharacterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForbidEncodedCharactersFilterFactory".equals(filter.getName()))); @@ -336,7 +337,7 @@ class RateLimiter { @Test void givenServiceNotAllowingRateLimiter_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(null, null, Boolean.FALSE); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "InMemoryRateLimiterFilterFactory".equals(filter.getName()))); @@ -345,7 +346,7 @@ void givenServiceNotAllowingRateLimiter_whenGetPostRoutingFilters_thenReturnJust @Test void givenServiceAllowingRateLimiter_whenGetPostRoutingFilters_thenAddInMemoryRateLimiterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, null, Boolean.TRUE); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertEquals(3, filterDefinitions.size()); assertEquals("InMemoryRateLimiterFilterFactory", filterDefinitions.get(1).getName()); } @@ -353,7 +354,7 @@ void givenServiceAllowingRateLimiter_whenGetPostRoutingFilters_thenAddInMemoryRa @Test void givenServiceWithoutAllowingRateLimiter_whenGetPostRoutingFilters_thenDoNotAddInMemoryRateLimiterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "InMemoryRateLimiterFilterFactory".equals(filter.getName()))); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/x509/ClientCertFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/x509/ClientCertFilterFactoryTest.java index 603af0d308..7f9fdf1c9b 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/x509/ClientCertFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/x509/ClientCertFilterFactoryTest.java @@ -23,6 +23,7 @@ import org.springframework.web.server.ServerWebExchange; import org.zowe.apiml.constants.ApimlConstants; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import java.net.InetSocketAddress; import java.net.URI; @@ -34,7 +35,9 @@ import java.util.Map; import java.util.function.Consumer; -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.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE; @@ -84,8 +87,10 @@ void setup() throws CertificateException { @Test void whenFilter_thenAddHeaderToRequest() { GatewayFilter filter = filterFactory.apply(filterConfig); - Mono result = filter.filter(exchange, chain); - result.block(); + + var elapsed = StepVerifier.create(filter.filter(exchange, chain)) + .verifyComplete(); + assertEquals(0L, elapsed.toSeconds()); assertNull(exchange.getRequest().getHeaders().get(ApimlConstants.AUTH_FAIL_HEADER)); assertNotNull(exchange.getRequest().getHeaders().get(CLIENT_CERT_HEADER)); diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogAuthenticationTest.java index b083283eb8..95d0151141 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogAuthenticationTest.java @@ -17,11 +17,8 @@ import io.restassured.specification.RequestSpecification; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -35,6 +32,7 @@ import org.zowe.apiml.util.service.DiscoveryUtils; import java.net.URISyntaxException; +import java.util.LinkedList; import java.util.List; import java.util.stream.Stream; @@ -50,6 +48,10 @@ @Slf4j class ApiCatalogAuthenticationTest { + private static final boolean IS_MODULITH_ENABLED = Boolean.getBoolean("environment.modulith"); + + private static final String UNAUTHENTICATED_ERROR_NUMBER = IS_MODULITH_ENABLED ? "ZWEAG120E" : "ZWEAS120E"; + private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); @@ -59,7 +61,7 @@ class ApiCatalogAuthenticationTest { private static final String CATALOG_APIDOC_ENDPOINT = "/apidoc/discoverableclient/zowe.apiml.discoverableclient.rest v1.0.0"; private static final String CATALOG_STATIC_REFRESH_ENDPOINT = "/static-api/refresh"; - private static final String CATALOG_ACTUATOR_ENDPOINT = "/application"; + private static final String CATALOG_APPLICATION_ENDPOINT = "/application"; private static final String CATALOG_HEALTH_ENDPOINT = "/application/health"; private final static String COOKIE = "apimlAuthenticationToken"; private final static String BASIC_AUTHENTICATION_PREFIX = "Basic"; @@ -74,20 +76,30 @@ private interface Request { } static Stream requestsToTest() { - return Stream.of( + var arguments = new LinkedList(); + arguments.add( Arguments.of(CATALOG_APIDOC_ENDPOINT, (Request) (when, endpoint) -> when.urlEncodingEnabled(false) // space in URL gets encoded by getUriFromGateway .get(getUriFromGateway(CATALOG_SERVICE_ID_PATH + CATALOG_PREFIX + endpoint)) - ), - Arguments.of(CATALOG_STATIC_REFRESH_ENDPOINT, (Request) (when, endpoint) -> when.post(getUriFromGateway(CATALOG_SERVICE_ID_PATH + CATALOG_PREFIX + endpoint))), - Arguments.of(CATALOG_ACTUATOR_ENDPOINT, (Request) (when, endpoint) -> when.get(getUriFromGateway(CATALOG_SERVICE_ID_PATH + CATALOG_PREFIX + endpoint))) + ) + ); + arguments.add( + Arguments.of(CATALOG_STATIC_REFRESH_ENDPOINT, (Request) (when, endpoint) -> when.post(getUriFromGateway(CATALOG_SERVICE_ID_PATH + CATALOG_PREFIX + endpoint))) ); + + if (!IS_MODULITH_ENABLED) { + arguments.add( + Arguments.of(CATALOG_APPLICATION_ENDPOINT, (Request) (when, endpoint) -> when.get(getUriFromGateway(CATALOG_SERVICE_ID_PATH + CATALOG_PREFIX + endpoint))) + ); + } + + return arguments.stream(); } static Stream requestsToTestWithCertificate() { return Stream.of( - Arguments.of(CATALOG_SERVICE_ID_PATH + CATALOG_APIDOC_ENDPOINT, (Request) (when, endpoint) -> when.get(apiCatalogServiceUrl + endpoint)), - Arguments.of(CATALOG_SERVICE_ID_PATH + CATALOG_STATIC_REFRESH_ENDPOINT, (Request) (when, endpoint) -> when.post(apiCatalogServiceUrl + endpoint)) + Arguments.of(CATALOG_SERVICE_ID_PATH + (IS_MODULITH_ENABLED ? CATALOG_PREFIX : "") + CATALOG_APIDOC_ENDPOINT, (Request) (when, endpoint) -> when.get(apiCatalogServiceUrl + endpoint)), + Arguments.of(CATALOG_SERVICE_ID_PATH + (IS_MODULITH_ENABLED ? CATALOG_PREFIX : "") + CATALOG_STATIC_REFRESH_ENDPOINT, (Request) (when, endpoint) -> when.post(apiCatalogServiceUrl + endpoint)) ); } @@ -168,7 +180,7 @@ class ReturnUnauthorized { @ParameterizedTest(name = "givenNoAuthentication {index} {0}") @MethodSource("org.zowe.apiml.functional.apicatalog.ApiCatalogAuthenticationTest#requestsToTest") void givenNoAuthentication(String endpoint, Request request) throws URISyntaxException { - String expectedMessage = "Authentication is required for URL '" + CATALOG_SERVICE_ID_PATH + new URIBuilder().setPath(endpoint).build() + "'"; + String expectedMessage = "Invalid username or password for URL '" + CATALOG_SERVICE_ID_PATH + (IS_MODULITH_ENABLED ? CATALOG_PREFIX : "") + new URIBuilder().setPath(endpoint).build() + "'"; request.execute( given() @@ -177,17 +189,18 @@ void givenNoAuthentication(String endpoint, Request request) throws URISyntaxExc endpoint ) .then() + .log().ifValidationFails() .statusCode(is(SC_UNAUTHORIZED)) .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_AUTHENTICATION_PREFIX) .body( - "messages.find { it.messageNumber == 'ZWEAS105E' }.messageContent", equalTo(expectedMessage) + "messages.find { it.messageNumber == '" + UNAUTHENTICATED_ERROR_NUMBER + "' }.messageContent", equalTo(expectedMessage) ); } @ParameterizedTest(name = "givenInvalidBasicAuthentication {index} {0}") @MethodSource("org.zowe.apiml.functional.apicatalog.ApiCatalogAuthenticationTest#requestsToTest") void givenInvalidBasicAuthentication(String endpoint, Request request) throws URISyntaxException { - String expectedMessage = "Invalid username or password for URL '" + CATALOG_SERVICE_ID_PATH + new URIBuilder().setPath(endpoint).build() + "'"; + String expectedMessage = "Invalid username or password for URL '" + CATALOG_SERVICE_ID_PATH + (IS_MODULITH_ENABLED ? CATALOG_PREFIX : "") + new URIBuilder().setPath(endpoint).build() + "'"; request.execute( given() @@ -199,7 +212,7 @@ void givenInvalidBasicAuthentication(String endpoint, Request request) throws UR .then() .statusCode(is(SC_UNAUTHORIZED)) .body( - "messages.find { it.messageNumber == 'ZWEAS120E' }.messageContent", equalTo(expectedMessage) + "messages.find { it.messageNumber == '" + UNAUTHENTICATED_ERROR_NUMBER + "' }.messageContent", equalTo(expectedMessage) ); } @@ -215,6 +228,7 @@ void givenInvalidBearerAuthentication(String endpoint, Request request) { endpoint ) .then() + .log().ifValidationFails() .body( "messages.find { it.messageNumber == 'ZWEAO402E' }.messageContent", equalTo(expectedMessage) ).statusCode(is(SC_UNAUTHORIZED)); @@ -308,16 +322,21 @@ void givenNoCertificateAndNoBasicAuth_thenReturnUnauthorized(String endpoint, Re } @Test + @DisabledIfSystemProperty( + disabledReason = "In Modulith, API Catalog does not have its own /application/** endpoints", + named = "environment.modulith", + matches = "true" + ) void givenOnlyValidCertificate_whenAccessNotCertificateAuthedRoute_thenReturnUnauthorized() { try { given() .config(SslContext.clientCertApiml) .when() - .get(apiCatalogServiceUrl + CATALOG_SERVICE_ID_PATH + CATALOG_ACTUATOR_ENDPOINT) + .get(apiCatalogServiceUrl + CATALOG_SERVICE_ID_PATH + CATALOG_APPLICATION_ENDPOINT) .then() .statusCode(HttpStatus.UNAUTHORIZED.value()); } catch (Exception e) { - fail("Failure to GET " + apiCatalogServiceUrl + CATALOG_SERVICE_ID_PATH + CATALOG_ACTUATOR_ENDPOINT); + fail("Failure to GET " + apiCatalogServiceUrl + CATALOG_SERVICE_ID_PATH + CATALOG_APPLICATION_ENDPOINT); } } @@ -326,6 +345,11 @@ void givenOnlyValidCertificate_whenAccessNotCertificateAuthedRoute_thenReturnUna @Test @DisplayName("This test needs to run against catalog service instance that has application/health endpoint authentication enabled.") + @DisabledIfSystemProperty( + disabledReason = "In Modulith, API Catalog does not have its own /application/** endpoints", + named = "environment.modulith", + matches = "true" + ) void thenDoNotAuthenticate() { try { given() @@ -340,6 +364,11 @@ void thenDoNotAuthenticate() { @Test @DisplayName("This test needs to run against catalog service instance that has application/health endpoint authentication provided.") + @DisabledIfSystemProperty( + disabledReason = "In Modulith, API Catalog does not have its own /application/** endpoints", + named = "environment.modulith", + matches = "true" + ) void thenAuthenticateTheRequest() { try { given() diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java index 1f5d2b6ab8..40601bf50f 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java @@ -55,6 +55,8 @@ @TestInstance(Lifecycle.PER_CLASS) class ApiCatalogEndpointIntegrationTest implements TestWithStartedInstances { + private static final boolean IS_MODULITH_ENABLED = Boolean.parseBoolean(System.getProperty("environment.modulith")); + private static final String GET_ALL_CONTAINERS_ENDPOINT = "/apicatalog/api/v1/containers"; private static final String GET_CONTAINER_BY_ID_ENDPOINT = "/apicatalog/api/v1/containers/apimediationlayer"; private static final String GET_CONTAINER_BY_INVALID_ID_ENDPOINT = "/apicatalog/api/v1/containers/bad"; @@ -117,16 +119,19 @@ void whenGetContainerById() throws IOException { @Test void whenGetContainerByInvalidId() throws IOException { - final HttpResponse response = getResponse(GET_CONTAINER_BY_INVALID_ID_ENDPOINT, HttpStatus.SC_OK); - assertEquals("[]", EntityUtils.toString(response.getEntity())); + getResponse(GET_CONTAINER_BY_INVALID_ID_ENDPOINT, HttpStatus.SC_NOT_FOUND); } } } @Nested class ApiDoc { + @Nested class ThenResponseOk { + + private static final String BASIC_SCHEME = IS_MODULITH_ENABLED ? "LoginBasicAuth" : "BasicAuthorization"; + @Test // Functional void whenSpecificCatalogApiDoc() throws Exception { @@ -159,7 +164,7 @@ void whenSpecificCatalogApiDoc() throws Exception { assertNotNull(paths.get("/apidoc/{serviceId}/{apiId}"), apiCatalogSwagger); assertNotNull(componentSchemas.get("APIContainer"), apiCatalogSwagger); assertNotNull(componentSchemas.get("APIService"), apiCatalogSwagger); - assertNotNull(securitySchemes.get("BasicAuthorization"), apiCatalogSwagger); + assertNotNull(securitySchemes.get(BASIC_SCHEME), apiCatalogSwagger); assertNotNull(securitySchemes.get("CookieAuth"), apiCatalogSwagger); } @@ -194,7 +199,7 @@ void whenDefaultCatalogApiDoc() throws Exception { assertNotNull(paths.get("/apidoc/{serviceId}/{apiId}"), apiCatalogSwagger); assertNotNull(componentSchemas.get("APIContainer"), apiCatalogSwagger); assertNotNull(componentSchemas.get("APIService"), apiCatalogSwagger); - assertNotNull(securitySchemes.get("BasicAuthorization"), apiCatalogSwagger); + assertNotNull(securitySchemes.get(BASIC_SCHEME), apiCatalogSwagger); assertNotNull(securitySchemes.get("CookieAuth"), apiCatalogSwagger); } } @@ -260,6 +265,7 @@ private Response getStaticApiResponse(String endpoint, String definitionFileName } return requestSpecification.post(uri).then() + .log().ifValidationFails() .statusCode(returnCode).extract().response(); } } diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogLoginIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogLoginIntegrationTest.java index 1c84bb84e1..ec77249064 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogLoginIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogLoginIntegrationTest.java @@ -11,7 +11,9 @@ package org.zowe.apiml.functional.apicatalog; import io.restassured.RestAssured; +import org.apache.http.HttpHeaders; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.util.TestWithStartedInstances; @@ -28,73 +30,141 @@ import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.zowe.apiml.security.SecurityUtils.readPassword; @GeneralAuthenticationTest class ApiCatalogLoginIntegrationTest implements TestWithStartedInstances { - private final static String CATALOG_PREFIX = "/api/v1"; - private final static String CATALOG_SERVICE_ID = "/apicatalog"; - private final static String LOGIN_ENDPOINT = "/auth/login"; - private final static String COOKIE_NAME = "apimlAuthenticationToken"; - private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); - private final static String PASSWORD = new String(readPassword(ConfigReader.environmentConfiguration().getCredentials().getPassword())); - private final static String INVALID_USERNAME = "incorrectUser"; - private final static String INVALID_PASSWORD = "incorrectPassword"; - private final static URI LOGIN_ENDPOINT_URL = HttpRequestUtils.getUriFromGateway(CATALOG_SERVICE_ID + CATALOG_PREFIX + LOGIN_ENDPOINT); + private static final boolean IS_MODULITH_ENABLED = Boolean.parseBoolean(System.getProperty("environment.modulith")); + + private static final String API_PREFIX = "/api/v1"; + private static final String GATEWAY_SERVICE_ID = "/gateway"; + private static final String CATALOG_SERVICE_ID = "/apicatalog"; + private static final String LOGIN_ENDPOINT = "/auth/login"; + private static final String COOKIE_NAME = "apimlAuthenticationToken"; + private static final String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); + private static final String PASSWORD = new String(readPassword(ConfigReader.environmentConfiguration().getCredentials().getPassword())); + private static final String INVALID_USERNAME = "incorrectUser"; + private static final String INVALID_PASSWORD = "incorrectPassword"; + + private static final URI LOGIN_ENDPOINT_URL_CATALOG = HttpRequestUtils.getUriFromGateway(CATALOG_SERVICE_ID + API_PREFIX + LOGIN_ENDPOINT); + private static final String LOGIN_ENDPOINT_URL_GATEWAY = GATEWAY_SERVICE_ID + API_PREFIX + LOGIN_ENDPOINT; @BeforeEach void setUp() { RestAssured.useRelaxedHTTPSValidation(); } - //@formatter:off - @Test - void doLoginWithValidBodyLoginRequest() { - LoginRequest loginRequest = new LoginRequest(USERNAME, PASSWORD.toCharArray()); - - given() - .contentType(JSON) - .body(loginRequest) - .when() - .post(LOGIN_ENDPOINT_URL) - .then() - .statusCode(is(SC_NO_CONTENT)) - .cookie(COOKIE_NAME, not(is(emptyString()))) - .extract().detailedCookie(COOKIE_NAME); - } + @Nested + class Microservices { + + @BeforeEach + void checkIfMicroserviceIsAvailable() { + assumeFalse(IS_MODULITH_ENABLED); + } + + //@formatter:off + @Test + void doLoginWithValidBodyLoginRequest() { + LoginRequest loginRequest = new LoginRequest(USERNAME, PASSWORD.toCharArray()); + + given() + .contentType(JSON) + .body(loginRequest) + .when() + .post(LOGIN_ENDPOINT_URL_CATALOG) + .then() + .statusCode(is(SC_NO_CONTENT)) + .cookie(COOKIE_NAME, not(is(emptyString()))) + .extract().detailedCookie(COOKIE_NAME); + } + + @Test + void doLoginWithInvalidCredentialsInLoginRequest() { + String expectedMessage = "Invalid username or password for URL '" + CATALOG_SERVICE_ID + LOGIN_ENDPOINT + "'"; + + LoginRequest loginRequest = new LoginRequest(INVALID_USERNAME, INVALID_PASSWORD.toCharArray()); + + given() + .contentType(JSON) + .body(loginRequest) + .when() + .post(LOGIN_ENDPOINT_URL_CATALOG) + .then() + .statusCode(is(SC_UNAUTHORIZED)) + .body( + "messages.find { it.messageNumber == 'ZWEAS120E' }.messageContent", equalTo(expectedMessage) + ); + } + + @Test + void doLoginWithoutCredentials() { + String expectedMessage = "Authorization header is missing, or the request body is missing or invalid for URL '" + + CATALOG_SERVICE_ID + LOGIN_ENDPOINT + "'"; + + given() + .when() + .post(LOGIN_ENDPOINT_URL_CATALOG) + .then() + .statusCode(is(SC_BAD_REQUEST)) + .body( + "messages.find { it.messageNumber == 'ZWEAS121E' }.messageContent", equalTo(expectedMessage) + ); + } + //@formatter:on - @Test - void doLoginWithInvalidCredentialsInLoginRequest() { - String expectedMessage = "Invalid username or password for URL '" + CATALOG_SERVICE_ID + LOGIN_ENDPOINT + "'"; - - LoginRequest loginRequest = new LoginRequest(INVALID_USERNAME, INVALID_PASSWORD.toCharArray()); - - given() - .contentType(JSON) - .body(loginRequest) - .when() - .post(LOGIN_ENDPOINT_URL) - .then() - .statusCode(is(SC_UNAUTHORIZED)) - .body( - "messages.find { it.messageNumber == 'ZWEAS120E' }.messageContent", equalTo(expectedMessage) - ); } - @Test - void doLoginWithoutCredentials() { - String expectedMessage = "Authorization header is missing, or the request body is missing or invalid for URL '" + - CATALOG_SERVICE_ID + LOGIN_ENDPOINT + "'"; - - given() - .when() - .post(LOGIN_ENDPOINT_URL) - .then() - .statusCode(is(SC_BAD_REQUEST)) - .body( - "messages.find { it.messageNumber == 'ZWEAS121E' }.messageContent", equalTo(expectedMessage) - ); + @Nested + class Modulith { + + @BeforeEach + void checkIfModulithIsAvailable() { + assumeTrue(IS_MODULITH_ENABLED); + } + + //@formatter:off + @Test + void doLoginWithValidBodyLoginRequest() { + LoginRequest loginRequest = new LoginRequest(USERNAME, PASSWORD.toCharArray()); + + given() + .contentType(JSON) + .body(loginRequest) + .when() + .post(LOGIN_ENDPOINT_URL_CATALOG) + .then() + .statusCode(is(308)) + .header(HttpHeaders.LOCATION, LOGIN_ENDPOINT_URL_GATEWAY); + } + + @Test + void doLoginWithInvalidCredentialsInLoginRequest() { + LoginRequest loginRequest = new LoginRequest(INVALID_USERNAME, INVALID_PASSWORD.toCharArray()); + + given() + .contentType(JSON) + .body(loginRequest) + .when() + .post(LOGIN_ENDPOINT_URL_CATALOG) + .then() + .statusCode(is(308)) + .header(HttpHeaders.LOCATION, LOGIN_ENDPOINT_URL_GATEWAY); + } + + @Test + void doLoginWithoutCredentials() { + given() + .when() + .post(LOGIN_ENDPOINT_URL_CATALOG) + .then() + .statusCode(is(308)) + .header(HttpHeaders.LOCATION, LOGIN_ENDPOINT_URL_GATEWAY); + } + //@formatter:on + } - //@formatter:on + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogStandaloneTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogStandaloneTest.java deleted file mode 100644 index 4cdd3a41bb..0000000000 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogStandaloneTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.functional.apicatalog; - -import io.restassured.RestAssured; -import io.restassured.config.SSLConfig; -import io.restassured.response.ValidatableResponse; -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.zowe.apiml.util.config.ApiCatalogServiceConfiguration; -import org.zowe.apiml.util.config.ConfigReader; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import static io.restassured.RestAssured.given; -import static io.restassured.RestAssured.when; -import static org.apache.http.HttpStatus.SC_OK; -import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@Tag("ApiCatalogStandaloneTest") -public class ApiCatalogStandaloneTest { - - private static final String GET_ALL_CONTAINERS_ENDPOINT = "/apicatalog/containers"; - private static final String GET_API_CATALOG_API_DOC_DEFAULT_ENDPOINT = "/apicatalog/apidoc/service2"; - private static final String GET_API_CATALOG_API_DOC_ENDPOINT = "/apicatalog/apidoc/service2/org.zowe v2.0.0"; - - private final static String USERNAME = ConfigReader.environmentConfiguration().getAuxiliaryUserList().getCredentials("servicesinfo-authorized").get(0).getUser(); - private final static String PASSWORD = ConfigReader.environmentConfiguration().getAuxiliaryUserList().getCredentials("servicesinfo-authorized").get(0).getPassword(); - - private String baseHost; - - @BeforeAll - static void init() throws Exception { - RestAssured.useRelaxedHTTPSValidation(); - } - - @BeforeEach - void setUp() { - ApiCatalogServiceConfiguration configuration = ConfigReader.environmentConfiguration().getApiCatalogStandaloneConfiguration(); - String host = configuration.getHost(); - String scheme = configuration.getScheme(); - int port = configuration.getPort(); - baseHost = scheme + "://" + host + ":" + port; - RestAssured.config = RestAssured.config().sslConfig(SSLConfig.sslConfig()); - RestAssured.useRelaxedHTTPSValidation(); - } - - @Nested - class Containers { - - @Nested - class HasRegisteredServices { - - @Test - void whenGetContainers() throws IOException { - final ValidatableResponse response = when() - .get(baseHost + GET_ALL_CONTAINERS_ENDPOINT) - .then() - .statusCode(is(SC_OK)) - .contentType("application/json"); - - List> list = response.extract().jsonPath().getList("$."); - assertEquals(2, list.size()); - assertEquals("Mocked Services from file", list.get(0).get("title")); - assertEquals("Another mocked Services from file", list.get(1).get("title")); - } - } - - @Nested - class ApiDocIsAvailable { - - @Test - void whenGetApiDocDefaultEndpoint() { - final ValidatableResponse response = when() - .get(baseHost + GET_API_CATALOG_API_DOC_DEFAULT_ENDPOINT) - .then() - .statusCode(is(SC_OK)) - .contentType("application/json"); - assertEquals("Service 2 - v1 (default)", response.extract().jsonPath().get("info.title")); - } - - @Test - void whenGetApiDocv2Endpoint() { - final ValidatableResponse response = when() - .get(baseHost + GET_API_CATALOG_API_DOC_ENDPOINT) - .then() - .statusCode(is(SC_OK)) - .contentType("application/json"); - assertEquals("Service 2 - v2", response.extract().jsonPath().get("info.title")); - } - } - } - - @Nested - class Access { - - @Nested - class AuthenticationIsNotRequired { - - @Test - void givenBasicAuthenticationIsProvided() { - given() - .auth() - .basic(USERNAME, PASSWORD) - .when() - .get(baseHost + GET_ALL_CONTAINERS_ENDPOINT) - .then() - .statusCode(is(SC_OK)) - .contentType("application/json"); - } - } - } -} diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/ApiCatalogMultipleInstancesTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/ApiCatalogMultipleInstancesTest.java index 9794dfa880..7ef5f7f723 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/ha/ApiCatalogMultipleInstancesTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/ha/ApiCatalogMultipleInstancesTest.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.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.zowe.apiml.util.categories.HATest; import org.zowe.apiml.util.requests.Apps; import org.zowe.apiml.util.requests.ha.HAApiCatalogRequests; @@ -29,6 +30,7 @@ */ @HATest public class ApiCatalogMultipleInstancesTest { + private final HAApiCatalogRequests haApiCatalogRequests = new HAApiCatalogRequests(); private final HADiscoveryRequests haDiscoveryRequests = new HADiscoveryRequests(); @@ -39,9 +41,16 @@ void setUp() { @Nested class GivenMultipleApiCatalogInstances { + @Nested class WhenSendingRequest { + @Test + @DisabledIfSystemProperty( + disabledReason = "In Modulith, API Catalog is only one in each API ML instance", + named = "environment.modulith", + matches = "true" + ) void apiCatalogInstancesAreUp() { assumeTrue(haApiCatalogRequests.existing() > 1); @@ -49,11 +58,19 @@ void apiCatalogInstancesAreUp() { } @Test + @DisabledIfSystemProperty( + disabledReason = "In Modulith, API Catalog is only one in each API ML instance", + named = "environment.modulith", + matches = "true" + ) void apiCatalogInstancesAreRegistered() { assumeTrue(haApiCatalogRequests.existing() > 1 && haDiscoveryRequests.existing() > 1); assertThat(haDiscoveryRequests.getAmountOfRegisteredInstancesForService(0, Apps.API_CATALOG), is(2)); } + } + } + } diff --git a/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java b/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java index 0df05a9af1..6ba06ec4dc 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java +++ b/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java @@ -101,16 +101,23 @@ private DocumentContext getDocumentAsContext(HttpGet request) { private boolean areAllServicesUp() { try { - HttpGet requestToGateway = HttpRequestUtils.getRequest(healthEndpoint); - requestToGateway.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString(String.format("%s:%s", credentials.getUser(), credentials.getPassword()).getBytes())); - DocumentContext context = getDocumentAsContext(requestToGateway); - if (context == null) { + var gatewayHosts = gatewayConfiguration.getHost().split(","); + var requestToGateway1 = HttpRequestUtils.getRequest(gatewayHosts[0], healthEndpoint); + // If second one does not exist, redundant call and check to same gateway + var requestToGateway2 = HttpRequestUtils.getRequest(gatewayHosts.length > 1 ? gatewayHosts[1] : gatewayHosts[0], healthEndpoint); + + requestToGateway1.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString(String.format("%s:%s", credentials.getUser(), credentials.getPassword()).getBytes())); + requestToGateway2.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString(String.format("%s:%s", credentials.getUser(), credentials.getPassword()).getBytes())); + DocumentContext context1 = getDocumentAsContext(requestToGateway1); + DocumentContext context2 = getDocumentAsContext(requestToGateway2); + + if (context1 == null || context2 == null) { return false; } boolean areAllServicesUp = true; for (Service toCheck : servicesToCheck) { - boolean isUp = isServiceUp(context, toCheck.path); + boolean isUp = isServiceUp(context1, toCheck.path); logDebug(toCheck.name + " is {}", isUp); if (!isUp) { @@ -121,7 +128,7 @@ private boolean areAllServicesUp() { areAllServicesUp = false; } - String allComponents = context.read("$.components.discoveryComposite.components.discoveryClient.details.services").toString(); + String allComponents = context1.read("$.components.discoveryComposite.components.discoveryClient.details.services").toString(); boolean isTestApplicationUp = allComponents.toLowerCase().contains("discoverableclient"); boolean needsTestApplication = discoverableClientConfiguration.getInstances() > 0; @@ -129,38 +136,45 @@ private boolean areAllServicesUp() { log.debug("Needs Discoverable Client: {}", needsTestApplication); isTestApplicationUp = !needsTestApplication || isTestApplicationUp; - - Integer amountOfActiveGateways = context.read("$.components.gateway.details.gatewayCount"); + Integer amountOfActiveGateways1 = context1.read("$.components.gateway.details.gatewayCount"); + Integer amountOfActiveGateways2 = context2.read("$.components.gateway.details.gatewayCount"); var expectedGatewayCount = Integer.getInteger("environment.gwCount", gatewayConfiguration.getInstances()); - boolean isValidAmountOfGatewaysUp = amountOfActiveGateways != null && - amountOfActiveGateways >= expectedGatewayCount; - log.debug("There are {} gateways", amountOfActiveGateways); + boolean isValidAmountOfGatewaysUp = amountOfActiveGateways1 != null && amountOfActiveGateways2 != null && + amountOfActiveGateways1 >= expectedGatewayCount && amountOfActiveGateways2 >= expectedGatewayCount; + log.debug("There are {} gateways in GW1 and {} in GW2", amountOfActiveGateways1, amountOfActiveGateways2); + if (!isValidAmountOfGatewaysUp) { log.debug("Expecting at least {} gateways", gatewayConfiguration.getInstances()); callEurekaApps(); return false; } + // Consider properly the case with multiple gateway services running on different ports. if (gatewayConfiguration.getInternalPorts() != null && !gatewayConfiguration.getInternalPorts().isEmpty()) { String[] internalPorts = gatewayConfiguration.getInternalPorts().split(","); String[] hosts = gatewayConfiguration.getHost().split(","); + for (int i = 0; i < Math.min(internalPorts.length, hosts.length); i++) { log.debug("Trying to access the Gateway at port {}", internalPorts[i]); - requestToGateway = HttpRequestUtils.getRequest(healthEndpoint); - requestToGateway.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString(String.format("%s:%s", credentials.getUser(), credentials.getPassword()).getBytes())); - var response = HttpClientUtils.client().execute(requestToGateway); + requestToGateway1 = HttpRequestUtils.getRequest(healthEndpoint); + requestToGateway1.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString(String.format("%s:%s", credentials.getUser(), credentials.getPassword()).getBytes())); + var response = HttpClientUtils.client().execute(requestToGateway1); + if (response.getStatusLine().getStatusCode() != 200) { - log.debug("Response from gateway at {} was: {}", requestToGateway.getURI(), response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : "undefined"); + log.debug("Response from gateway at {} was: {}", requestToGateway1.getURI(), response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : "undefined"); throw new IOException(); } + } + } var result = areAllServicesUp && isTestApplicationUp; if (!result) { log.debug("API ML is not ready, check which services are missing in the above messages"); } + return result; } catch (PathNotFoundException | IOException e) { log.warn("Check failed on retrieving the information from document: {}", e.getMessage()); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java b/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java index fced3dc4f9..18386f9bb7 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/http/HttpRequestUtils.java @@ -63,7 +63,13 @@ public static HttpResponse getResponse(String endpoint, int returnCode, int port } public static HttpGet getRequest(String endpoint) { - URI uri = getUriFromGateway(endpoint); + var uri = getUriFromGateway(endpoint); + + return new HttpGet(uri); + } + + public static HttpGet getRequest(String gatewayHostname, String endpoint) { + var uri = getUriFromGateway(endpoint, gatewayHostname); return new HttpGet(uri); } diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/requests/ApiCatalogRequests.java b/integration-tests/src/test/java/org/zowe/apiml/util/requests/ApiCatalogRequests.java index 3800b3b8a2..3bdb589754 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/requests/ApiCatalogRequests.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/requests/ApiCatalogRequests.java @@ -27,6 +27,9 @@ @Slf4j public class ApiCatalogRequests { + + private static final boolean IS_MODULITH_ENABLED = Boolean.getBoolean("environment.modulith"); + private static final ApiCatalogServiceConfiguration apiCatalogServiceConfiguration = ConfigReader.environmentConfiguration().getApiCatalogServiceConfiguration(); private static final Credentials credentials = ConfigReader.environmentConfiguration().getCredentials(); @@ -57,9 +60,10 @@ public boolean isUp() { .contentType(JSON) .auth() .basic(credentials.getUser(), new String(credentials.getPassword())) - .when() - .get(getApiCatalogUriWithPath("/apicatalog" + Endpoints.HEALTH)) - .then() + .when() + .get(getApiCatalogUriWithPath(Endpoints.HEALTH)) + .then() + .log().ifValidationFails() .statusCode(200) .body("status", Matchers.is("UP")); return true; @@ -91,7 +95,7 @@ private URI getApiCatalogUriWithPath(String path) throws URISyntaxException { .setScheme(scheme) .setHost(host) .setPort(port) - .setPath(path) + .setPath((IS_MODULITH_ENABLED ? "/apicatalog/api/v1" : "/apicatalog") + path) .build(); } diff --git a/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml b/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml index f2548c9a1d..ffe7ed30b0 100644 --- a/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml +++ b/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml @@ -34,8 +34,8 @@ discoverableClientConfiguration: apiCatalogServiceConfiguration: scheme: https url: - host: api-catalog-services,api-catalog-services-2 - port: 10014 + host: apiml,apiml-2 + port: 10010 instances: 2 cachingServiceConfiguration: url: diff --git a/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml b/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml index 6e17751745..6e75bc3ad7 100644 --- a/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml +++ b/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml @@ -34,14 +34,8 @@ discoverableClientConfiguration: apiCatalogServiceConfiguration: scheme: https url: - host: api-catalog-services - port: 10014 - instances: 1 -apiCatalogStandaloneConfiguration: - scheme: https - url: - host: api-catalog-services-2 - port: 10015 + host: apiml + port: 10010 instances: 1 cachingServiceConfiguration: url: diff --git a/integration-tests/src/test/resources/environment-configuration-docker.yml b/integration-tests/src/test/resources/environment-configuration-docker.yml index 0f2dd41ff1..6f40352496 100644 --- a/integration-tests/src/test/resources/environment-configuration-docker.yml +++ b/integration-tests/src/test/resources/environment-configuration-docker.yml @@ -41,12 +41,6 @@ apiCatalogServiceConfiguration: host: api-catalog-services port: 10014 instances: 1 -apiCatalogStandaloneConfiguration: - scheme: https - url: - host: api-catalog-services-2 - port: 10015 - instances: 1 cachingServiceConfiguration: url: tlsConfiguration: diff --git a/integration-tests/src/test/resources/environment-configuration.yml b/integration-tests/src/test/resources/environment-configuration.yml index 543601a32c..5c069ea7e9 100644 --- a/integration-tests/src/test/resources/environment-configuration.yml +++ b/integration-tests/src/test/resources/environment-configuration.yml @@ -41,12 +41,6 @@ apiCatalogServiceConfiguration: host: localhost port: 10014 instances: 1 -apiCatalogStandaloneConfiguration: - scheme: https - url: - host: localhost - port: 10015 - instances: 1 cachingServiceConfiguration: url: tlsConfiguration: diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/login/GatewayLoginProvider.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/login/GatewayLoginProvider.java index 797ed38e88..0ee491099f 100644 --- a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/login/GatewayLoginProvider.java +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/login/GatewayLoginProvider.java @@ -11,12 +11,13 @@ package org.zowe.apiml.security.client.login; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; -import org.zowe.apiml.security.client.service.GatewaySecurityService; +import org.zowe.apiml.security.client.service.GatewaySecurity; import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.security.common.token.TokenAuthentication; @@ -30,8 +31,10 @@ */ @Component @RequiredArgsConstructor +@ConditionalOnMissingBean(name = "modulithConfig") public class GatewayLoginProvider implements AuthenticationProvider { - private final GatewaySecurityService gatewaySecurityService; + + private final GatewaySecurity gatewaySecurity; /** * Authenticate the credentials @@ -55,7 +58,7 @@ public Authentication authenticate(Authentication authentication) { cleanup = !(authentication.getCredentials() instanceof char[]); } - Optional token = gatewaySecurityService.login(username, password, newPassword); + Optional token = gatewaySecurity.login(username, password, newPassword); if (!token.isPresent()) { throw new BadCredentialsException("Invalid Credentials"); diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurity.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurity.java new file mode 100644 index 0000000000..eea805deb8 --- /dev/null +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurity.java @@ -0,0 +1,38 @@ +/* + * 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.security.client.service; + +import org.zowe.apiml.security.common.token.QueryResponse; + +import java.util.Optional; + +public interface GatewaySecurity { + + /** + * Logs into the gateway with username and password, and retrieves valid JWT token + * + * @param username Username + * @param password Password + * @return Valid JWT token for the supplied credentials + */ + Optional login(String username, char[] password, char[] newPassword); + + /** + * Verifies JWT token validity and returns JWT token data + * + * @param token JWT token to be validated + * @return JWT token data as {@link QueryResponse} + */ + QueryResponse query(String token); + + QueryResponse verifyOidc(String token); + +} diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java index 2971d3fbf3..9709a06cd7 100644 --- a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/service/GatewaySecurityService.java @@ -45,7 +45,8 @@ @Service @Slf4j @RequiredArgsConstructor -public class GatewaySecurityService { +public class GatewaySecurityService implements GatewaySecurity { + private static final String MESSAGE_KEY_STRING = "messageKey\":\""; private final GatewayClient gatewayClient; @@ -54,13 +55,7 @@ public class GatewaySecurityService { private final RestResponseHandler responseHandler; private final ObjectMapper objectMapper = new ObjectMapper(); - /** - * Logs into the gateway with username and password, and retrieves valid JWT token - * - * @param username Username - * @param password Password - * @return Valid JWT token for the supplied credentials - */ + @Override public Optional login(String username, char[] password, char[] newPassword) { ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), @@ -97,12 +92,7 @@ public Optional login(String username, char[] password, char[] newPasswo return Optional.empty(); } - /** - * Verifies JWT token validity and returns JWT token data - * - * @param token JWT token to be validated - * @return JWT token data as {@link QueryResponse} - */ + @Override public QueryResponse query(String token) { ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), @@ -133,6 +123,7 @@ public QueryResponse query(String token) { return null; } + @Override public QueryResponse verifyOidc(String token) { ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/token/GatewayTokenProvider.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/token/GatewayTokenProvider.java index 9915345098..294436133f 100644 --- a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/token/GatewayTokenProvider.java +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/token/GatewayTokenProvider.java @@ -10,21 +10,24 @@ package org.zowe.apiml.security.client.token; -import org.zowe.apiml.security.client.service.GatewaySecurityService; -import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.security.common.token.TokenAuthentication; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; +import org.zowe.apiml.security.client.service.GatewaySecurity; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenAuthentication; /** * Authentication provider that authenticates TokenAuthentication against Gateway */ @Component @RequiredArgsConstructor +@ConditionalOnMissingBean(name = "modulithConfig") public class GatewayTokenProvider implements AuthenticationProvider { - private final GatewaySecurityService gatewaySecurityService; + + private final GatewaySecurity gatewaySecurity; /** * Authenticate the token @@ -37,9 +40,9 @@ public Authentication authenticate(Authentication authentication) { TokenAuthentication tokenAuthentication = (TokenAuthentication) authentication; QueryResponse queryResponse; if (tokenAuthentication.getType() == TokenAuthentication.Type.OIDC) { - queryResponse = gatewaySecurityService.verifyOidc(tokenAuthentication.getCredentials()); + queryResponse = gatewaySecurity.verifyOidc(tokenAuthentication.getCredentials()); } else { - queryResponse = gatewaySecurityService.query(tokenAuthentication.getCredentials()); + queryResponse = gatewaySecurity.query(tokenAuthentication.getCredentials()); } TokenAuthentication validTokenAuthentication = new TokenAuthentication(queryResponse.getUserId(), tokenAuthentication.getCredentials(), tokenAuthentication.getType()); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/custom/CustomErrorStatusHandlingBean.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/custom/CustomErrorStatusHandlingBean.java index 759b426ed7..ccf1f0af90 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/error/custom/CustomErrorStatusHandlingBean.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/error/custom/CustomErrorStatusHandlingBean.java @@ -10,6 +10,7 @@ package org.zowe.apiml.zaas.error.custom; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; @@ -17,10 +18,13 @@ import org.springframework.stereotype.Component; @Component +@ConditionalOnMissingBean(name = "modulithConfig") public class CustomErrorStatusHandlingBean implements WebServerFactoryCustomizer { + @Override public void customize(ConfigurableServletWebServerFactory factory) { factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/not_found")); factory.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/internal_error")); } + } From 3143af66166254eeed993eb343b5e5859668fa86 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 25 Jul 2025 16:33:11 +0200 Subject: [PATCH 034/152] fix: collect fixes to run modulith in z/OS (#4227) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../apicatalog/ApiCatalogApplication.java | 1 + .../apiml/apicatalog/AppReadyListener.java | 6 +- .../functional/ApiCatalogFunctionalTest.java | 3 + .../zowe/apiml/util/config/TestConfig.java | 25 +++++++ apiml-package/src/main/resources/bin/start.sh | 36 ++++++++-- .../logging/ApimlRollingFileAppender.java | 1 + .../service/ServiceStartupEventHandler.java | 7 ++ .../src/main/resources/logback-spring.xml | 53 +++++++++------ apiml/.gitignore | 3 +- .../java/org/zowe/apiml/ApimlApplication.java | 1 + .../zowe/apiml/GatewayHealthIndicator.java | 1 + apiml/src/main/resources/application.yml | 2 +- ...ce.java => CachingServiceApplication.java} | 14 +--- .../CachingServiceStartupListener.java | 32 +++++++++ .../apiml/caching/config/GeneralConfig.java | 9 +++ .../CachingServiceStartupListenerTest.java | 49 ++++++++++++++ .../apiml/caching/config/AttlsConfigTest.java | 10 +-- discoverable-client/build.gradle | 1 + .../DiscoverableClientSampleApplication.java | 6 +- .../ApplicationConfiguration.java | 8 ++- .../configuration/SecurityConfiguration.java | 8 ++- .../SpringComponentsConfiguration.java | 10 ++- .../ApiMediationClientTestControllerTest.java | 12 ++-- .../apiml/client/api/FileControllerTest.java | 7 +- .../client/api/GreetingControllerTest.java | 8 +-- .../api/GreetingGraphControllerTest.java | 9 ++- .../client/api/GreetingV2ControllerTest.java | 7 +- .../client/api/PetControllerDeleteTest.java | 6 +- .../client/api/PetControllerGetAllTest.java | 6 +- .../client/api/PetControllerGetOneTest.java | 10 +-- .../client/api/RequestInfoControllerTest.java | 9 ++- .../client/api/StatusCodeControllerTest.java | 6 +- .../client/api/WebServiceControllerTest.java | 6 +- .../apiml/client/api/X509ControllerTest.java | 6 +- .../api/ZaasClientTestControllerTest.java | 1 + .../ApiMediationClientServiceTest.java | 2 +- .../sse/ServerSentEventControllerTest.java | 6 +- .../src/main/resources/bin/start.sh | 26 ++++--- .../DiscoveryServiceApplication.java | 10 ++- .../EurekaRegistryAvailableListener.java | 12 +++- .../EurekaRegistryAvailableListenerTest.java | 67 +++++++++++++++++++ .../discovery/config/AttlsConfigTest.java | 3 + .../functional/DiscoveryFunctionalTest.java | 3 + docker-compose.yml | 4 +- .../gateway/GatewayServiceApplication.java | 1 + .../zowe/apiml/zaas/ZaasStartupListener.java | 3 +- 46 files changed, 389 insertions(+), 127 deletions(-) create mode 100644 apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/TestConfig.java rename caching-service/src/main/java/org/zowe/apiml/caching/{CachingService.java => CachingServiceApplication.java} (61%) create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/CachingServiceStartupListener.java create mode 100644 caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceStartupListenerTest.java create mode 100644 discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListenerTest.java diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java index 1a7b52d364..4656f0b135 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogApplication.java @@ -29,6 +29,7 @@ "org.zowe.apiml.product.security", "org.zowe.apiml.product.web", "org.zowe.apiml.product.gateway", + "org.zowe.apiml.product.service", "org.zowe.apiml.filter" }) @EnableScheduling diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java index f44ad75a3b..c1280e297e 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java @@ -10,6 +10,7 @@ package org.zowe.apiml.apicatalog; +import lombok.RequiredArgsConstructor; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -19,8 +20,11 @@ * This class fires on ApplicationReadyEvent event during Spring context initialization */ @Component +@RequiredArgsConstructor public class AppReadyListener { + private final ServiceStartupEventHandler handler; + /** * Fires on ApplicationReadyEvent * triggers ServiceStartupEventHandler @@ -29,7 +33,7 @@ public class AppReadyListener { */ @EventListener public void onApplicationEvent(ApplicationReadyEvent event) { - new ServiceStartupEventHandler().onServiceStartup("API Catalog Service", + handler.onServiceStartup("API Catalog Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java index 1101455516..0928ed588e 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/ApiCatalogFunctionalTest.java @@ -18,15 +18,18 @@ import org.springframework.beans.factory.annotation.Value; 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.context.ContextConfiguration; import org.zowe.apiml.apicatalog.ApiCatalogApplication; import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.util.config.TestConfig; @SpringBootTest( classes = ApiCatalogApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) @ContextConfiguration +@Import(TestConfig.class) public abstract class ApiCatalogFunctionalTest { @LocalServerPort diff --git a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/TestConfig.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/TestConfig.java new file mode 100644 index 0000000000..3fd72ca9a3 --- /dev/null +++ b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/config/TestConfig.java @@ -0,0 +1,25 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.util.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; + +@TestConfiguration +public class TestConfig { + + @Bean + ServiceStartupEventHandler serviceStartupEventHandler() { + return new ServiceStartupEventHandler(); + } + +} diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index a34cb19f02..9e1f8e412c 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -151,9 +151,9 @@ else fi ZOWE_CONSOLE_LOG_CHARSET=UTF-8 -APIML_LOADER_PATH=${COMMON_LIB} if [ "$(uname)" = "OS/390" ]; then QUICK_START="-Xquickstart" + APIML_LOADER_PATH=${COMMON_LIB},/usr/include/java_classes/IRRRacf.jar JAVA_VERSION=$(${JAVA_HOME}/bin/javap -J-Xms4m -J-Xmx16m -verbose java.lang.String \ | grep "major version" \ @@ -162,6 +162,8 @@ if [ "$(uname)" = "OS/390" ]; then if [ $JAVA_VERSION -ge 65 ]; then # Java 21 ZOWE_CONSOLE_LOG_CHARSET=IBM-1047 fi +else + APIML_LOADER_PATH=${COMMON_LIB} fi # Check if the directory containing the ZAAS shared JARs was set and append it to the ZAAS loader path @@ -322,6 +324,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ ${QUICK_START} \ ${ADD_OPENS} \ ${LOGBACK} \ + -Dapiml.cache.storage.location=${ZWE_zowe_workspaceDirectory}/api-mediation/${ZWE_haInstance_id:-localhost} \ -Dapiml.connection.idleConnectionTimeoutSeconds=${ZWE_configs_apiml_connection_idleConnectionTimeoutSeconds:-${ZWE_components_gateway_apiml_connection_idleConnectionTimeoutSeconds:-5}} \ -Dapiml.connection.timeout=${ZWE_configs_apiml_connection_timeout:-${ZWE_components_gateway_apiml_connection_timeout:-60000}} \ -Dapiml.connection.timeToLive=${ZWE_configs_apiml_connection_timeToLive:-${ZWE_components_gateway_apiml_connection_timeToLive:-10000}} \ @@ -344,31 +347,56 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.httpclient.ssl.enabled-protocols=${client_enabled_protocols} \ -Dapiml.internal-discovery.port=${ZWE_configs_internal_discovery_port:-${ZWE_components_discovery_port:-7553}} \ -Dapiml.logs.location=${ZWE_zowe_logDirectory} \ + -Dapiml.security.allowTokenRefresh=${ZWE_configs_apiml_security_allowtokenrefresh:-${ZWE_components_gateway_apiml_security_allowtokenrefresh:-false}} \ -Dapiml.security.auth.cookieProperties.cookieName=${cookieName:-apimlAuthenticationToken} \ -Dapiml.security.auth.jwt.customAuthHeader=${ZWE_configs_apiml_security_auth_jwt_customAuthHeader:-${ZWE_components_gateway_apiml_security_auth_jwt_customAuthHeader:-}} \ -Dapiml.security.auth.passticket.customAuthHeader=${ZWE_configs_apiml_security_auth_passticket_customAuthHeader:-${ZWE_components_gateway_apiml_security_auth_passticket_customAuthHeader:-}} \ -Dapiml.security.auth.passticket.customUserHeader=${ZWE_configs_apiml_security_auth_passticket_customUserHeader:-${ZWE_components_gateway_apiml_security_auth_passticket_customUserHeader:-}} \ + -Dapiml.security.auth.provider=${ZWE_configs_apiml_security_auth_provider:-${ZWE_components_gateway_apiml_security_auth_provider:-zosmf}} \ + -Dapiml.security.auth.zosmf.jwtAutoconfiguration=${ZWE_configs_apiml_security_auth_zosmf_jwtAutoconfiguration:-${ZWE_components_gateway_apiml_security_auth_zosmf_jwtAutoconfiguration:-jwt}} \ + -Dapiml.security.auth.zosmf.serviceId=${ZWE_configs_apiml_security_auth_zosmf_serviceId:-${ZWE_components_gateway_apiml_security_auth_zosmf_serviceId:-ibmzosmf}} \ -Dapiml.security.authorization.endpoint.enabled=${ZWE_configs_apiml_security_authorization_endpoint_enabled:-${ZWE_components_gateway_apiml_security_authorization_endpoint_enabled:-false}} \ -Dapiml.security.authorization.endpoint.url=${ZWE_configs_apiml_security_authorization_endpoint_url:-${ZWE_components_gateway_apiml_security_authorization_endpoint_url:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/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_configs_apiml_security_authorization_resourceClass:-${ZWE_components_gateway_apiml_security_authorization_resourceClass:-ZOWE}} \ + -Dapiml.security.authorization.resourceNamePrefix=${ZWE_configs_apiml_security_authorization_resourceNamePrefix:-${ZWE_components_gateway_apiml_security_authorization_resourceNamePrefix:-APIML.}} \ + -Dapiml.security.jwtInitializerTimeout=${ZWE_configs_apiml_security_jwtInitializerTimeout:-${ZWE_components_gateway_apiml_security_jwtInitializerTimeout:-5}} \ + -Dapiml.security.oidc.enabled=${ZWE_configs_apiml_security_oidc_enabled:-${ZWE_components_gateway_apiml_security_oidc_enabled:-false}} \ + -Dapiml.security.oidc.identityMapperUrl=${ZWE_configs_apiml_security_oidc_identityMapperUrl:-${ZWE_components_gateway_apiml_security_oidc_identityMapperUrl:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/certificate/dn"}} \ + -Dapiml.security.oidc.identityMapperUser=${ZWE_configs_apiml_security_oidc_identityMapperUser:-${ZWE_components_gateway_apiml_security_oidc_identityMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ + -Dapiml.security.oidc.jwks.refreshInternalHours=${ZWE_configs_apiml_security_oidc_jwks_refreshInternalHours:-${ZWE_components_gateway_apiml_security_oidc_jwks_refreshInternalHours:-1}} \ + -Dapiml.security.oidc.jwks.uri=${ZWE_configs_apiml_security_oidc_jwks_uri:-${ZWE_components_gateway_apiml_security_oidc_jwks_uri:-}} \ + -Dapiml.security.oidc.registry=${ZWE_configs_apiml_security_oidc_registry:-${ZWE_components_gateway_apiml_security_oidc_registry:-}} \ + -Dapiml.security.oidc.userInfo.uri=${ZWE_configs_apiml_security_oidc_userInfo_uri:-${ZWE_components_gateway_apiml_security_oidc_userInfo_uri:-}} \ + -Dapiml.security.oidc.validationType=${ZWE_configs_apiml_security_oidc_validationType:-${ZWE_components_gateway_apiml_security_oidc_validationType:-"JWK"}} \ + -Dapiml.security.personalAccessToken.enabled=${ZWE_configs_apiml_security_personalAccessToken_enabled:-${ZWE_components_gateway_apiml_security_personalAccessToken_enabled:-false}} \ + -Dapiml.security.saf.provider=${ZWE_configs_apiml_security_saf_provider:-${ZWE_components_gateway_apiml_security_saf_provider:-"rest"}} \ + -Dapiml.security.saf.urls.authenticate=${ZWE_configs_apiml_security_saf_urls_authenticate:-${ZWE_components_gateway_apiml_security_saf_urls_authenticate:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf/authenticate"}} \ + -Dapiml.security.saf.urls.verify=${ZWE_configs_apiml_security_saf_urls_verify:-${ZWE_components_gateway_apiml_security_saf_urls_verify:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf/verify"}} \ -Dapiml.security.ssl.nonStrictVerifySslCertificatesOfServices=${nonStrictVerifySslCertificatesOfServices:-false} \ -Dapiml.security.ssl.verifySslCertificatesOfServices=${verifySslCertificatesOfServices} \ + -Dapiml.security.useInternalMapper=${ZWE_configs_apiml_security_useInternalMapper:-${ZWE_components_gateway_apiml_security_useInternalMapper:-true}} \ -Dapiml.security.x509.acceptForwardedCert=${ZWE_configs_apiml_security_x509_acceptForwardedCert:-${ZWE_components_gateway_apiml_security_x509_acceptForwardedCert:-false}} \ + -Dapiml.security.x509.acceptForwardedCert=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-true}}} \ + -Dapiml.security.x509.certificatesUrls=${CERTIFICATES_URLS} \ -Dapiml.security.x509.certificatesUrls=${ZWE_configs_apiml_security_x509_certificatesUrls:-${ZWE_configs_apiml_security_x509_certificatesUrl:-${ZWE_components_gateway_apiml_security_x509_certificatesUrls:-${ZWE_components_gateway_apiml_security_x509_certificatesUrl}}}} \ -Dapiml.security.x509.enabled=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-false}} \ + -Dapiml.security.x509.externalMapperUrl=${ZWE_configs_apiml_security_x509_externalMapperUrl:-${ZWE_components_gateway_apiml_security_x509_externalMapperUrl:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/certificate/x509/map"}} \ + -Dapiml.security.x509.externalMapperUser=${ZWE_configs_apiml_security_x509_externalMapperUser:-${ZWE_components_gateway_apiml_security_x509_externalMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ -Dapiml.security.x509.registry.allowedUsers=${ZWE_configs_apiml_security_x509_registry_allowedUsers:-${ZWE_components_gateway_apiml_security_x509_registry_allowedUsers:-}} \ + -Dapiml.security.zosmf.applid=${ZWE_configs_apiml_security_zosmf_applid:-${ZWE_components_gateway_apiml_security_zosmf_applid:-IZUDFLT}} \ -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-${ZWE_components_gateway_apiml_service_allowEncodedSlashes:-true}} \ -Dapiml.service.apimlId=${ZWE_configs_apimlId:-${ZWE_components_gateway_apimlId:-}} \ -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-${ZWE_components_gateway_apiml_service_corsEnabled:-false}} \ -Dapiml.service.externalUrl="${externalProtocol}://${ZWE_zowe_externalDomains_0}:${ZWE_zowe_externalPort}" \ -Dapiml.service.forwardClientCertEnabled=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-false}} \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ - -Dapiml.service.port=${ZWE_configs_port:-${ZWE_components_gateway_port:-7554}} \ + -Dapiml.service.port=${ZWE_components_gateway_port:-${ZWE_configs_port:-7554}} \ -Dapiml.zoweManifest=${ZWE_zowe_runtimeDirectory}/manifest.json \ -Dcaching.storage.evictionStrategy=${ZWE_configs_storage_evictionStrategy:-${ZWE_components_caching_service_storage_evictionStrategy:-reject}} \ -Dcaching.storage.infinispan.initialHosts=${ZWE_configs_storage_infinispan_initialHosts:-${ZWE_components_caching_service_storage_infinispan_initialHosts:-"localhost[7600]"}} \ -Dcaching.storage.mode=${ZWE_configs_storage_mode:-${ZWE_components_caching_service_storage_mode:-infinispan}} \ - -Dcaching.storage.size=${ZWE_configs_storage_size:${ZWE_components_caching_service_storage_size:-10000}} \ + -Dcaching.storage.size=${ZWE_configs_storage_size:-${ZWE_components_caching_service_storage_size:-10000}} \ -Dcaching.storage.vsam.name=${VSAM_FILE_NAME} \ -Deureka.client.serviceUrl.defaultZone=${ZWE_DISCOVERY_SERVICES_LIST} \ -Dfile.encoding=UTF-8 \ @@ -403,7 +431,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dserver.webSocket.connectTimeout=${ZWE_configs_server_webSocket_connectTimeout:-${ZWE_components_gateway_server_webSocket_connectTimeout:-45000}} \ -Dserver.webSocket.maxIdleTimeout=${ZWE_configs_server_webSocket_maxIdleTimeout:-${ZWE_components_gateway_server_webSocket_maxIdleTimeout:-3600000}} \ -Dserver.webSocket.requestBufferSize=${ZWE_configs_server_webSocket_requestBufferSize:-${ZWE_components_gateway_server_webSocket_requestBufferSize:-8192}} \ - -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-https} \ -Dapiml.catalog.hide.serviceInfo=${ZWE_configs_apiml_catalog_hide_serviceInfo:-${ZWE_components_apicatalog_apiml_catalog_hide_serviceInfo:-false}} \ -Dapiml.catalog.customStyle.logo=${ZWE_configs_apiml_catalog_customStyle_logo:-${ZWE_components_apicatalog_apiml_catalog_customStyle_logo:-}} \ -Dapiml.catalog.customStyle.fontFamily=${ZWE_configs_apiml_catalog_customStyle_fontFamily:-${ZWE_components_apicatalog_apiml_catalog_customStyle_fontFamily:-}} \ @@ -412,6 +439,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.catalog.customStyle.headerColor=${ZWE_configs_apiml_catalog_customStyle_headerColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_headerColor:-}} \ -Dapiml.catalog.customStyle.textColor=${ZWE_configs_apiml_catalog_customStyle_textColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_textColor:-}} \ -Dapiml.catalog.customStyle.docLink=${ZWE_configs_apiml_catalog_customStyle_docLink:-${ZWE_components_apicatalog_apiml_catalog_customStyle_docLink:-}} \ + -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-} \ -jar "${JAR_FILE}" & pid=$! diff --git a/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlRollingFileAppender.java b/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlRollingFileAppender.java index 12a283598c..c5606b2de8 100644 --- a/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlRollingFileAppender.java +++ b/apiml-utility/src/main/java/org/zowe/apiml/product/logging/ApimlRollingFileAppender.java @@ -17,6 +17,7 @@ * The conditionality is checked on the start of the Appender to limit the overhead. */ public class ApimlRollingFileAppender extends RollingFileAppender { // NOSONAR + @Override public void start() { if (verifyStartupParams()) { diff --git a/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java b/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java index 78bddc2a20..4a1337649f 100644 --- a/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java +++ b/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java @@ -13,13 +13,18 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; +import lombok.NoArgsConstructor; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; import java.lang.management.ManagementFactory; +@Component +@NoArgsConstructor public class ServiceStartupEventHandler { + public static final int DEFAULT_DELAY_FACTOR = 5; private final ApimlLogger apimlLog = ApimlLogger.of(ServiceStartupEventHandler.class, @@ -43,5 +48,7 @@ public void run() { } } }, uptime * delayFactor); + } + } diff --git a/apiml-utility/src/main/resources/logback-spring.xml b/apiml-utility/src/main/resources/logback-spring.xml index b59b5df580..f74ab515df 100644 --- a/apiml-utility/src/main/resources/logback-spring.xml +++ b/apiml-utility/src/main/resources/logback-spring.xml @@ -28,28 +28,41 @@ - - ${STORAGE_LOCATION}/${logbackService:-${logbackServiceName}}.log + + + + ${STORAGE_LOCATION}/${logbackService:-${logbackServiceName}}.log - - ${STORAGE_LOCATION}/${logbackService:-${logbackServiceName}}.%i.log - ${MIN_INDEX} - ${MAX_INDEX} - + + ${STORAGE_LOCATION}/${logbackService:-${logbackServiceName}}.%i.log + ${MIN_INDEX} + ${MAX_INDEX} + - - ${MAX_FILE_SIZE} - + + ${MAX_FILE_SIZE} + - - - ${apimlLogPattern} - - - + + + ${apimlLogPattern} + + + + + - - - - + + + + + + + + + + + + + diff --git a/apiml/.gitignore b/apiml/.gitignore index 4936402ee1..ccb1ebf516 100644 --- a/apiml/.gitignore +++ b/apiml/.gitignore @@ -37,10 +37,9 @@ out/ .vscode/ ## Caching service infinispan artifacts - - # created by infinispan during build index *.lck ispn12.* *.state + diff --git a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java index d31c181636..b326705a62 100644 --- a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java +++ b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java @@ -30,6 +30,7 @@ "org.zowe.apiml.product.version", "org.zowe.apiml.product.logging", "org.zowe.apiml.product.security", + "org.zowe.apiml.product.service", "org.zowe.apiml.security", "org.zowe.apiml.discovery" } diff --git a/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java index e065d1a2eb..8dce1b86c2 100644 --- a/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java +++ b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java @@ -53,6 +53,7 @@ public class GatewayHealthIndicator extends AbstractHealthIndicator { private AtomicBoolean discoveryAvailable = new AtomicBoolean(false); private AtomicBoolean zaasAvailable = new AtomicBoolean(false); private AtomicBoolean catalogAvailable = new AtomicBoolean(false); + private AtomicBoolean startedInformationPublished = new AtomicBoolean(false); @Override diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 6f1dcfe54d..8b89f01ac9 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -38,7 +38,7 @@ spring: redirectUri: "{baseUrl}/gateway/{action}/oauth2/code/{registrationId}" main: allow-circular-references: true - banner-mode: ${apiml.banner:"console"} + banner-mode: ${apiml.banner:"off"} web-application-type: reactive allow-bean-definition-overriding: true diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/CachingService.java b/caching-service/src/main/java/org/zowe/apiml/caching/CachingServiceApplication.java similarity index 61% rename from caching-service/src/main/java/org/zowe/apiml/caching/CachingService.java rename to caching-service/src/main/java/org/zowe/apiml/caching/CachingServiceApplication.java index 8414c5ea15..77046e2afe 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/CachingService.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/CachingServiceApplication.java @@ -12,31 +12,21 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ApplicationListener; import org.springframework.retry.annotation.EnableRetry; import org.zowe.apiml.enable.EnableApiDiscovery; import org.zowe.apiml.product.logging.annotations.EnableApimlLogger; -import org.zowe.apiml.product.service.ServiceStartupEventHandler; - -import jakarta.annotation.Nonnull; @SpringBootApplication @EnableApiDiscovery @EnableRetry @EnableApimlLogger -public class CachingService implements ApplicationListener { +public class CachingServiceApplication { public static void main(String[] args) { - SpringApplication app = new SpringApplication(CachingService.class); + SpringApplication app = new SpringApplication(CachingServiceApplication.class); app.setLogStartupInfo(false); app.run(args); } - @Override - public void onApplicationEvent(@Nonnull final ApplicationReadyEvent event) { - new ServiceStartupEventHandler().onServiceStartup("Caching Service", - ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); - } } diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/CachingServiceStartupListener.java b/caching-service/src/main/java/org/zowe/apiml/caching/CachingServiceStartupListener.java new file mode 100644 index 0000000000..af4583425b --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/CachingServiceStartupListener.java @@ -0,0 +1,32 @@ +/* + * 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.caching; + +import jakarta.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; + +@Component +@RequiredArgsConstructor +public class CachingServiceStartupListener implements ApplicationListener { + + private final ServiceStartupEventHandler handler; + + @Override + public void onApplicationEvent(@Nonnull final ApplicationReadyEvent event) { + handler.onServiceStartup("Caching Service", + ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); + } + +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java index 4d9c6fc464..dbe0a800a4 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java @@ -13,11 +13,14 @@ import lombok.Data; import lombok.ToString; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.zowe.apiml.filter.AttlsHttpHandler; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; import org.zowe.apiml.product.web.ApimlTomcatCustomizer; import org.zowe.apiml.product.web.TomcatAcceptFixConfig; import org.zowe.apiml.product.web.TomcatKeyringFix; @@ -38,4 +41,10 @@ public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setUseTrailingSlashMatch(true); } + @Bean + @ConditionalOnMissingBean(name = "modulithConfig") + ServiceStartupEventHandler serviceStartupEventHandler() { + return new ServiceStartupEventHandler(); + } + } diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceStartupListenerTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceStartupListenerTest.java new file mode 100644 index 0000000000..a23e2dbc93 --- /dev/null +++ b/caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceStartupListenerTest.java @@ -0,0 +1,49 @@ +/* + * 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.caching; + +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.boot.context.event.ApplicationReadyEvent; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CachingServiceStartupListenerTest { + + @Mock private ServiceStartupEventHandler startupEventHandler; + + private CachingServiceStartupListener listener; + + @BeforeEach + void setUp() { + listener = new CachingServiceStartupListener(startupEventHandler); + } + + @Test + void onEvent_thenNotifyStartup() { + doNothing().when(startupEventHandler).onServiceStartup(anyString(), anyInt()); + + listener.onApplicationEvent(mock(ApplicationReadyEvent.class)); + + verify(startupEventHandler, times(1)).onServiceStartup("Caching Service", 5); + } + +} 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 549dfb9882..cb80411970 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 @@ -30,7 +30,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; -import org.zowe.apiml.caching.CachingService; +import org.zowe.apiml.caching.CachingServiceApplication; import org.zowe.apiml.filter.AttlsHttpHandler; import org.zowe.apiml.util.config.SslContext; @@ -45,7 +45,7 @@ import static org.mockito.Mockito.verify; @SpringBootTest( - classes = CachingService.class, + classes = CachingServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) @ActiveProfiles("AttlsConfigTestCachingService") @@ -58,14 +58,13 @@ ) @DirtiesContext @TestInstance(Lifecycle.PER_CLASS) -public class AttlsConfigTest { +class AttlsConfigTest { @Value("${apiml.service.hostname:localhost}") String hostname; @LocalServerPort int port; - @Nested class GivenAttlsModeEnabled { @@ -115,6 +114,9 @@ void requestFailsWithAttlsReasonWithHttp() { .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) .isNotEmpty(); } + } + } + } diff --git a/discoverable-client/build.gradle b/discoverable-client/build.gradle index b71bf54972..272edb15b3 100644 --- a/discoverable-client/build.gradle +++ b/discoverable-client/build.gradle @@ -80,6 +80,7 @@ dependencies { implementation libs.spring.boot.starter.graphql testImplementation libs.spring.boot.starter.test + testImplementation(testFixtures(project(":apiml-common"))) compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java b/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java index e1b3f9b880..7e05e3b82e 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java @@ -15,6 +15,7 @@ import org.zowe.apiml.product.monitoring.LatencyUtilsConfigInitializer; import org.zowe.apiml.product.service.ServiceStartupEventHandler; import org.zowe.apiml.product.version.BuildInfo; +import lombok.RequiredArgsConstructor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -25,8 +26,11 @@ @EnableApiDiscovery @EnableWebSocket @EnableApimlLogger +@RequiredArgsConstructor public class DiscoverableClientSampleApplication implements ApplicationListener { + private final ServiceStartupEventHandler handler; + public static void main(String[] args) { SpringApplication app = new SpringApplication(DiscoverableClientSampleApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); @@ -37,7 +41,7 @@ public static void main(String[] args) { @Override public void onApplicationEvent(final ApplicationReadyEvent event) { - new ServiceStartupEventHandler().onServiceStartup("Discoverable Client Service", + handler.onServiceStartup("Discoverable Client Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); } } diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ApplicationConfiguration.java b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ApplicationConfiguration.java index e735e68138..1fd48692b4 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ApplicationConfiguration.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ApplicationConfiguration.java @@ -10,19 +10,21 @@ package org.zowe.apiml.client.configuration; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; @Configuration public class ApplicationConfiguration { + @Bean - public MessageService messageService() { + MessageService messageService() { MessageService messageService = YamlMessageServiceInstance.getInstance(); messageService.loadMessages("/utility-log-messages.yml"); messageService.loadMessages("/api-messages.yml"); messageService.loadMessages("/log-messages.yml"); return messageService; } + } diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java index b8579cc058..d9ce5ef2de 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java @@ -38,7 +38,7 @@ public class SecurityConfiguration { private boolean isAttlsEnabled; @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { HttpSecurity newConf = http.csrf(AbstractHttpConfigurer::disable) // NOSONAR .authorizeHttpRequests(requests -> requests // .requestMatchers("/api/v3/graphql/**").authenticated() @@ -54,7 +54,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean - public InMemoryUserDetailsManager userDetailsService() { + InMemoryUserDetailsManager userDetailsService() { UserDetails user = User.withDefaultPasswordEncoder() .username("user").password("pass").authorities(new SimpleGrantedAuthority("ADMIN")) .build(); @@ -63,7 +63,7 @@ public InMemoryUserDetailsManager userDetailsService() { @Bean - public WebSecurityCustomizer webSecurityCustomizer() { + WebSecurityCustomizer webSecurityCustomizer() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); firewall.setAllowUrlEncodedDoubleSlash(true); @@ -76,5 +76,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { web.httpFirewall(firewall); web.ignoring().requestMatchers("/api/v1/**"); }; + } + } diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SpringComponentsConfiguration.java b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SpringComponentsConfiguration.java index 7422b887c1..594e0cef75 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SpringComponentsConfiguration.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SpringComponentsConfiguration.java @@ -14,6 +14,7 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; /** * Configuration for Spring Boot components. @@ -21,9 +22,16 @@ */ @SpringBootConfiguration public class SpringComponentsConfiguration { + @Bean - public Jackson2ObjectMapperBuilderCustomizer failOnUnknownProperties() { + Jackson2ObjectMapperBuilderCustomizer failOnUnknownProperties() { return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder .featuresToEnable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } + + @Bean + ServiceStartupEventHandler serviceStartupEventHandler() { + return new ServiceStartupEventHandler(); + } + } diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiMediationClientTestControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiMediationClientTestControllerTest.java index 5ef213e374..cec660be8a 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiMediationClientTestControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiMediationClientTestControllerTest.java @@ -11,26 +11,27 @@ package org.zowe.apiml.client.api; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; import org.zowe.apiml.client.service.ApiMediationClientService; +import org.zowe.apiml.util.config.TestConfig; import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {ApiMediationClientTestController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class ApiMediationClientTestControllerTest { + private static final String MEDIATION_CLIENT_URI = "/api/v1/apiMediationClient"; @Autowired @@ -66,4 +67,5 @@ void isRegisteredTest_notRegistered() throws Exception { void isRegisteredTestService_notRegistered() { assertFalse(apiMediationClientService.isRegistered()); } + } diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/FileControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/FileControllerTest.java index 2caf2d36cb..4ae095bd5e 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/FileControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/FileControllerTest.java @@ -11,22 +11,21 @@ package org.zowe.apiml.client.api; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {FileController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class FileControllerTest { + @Autowired private MockMvc mockMvc; diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingControllerTest.java index d99954b4c1..05281253de 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingControllerTest.java @@ -11,23 +11,22 @@ package org.zowe.apiml.client.api; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.hamcrest.core.Is.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {GreetingController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class GreetingControllerTest { + @Autowired private MockMvc mockMvc; @@ -65,4 +64,5 @@ void callCustomGreetingEndpoint() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.content", is("Hello, " + name + "!"))); } + } diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingGraphControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingGraphControllerTest.java index 87233a5a3a..4983dfc218 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingGraphControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingGraphControllerTest.java @@ -12,21 +12,20 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.hamcrest.core.Is.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {GreetingGraphController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class GreetingGraphControllerTest { @Autowired diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingV2ControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingV2ControllerTest.java index 3c47cbe608..2f503e38bc 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingV2ControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/GreetingV2ControllerTest.java @@ -10,24 +10,23 @@ package org.zowe.apiml.client.api; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.hamcrest.core.Is.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {GreetingV2Controller.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class GreetingV2ControllerTest { + private static final String NAME = "Carson"; @Autowired diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerDeleteTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerDeleteTest.java index fcb9895766..50724e82ce 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerDeleteTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerDeleteTest.java @@ -11,25 +11,23 @@ package org.zowe.apiml.client.api; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.ApplicationConfiguration; import org.zowe.apiml.client.configuration.SecurityConfiguration; import org.zowe.apiml.client.service.PetService; +import org.zowe.apiml.util.config.TestConfig; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {PetController.class}) -@Import(value = {SecurityConfiguration.class, ApplicationConfiguration.class}) +@Import(value = {SecurityConfiguration.class, ApplicationConfiguration.class, TestConfig.class}) class PetControllerDeleteTest { @Autowired private MockMvc mockMvc; diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetAllTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetAllTest.java index 410df69a42..026f7fb944 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetAllTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetAllTest.java @@ -11,17 +11,16 @@ package org.zowe.apiml.client.api; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.ApplicationConfiguration; import org.zowe.apiml.client.configuration.SecurityConfiguration; import org.zowe.apiml.client.model.Pet; import org.zowe.apiml.client.service.PetService; +import org.zowe.apiml.util.config.TestConfig; import java.util.ArrayList; import java.util.List; @@ -34,9 +33,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {PetController.class}) -@Import(value = {SecurityConfiguration.class, ApplicationConfiguration.class}) +@Import(value = { SecurityConfiguration.class, ApplicationConfiguration.class, TestConfig.class }) class PetControllerGetAllTest { @Autowired private MockMvc mockMvc; diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetOneTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetOneTest.java index b92863fc1c..fea125afad 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetOneTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/PetControllerGetOneTest.java @@ -11,29 +11,29 @@ package org.zowe.apiml.client.api; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.ApplicationConfiguration; import org.zowe.apiml.client.configuration.SecurityConfiguration; import org.zowe.apiml.client.model.Pet; import org.zowe.apiml.client.service.PetService; +import org.zowe.apiml.util.config.TestConfig; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.core.Is.is; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {PetController.class}) -@Import(value = {SecurityConfiguration.class, ApplicationConfiguration.class}) +@Import(value = {SecurityConfiguration.class, ApplicationConfiguration.class, TestConfig.class}) class PetControllerGetOneTest { @Autowired private MockMvc mockMvc; diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/RequestInfoControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/RequestInfoControllerTest.java index c29955df02..32bba95003 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/RequestInfoControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/RequestInfoControllerTest.java @@ -12,24 +12,22 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; -import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {RequestInfoController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class RequestInfoControllerTest { @Autowired @@ -48,6 +46,7 @@ void whenRequest_thenReturnInfo() throws Exception { .andExpect(jsonPath("$.cookies", aMapWithSize(0))) .andExpect(jsonPath("$.content", is(""))); } + } } diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/StatusCodeControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/StatusCodeControllerTest.java index bb58b75df8..1f34028f8c 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/StatusCodeControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/StatusCodeControllerTest.java @@ -12,22 +12,20 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {StatusCodeController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class StatusCodeControllerTest { @Autowired diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/WebServiceControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/WebServiceControllerTest.java index 00e5ea6542..8c8c5b4740 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/WebServiceControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/WebServiceControllerTest.java @@ -12,22 +12,20 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.hamcrest.core.Is.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {WebServiceController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class WebServiceControllerTest { @Autowired diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/X509ControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/X509ControllerTest.java index a63ea7351d..7f5de6fcc1 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/X509ControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/X509ControllerTest.java @@ -11,22 +11,20 @@ package org.zowe.apiml.client.api; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.hamcrest.CoreMatchers.hasItems; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = X509Controller.class) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class X509ControllerTest { @Autowired MockMvc mockMvc; diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/ZaasClientTestControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/ZaasClientTestControllerTest.java index 94aa917d0f..95cbe5e226 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/api/ZaasClientTestControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/ZaasClientTestControllerTest.java @@ -35,6 +35,7 @@ @WebMvcTest(controllers = {ZaasClientTestController.class}) @Import(value = {SecurityConfiguration.class, SpringComponentsConfiguration.class, ApplicationConfiguration.class, AnnotationConfigContextLoader.class}) class ZaasClientTestControllerTest { + @Autowired private MockMvc mockMvc; diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/service/ApiMediationClientServiceTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/service/ApiMediationClientServiceTest.java index 1081ebdb24..b5d341b012 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/service/ApiMediationClientServiceTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/service/ApiMediationClientServiceTest.java @@ -77,7 +77,7 @@ void unregisterTest_notRegistered() { @Profile("test") public static class TestConfig { @Bean - public DiscoverableClientConfig discoverableClientConfig() { + DiscoverableClientConfig discoverableClientConfig() { return new DiscoverableClientConfig(); } } diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/sse/ServerSentEventControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/sse/ServerSentEventControllerTest.java index 50da394c1a..6044a67753 100644 --- a/discoverable-client/src/test/java/org/zowe/apiml/client/sse/ServerSentEventControllerTest.java +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/sse/ServerSentEventControllerTest.java @@ -11,20 +11,18 @@ package org.zowe.apiml.client.sse; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {ServerSentEventController.class}) -@Import(SecurityConfiguration.class) +@Import({ SecurityConfiguration.class, TestConfig.class }) class ServerSentEventControllerTest { @Autowired private MockMvc mockMvc; diff --git a/discovery-package/src/main/resources/bin/start.sh b/discovery-package/src/main/resources/bin/start.sh index b06405a600..fcd56f7ad6 100755 --- a/discovery-package/src/main/resources/bin/start.sh +++ b/discovery-package/src/main/resources/bin/start.sh @@ -42,6 +42,15 @@ # - ZWE_zowe_certificate_keystore_type - The default keystore type to use for SSL certificates # - ZWE_zowe_verifyCertificates - if we accept only verified certificates # - ZWE_configs_apiml_discovery_serviceIdPrefixReplacer - The service ID prefix replacer to be V2 conformant + +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + if [ -n "${LAUNCH_COMPONENT}" ]; then JAR_FILE="${LAUNCH_COMPONENT}/discovery-service-lite.jar" else @@ -61,11 +70,7 @@ if [ -z "${LIBRARY_PATH}" ]; then fi if [ "${ZWE_configs_debug}" = "true" ]; then - if [ -n "${ZWE_configs_spring_profiles_active}" ]; - then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}debug" + add_profile "debug" fi # FIXME: APIML_DIAG_MODE_ENABLED is not officially mentioned. We can still use it behind the scene, @@ -127,10 +132,11 @@ fi if [ "${ATTLS_ENABLED}" = "true" ]; then ZWE_configs_server_ssl_enabled="false" - if [ -n "${ZWE_configs_spring_profiles_active}" ]; then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}attls" + add_profile "attls" +fi + +if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" ]; then + add_profile "https" fi ZWE_DISCOVERY_SERVICES_LIST=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} @@ -293,7 +299,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${DISCOVERY_CODE} ${JAVA_BIN_DIR}java \ -Dserver.ssl.trustStore="${truststore_location}" \ -Dserver.ssl.trustStorePassword="${truststore_pass}" \ -Dserver.ssl.trustStoreType="${truststore_type}" \ - -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-https} \ + -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-} \ -jar "${JAR_FILE}" & pid=$! echo "pid=${pid}" diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/DiscoveryServiceApplication.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/DiscoveryServiceApplication.java index ce7074763c..ef1b27d74f 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/DiscoveryServiceApplication.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/DiscoveryServiceApplication.java @@ -11,6 +11,7 @@ package org.zowe.apiml.discovery; import jakarta.annotation.Nonnull; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -30,13 +31,17 @@ @ComponentScan({ "org.zowe.apiml.discovery", "org.zowe.apiml.product.security", - "org.zowe.apiml.product.web" + "org.zowe.apiml.product.web", + "org.zowe.apiml.product.service", }) @EnableApimlLogger @EnableWebSecurity @EnableConfigurationProperties(SafSecurityConfigurationProperties.class) public class DiscoveryServiceApplication implements ApplicationListener { + @Autowired + private ServiceStartupEventHandler startupEventHandler; + public static void main(String[] args) { SpringApplication app = new SpringApplication(DiscoveryServiceApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); @@ -47,6 +52,7 @@ public static void main(String[] args) { @Override public void onApplicationEvent(@Nonnull final ApplicationReadyEvent event) { - new ServiceStartupEventHandler().onServiceStartup("Discovery Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); + startupEventHandler.onServiceStartup("Discovery Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); } + } diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListener.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListener.java index c6b7574f22..965f613f8e 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListener.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListener.java @@ -10,12 +10,14 @@ package org.zowe.apiml.discovery; -import org.zowe.apiml.discovery.staticdef.StaticServicesRegistrationService; import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; +import org.zowe.apiml.discovery.staticdef.StaticServicesRegistrationService; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; + +import java.util.concurrent.atomic.AtomicBoolean; /** * Called by Eureka when its service registry is initialized. @@ -24,14 +26,18 @@ */ @Component @RequiredArgsConstructor -@ConditionalOnMissingBean(name = "modulithConfig") public class EurekaRegistryAvailableListener implements ApplicationListener { private final StaticServicesRegistrationService registrationService; + private final ServiceStartupEventHandler handler; + private final AtomicBoolean startUpInfoPublished = new AtomicBoolean(false); @Override public void onApplicationEvent(EurekaRegistryAvailableEvent event) { registrationService.registerServices(); + if (startUpInfoPublished.compareAndSet(false, true)) { + handler.onServiceStartup("Discovery Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); + } } } diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListenerTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListenerTest.java new file mode 100644 index 0000000000..6f733c929f --- /dev/null +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaRegistryAvailableListenerTest.java @@ -0,0 +1,67 @@ +/* + * 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.discovery; + +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.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; +import org.zowe.apiml.discovery.staticdef.StaticServicesRegistrationService; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@ExtendWith(MockitoExtension.class) +class EurekaRegistryAvailableListenerTest { + + @Mock private StaticServicesRegistrationService registrationService; + @Mock private ServiceStartupEventHandler startupEventHandler; + + private EurekaRegistryAvailableListener listener; + + @BeforeEach + void setUp() { + this.listener = new EurekaRegistryAvailableListener(registrationService, startupEventHandler); + } + + @Test + void onEvent_notifyStartup() { + doNothing().when(registrationService).registerServices(); + doNothing().when(startupEventHandler).onServiceStartup(anyString(), anyInt()); + + listener.onApplicationEvent(mock(EurekaRegistryAvailableEvent.class)); + + verify(startupEventHandler, times(1)).onServiceStartup("Discovery Service", 5); + } + + @Test + void onMultipleEvents_notifyOnce() { + doNothing().when(registrationService).registerServices(); + doNothing().when(startupEventHandler).onServiceStartup(anyString(), anyInt()); + + listener.onApplicationEvent(mock(EurekaRegistryAvailableEvent.class)); + + verify(startupEventHandler, times(1)).onServiceStartup("Discovery Service", 5); + + listener.onApplicationEvent(mock(EurekaRegistryAvailableEvent.class)); + + verifyNoMoreInteractions(startupEventHandler); + } + +} diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java index 409246c885..c7e4486fd2 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java @@ -16,8 +16,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.util.config.TestConfig; import org.zowe.apiml.discovery.functional.DiscoveryFunctionalTest; import java.io.IOException; @@ -36,6 +38,7 @@ ) @TestInstance(Lifecycle.PER_CLASS) @ActiveProfiles("attls") +@Import(TestConfig.class) class AttlsConfigTest extends DiscoveryFunctionalTest { private String protocol = "http"; diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/functional/DiscoveryFunctionalTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/functional/DiscoveryFunctionalTest.java index a735f12272..0826036b07 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/functional/DiscoveryFunctionalTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/functional/DiscoveryFunctionalTest.java @@ -15,14 +15,17 @@ import org.springframework.beans.factory.annotation.Value; 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.discovery.DiscoveryServiceApplication; +import org.zowe.apiml.util.config.TestConfig; @SpringBootTest( classes = DiscoveryServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT ) @DirtiesContext +@Import(TestConfig.class) public abstract class DiscoveryFunctionalTest { protected static final String DISCOVERY_REALM = "API Mediation Discovery Service realm"; diff --git a/docker-compose.yml b/docker-compose.yml index 469920d496..15740b8b44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: - apiml_shared container_name: api-catalog-services volumes: - - /Users/pc891986/Projects/api-layer-v3/config/docker/api-defs:/api-defs + - /api-defs:/api-defs # Update to an existing local directory environment: - APIML_SERVICE_DISCOVERYSERVICEURLS=https://apiml:10011/eureka,https://apiml-2:10011/eureka - APIML_SERVICE_HOSTNAME=api-catalog-services @@ -58,7 +58,7 @@ services: - apiml2_net - apiml_shared volumes: - - /Users/pc891986/Projects/api-layer-v3/config/docker/api-defs:/api-defs + - /api-defs:/api-defs # Update to an existing local directory container_name: apiml-2 environment: - APIML_SECURITY_AUTH_PROVIDER=saf diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java index 92b4a92264..74549f2888 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java @@ -22,6 +22,7 @@ "org.zowe.apiml.product.gateway", "org.zowe.apiml.product.version", "org.zowe.apiml.product.logging", + "org.zowe.apiml.product.service", "org.zowe.apiml.security" }, exclude = {ReactiveOAuth2ClientAutoConfiguration.class} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java index ec2ef28d4b..c6185b0cb2 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/ZaasStartupListener.java @@ -32,6 +32,7 @@ public class ZaasStartupListener implements ApplicationListener Date: Mon, 28 Jul 2025 23:42:00 +0200 Subject: [PATCH 035/152] fix: ZAAS startup with `apiml.security.oidc.validationType` = `endpoint` (#4241) Signed-off-by: Elena Kubantseva Signed-off-by: Gowtham Selvaraj --- .../zowe/apiml/util/HttpClientMockHelper.java | 11 +++++ .../token/OIDCTokenProviderEndpoint.java | 15 +++--- .../OIDCTokenProviderJWKEndpointTest.java | 48 ++++++++----------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java index f7015e9e04..491333c969 100644 --- a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java +++ b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java @@ -16,6 +16,7 @@ import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.entity.BasicHttpEntity; import org.mockito.ArgumentMatchers; @@ -48,6 +49,12 @@ public static T invokeResponseHandler(InvocationOnMock invocation, ClassicHt } + public static void mockResponse(ClassicHttpResponse responseMock, int statusCode, String responseBody, Header... headers) { + mockResponse(responseMock, statusCode); + mockResponse(responseMock, responseBody); + mockResponse(responseMock, headers); + } + public static void mockResponse(ClassicHttpResponse responseMock, int statusCode, String responseBody) { mockResponse(responseMock, statusCode); mockResponse(responseMock, responseBody); @@ -61,4 +68,8 @@ public static void mockResponse(ClassicHttpResponse responseMock, String respons public static void mockResponse(ClassicHttpResponse responseMock, int statusCode) { Mockito.when(responseMock.getCode()).thenReturn(statusCode); } + + public static void mockResponse(ClassicHttpResponse responseMock, Header... headers) { + Mockito.when(responseMock.getHeaders()).thenReturn(headers); + } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java index 11e52a4653..404c28c0eb 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java @@ -12,10 +12,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.http.HttpStatus; @@ -43,11 +42,11 @@ public boolean isValid(String token) { HttpGet httpGet = new HttpGet(endpointUrl); httpGet.addHeader(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + token); - HttpResponse httpResponse = secureHttpClientWithKeystore.execute(httpGet); - - int responseCode = httpResponse.getStatusLine().getStatusCode(); - log.debug("Response code: {}", responseCode); - return HttpStatus.valueOf(responseCode).is2xxSuccessful(); + return secureHttpClientWithKeystore.execute(httpGet, response -> { + final int responseCode = response.getCode(); + log.debug("Response code: {}", responseCode); + return HttpStatus.valueOf(responseCode).is2xxSuccessful(); + }); } catch (IOException e) { log.error("An error occurred during validation of OIDC token using userInfo URI {}: {}", endpointUrl, e.getMessage()); return false; diff --git a/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderJWKEndpointTest.java b/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderJWKEndpointTest.java index 949ef46b6d..d39905fffd 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderJWKEndpointTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderJWKEndpointTest.java @@ -13,20 +13,18 @@ import io.restassured.RestAssured; import io.restassured.config.RestAssuredConfig; import io.restassured.config.SSLConfig; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpStatus; -import org.apache.http.Header; -import org.apache.http.ProtocolVersion; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpUriRequest; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.message.BasicStatusLine; import org.hamcrest.CoreMatchers; import org.hamcrest.core.Is; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentMatchers; @@ -40,8 +38,10 @@ import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.util.HttpClientMockHelper; import org.zowe.apiml.zaas.ZaasApplication; import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderEndpoint; @@ -77,28 +77,20 @@ class OIDCTokenProviderJWKEndpointTest { @LocalServerPort private int port; - private String zaasEndpoint; private RestAssuredConfig withClientCert; - @Autowired HttpConfig httpConfig; - - @Autowired - OIDCTokenProviderEndpoint oidcTokenProviderEndpoint; - - @Autowired - CloseableHttpClient secureHttpClientWithKeystore; + @MockitoSpyBean + CloseableHttpClient httpClientSecure; private CloseableHttpResponse createResponse(int responseCode, String body, Header...headers) { CloseableHttpResponse response = mock(CloseableHttpResponse.class); - Mockito.doReturn(new BasicStatusLine(new ProtocolVersion("http", 1, 1), responseCode, "")).when(response).getStatusLine(); - Mockito.doReturn(new StringEntity(body, ContentType.APPLICATION_JSON)).when(response).getEntity(); - Mockito.doReturn(headers).when(response).getAllHeaders(); + HttpClientMockHelper.mockResponse(response, responseCode, body, headers); return response; } - @BeforeAll + @BeforeEach public void init() throws IOException { zaasEndpoint = "https://localhost:" + port + CONTROLLER_PATH + "/zoweJwt"; @@ -106,13 +98,13 @@ public void init() throws IOException { withClientCert = RestAssuredConfig.newConfig().sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(sslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER))); // NOSONAR Mockito.doAnswer(invocation -> { - HttpUriRequest request = invocation.getArgument(0); + HttpGet request = invocation.getArgument(0); String authHeader = request.getFirstHeader(HttpHeaders.AUTHORIZATION).getValue(); - if (StringUtils.equals(ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN, authHeader)) { - return createResponse(HttpStatus.SC_OK, "{\"detail\":\"information\")"); + if (Strings.CS.equals(ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN, authHeader)) { + return HttpClientMockHelper.invokeResponseHandler(invocation, createResponse(HttpStatus.SC_OK, "{\"detail\":\"information\")")); } - return createResponse(HttpStatus.SC_UNAUTHORIZED, "{\"error\":\"message\")"); - }).when(secureHttpClientWithKeystore).execute(ArgumentMatchers.any()); + return HttpClientMockHelper.invokeResponseHandler(invocation, createResponse(HttpStatus.SC_UNAUTHORIZED, "{\"error\":\"message\")")); + }).when(httpClientSecure).execute(ArgumentMatchers.any(ClassicHttpRequest.class), ArgumentMatchers.any(HttpClientResponseHandler.class)); } @Test @@ -177,7 +169,7 @@ public AuthenticationMapper oidcMapper() { } @Bean - CloseableHttpClient secureHttpClientWithKeystore() { + CloseableHttpClient httpClientSecure() { return mock(CloseableHttpClient.class); } From c93e2fed3e93c7401c78bbd2807f2472b696f67f Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 29 Jul 2025 14:27:44 +0200 Subject: [PATCH 036/152] fix: api catalog startup message (#4240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pablo Carle Signed-off-by: Pavel Jareš Co-authored-by: Pablo Carle Co-authored-by: Pavel Jareš Signed-off-by: Gowtham Selvaraj --- .../ApiCatalogServiceAvailableEvent.java | 23 +++++++ .../apiml/apicatalog/AppReadyListener.java | 5 +- .../zowe/apiml/GatewayHealthIndicator.java | 35 ++++++++-- .../java/org/zowe/apiml/ModulithConfig.java | 27 ++++++-- .../apiml/GatewayHealthIndicatorTest.java | 65 ++++++++++++++++++- .../apiml/discovery/config/BeanConfig.java | 2 + 6 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogServiceAvailableEvent.java diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogServiceAvailableEvent.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogServiceAvailableEvent.java new file mode 100644 index 0000000000..83cf7bf863 --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/ApiCatalogServiceAvailableEvent.java @@ -0,0 +1,23 @@ +/* + * 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.apicatalog; + +import org.springframework.context.ApplicationEvent; + +public class ApiCatalogServiceAvailableEvent extends ApplicationEvent { + + private static final long serialVersionUID = 9223372036854775807L; + + public ApiCatalogServiceAvailableEvent(Object source) { + super(source); + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java index c1280e297e..153449a0b9 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/AppReadyListener.java @@ -11,6 +11,7 @@ package org.zowe.apiml.apicatalog; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -21,6 +22,7 @@ */ @Component @RequiredArgsConstructor +@ConditionalOnMissingBean(name = "modulithConfig") public class AppReadyListener { private final ServiceStartupEventHandler handler; @@ -33,8 +35,7 @@ public class AppReadyListener { */ @EventListener public void onApplicationEvent(ApplicationReadyEvent event) { - handler.onServiceStartup("API Catalog Service", - ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); + handler.onServiceStartup("API Catalog Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); } } diff --git a/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java index 8dce1b86c2..f4d37f5425 100644 --- a/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java +++ b/apiml/src/main/java/org/zowe/apiml/GatewayHealthIndicator.java @@ -11,7 +11,9 @@ package org.zowe.apiml; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health.Builder; @@ -19,12 +21,15 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; +import org.springframework.context.ApplicationContext; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import org.zowe.apiml.apicatalog.ApiCatalogServiceAvailableEvent; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.compatibility.ApimlHealthCheckHandler; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; import org.zowe.apiml.zaas.ZaasServiceAvailableEvent; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,9 +45,11 @@ */ @Component @RequiredArgsConstructor +@Slf4j public class GatewayHealthIndicator extends AbstractHealthIndicator { - private final DiscoveryClient discoveryClient; + private final ApplicationContext applicationContext; + private final ServiceStartupEventHandler serviceStartupEventHandler; @InjectApimlLogger private final ApimlLogger apimlLog = ApimlLogger.empty(); @@ -59,10 +66,18 @@ public class GatewayHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck(Builder builder) throws Exception { var anyCatalogIsAvailable = StringUtils.isNotBlank(apiCatalogServiceId); - catalogAvailable.set(anyCatalogIsAvailable && !this.discoveryClient.getInstances(apiCatalogServiceId).isEmpty()); + DiscoveryClient discoveryClient; + try { + discoveryClient = applicationContext.getBean(DiscoveryClient.class); + } catch (BeansException e) { + log.debug("DiscoveryClient is not available", e); + return; + } + + catalogAvailable.set(anyCatalogIsAvailable && !discoveryClient.getInstances(apiCatalogServiceId).isEmpty()); // Keeping for backwards compatibility, in modulith the amount of gateways is the amount of authentication services available - var gatewayCount = this.discoveryClient.getInstances(CoreService.GATEWAY.getServiceId()).size(); + var gatewayCount = discoveryClient.getInstances(CoreService.GATEWAY.getServiceId()).size(); var zaasCount = gatewayCount; builder.status(toStatus(discoveryAvailable.get() && zaasAvailable.get())) @@ -109,8 +124,18 @@ public void onApplicationEvent(EurekaRegistryAvailableEvent event) { @EventListener public void onApplicationEvent(EurekaInstanceRegisteredEvent event) { var instanceInfo = event.getInstanceInfo(); - if (String.valueOf(instanceInfo.getAppName()).equalsIgnoreCase(apiCatalogServiceId)) { - catalogAvailable.set(true); + if (String.valueOf(instanceInfo.getAppName()).equalsIgnoreCase(apiCatalogServiceId) && catalogAvailable.compareAndSet(false, true)) { + serviceStartupEventHandler.onServiceStartup("API Catalog Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); + } + if (isFullyUp()) { + onFullyUp(); + } + } + + @EventListener + public void onApplicationEvent(ApiCatalogServiceAvailableEvent event) { + if (catalogAvailable.compareAndSet(false, true)) { + serviceStartupEventHandler.onServiceStartup("API Catalog Service", ServiceStartupEventHandler.DEFAULT_DELAY_FACTOR); } if (isFullyUp()) { onFullyUp(); diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index a2f35e4414..67b3ef77f9 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -19,7 +19,13 @@ import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; import jakarta.annotation.PostConstruct; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.Context; @@ -39,6 +45,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; import org.springframework.context.event.EventListener; import org.springframework.http.server.reactive.HttpHandler; @@ -46,6 +53,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.web.context.ServletContextAware; +import org.zowe.apiml.apicatalog.ApiCatalogServiceAvailableEvent; import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.filter.PreFluxFilter; @@ -58,12 +66,20 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Timer; +import java.util.TimerTask; @EnableScheduling @Configuration @RequiredArgsConstructor @EnableConfigurationProperties +@DependsOn(value = { "gatewayHealthIndicator" }) @Slf4j public class ModulithConfig { @@ -73,6 +89,7 @@ public class ModulithConfig { private final CatalogEurekaInstanceConfigBean catalogEurekaInstanceConfigBean; private final EurekaClientConfig eurekaConfig; private final CachingServiceEurekaInstanceConfigBean cachingServiceEurekaInstanceConfigBean; + private final ApplicationEventPublisher eventPublisher; private final Timer timer = new Timer("PeerReplicated-StaticServices"); @@ -135,7 +152,7 @@ private InstanceInfo getInstanceInfo(String serviceId) { .build(); } - private ApimlInstanceRegistry getRegistry() { + static ApimlInstanceRegistry getRegistry() { return Optional.ofNullable(EurekaServerContextHolder.getInstance()) .map(EurekaServerContextHolder::getServerContext) .map(EurekaServerContext::getRegistry) @@ -158,6 +175,7 @@ void createLocalInstances() { @EventListener(ApplicationReadyEvent.class) public void onApplicationStart() { log.info("Initialize timer for static services peer-replicated heartbeats"); + eventPublisher.publishEvent(new ApiCatalogServiceAvailableEvent(new Object())); // This timer calls Eureka registry's peerReplicate method to accumulate all heartbeats of statically-onboarded services once timer.scheduleAtFixedRate(new TimerTask() { @@ -184,7 +202,6 @@ public void periodicJwtInit() { } } - @Bean ReactiveDiscoveryClient registryReactiveDiscoveryClient(DiscoveryClient registryDiscoveryClient) { return new ReactiveDiscoveryClient() { @@ -249,10 +266,12 @@ public List getServices() { } @Bean + @Primary MessageService messageService() { MessageService messageService = YamlMessageServiceInstance.getInstance(); messageService.loadMessages("/utility-log-messages.yml"); messageService.loadMessages("/common-log-messages.yml"); + messageService.loadMessages("/security-common-log-messages.yml"); messageService.loadMessages("/discovery-log-messages.yml"); messageService.loadMessages("/gateway-log-messages.yml"); diff --git a/apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java b/apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java index c12f5d045e..2ccb3bbc16 100644 --- a/apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java +++ b/apiml/src/test/java/org/zowe/apiml/GatewayHealthIndicatorTest.java @@ -10,6 +10,7 @@ package org.zowe.apiml; +import com.netflix.appinfo.InstanceInfo; import com.netflix.eureka.EurekaServerConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -17,33 +18,46 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; +import org.springframework.context.ApplicationContext; import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.apicatalog.ApiCatalogServiceAvailableEvent; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.service.ServiceStartupEventHandler; import org.zowe.apiml.zaas.ZaasServiceAvailableEvent; import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class GatewayHealthIndicatorTest { @Mock private DiscoveryClient discoveryClient; + @Mock private ApplicationContext applicationContext; + @Mock private ServiceStartupEventHandler serviceStartupEventHandler; private GatewayHealthIndicator healthIndicator; @BeforeEach void setUp() { - healthIndicator = new GatewayHealthIndicator(discoveryClient); + healthIndicator = new GatewayHealthIndicator(applicationContext, serviceStartupEventHandler); ReflectionTestUtils.setField(healthIndicator, "apiCatalogServiceId", CoreService.API_CATALOG.getServiceId()); + lenient().when(applicationContext.getBean(DiscoveryClient.class)).thenReturn(discoveryClient); } private DefaultServiceInstance getDefaultServiceInstance(String serviceId, String hostname, int port) { @@ -60,6 +74,7 @@ class WhenCatalogAndDiscoveryAreAvailable { void setUp() { healthIndicator.onApplicationEvent(new EurekaRegistryAvailableEvent(mock(EurekaServerConfig.class))); healthIndicator.onApplicationEvent(new ZaasServiceAvailableEvent("dummy")); + healthIndicator.onApplicationEvent(new ApiCatalogServiceAvailableEvent(new Object())); } @Test @@ -96,6 +111,17 @@ void thenStatusIsDown() throws Exception { assertEquals(Status.DOWN, builder.build().getStatus()); } + @Test + void whenClientNotAvailable_thenDoNothing() throws Exception { + when(applicationContext.getBean(DiscoveryClient.class)).thenThrow(new NoSuchBeanDefinitionException(DiscoveryClient.class)); + + Health.Builder builder = new Health.Builder(); + healthIndicator.doHealthCheck(builder); + + verifyNoInteractions(serviceStartupEventHandler); + verifyNoInteractions(discoveryClient); + } + } @Nested @@ -166,4 +192,41 @@ void whenHealthRequested_onceLogMessageAboutStartup() throws Exception { } + @Nested + class OnCatalogRegistration { + + @Test + void whenBothEvents_thenOneMessage() { + var registeredEvent = mock(EurekaInstanceRegisteredEvent.class); + + var instanceInfo = mock(InstanceInfo.class); + when(registeredEvent.getInstanceInfo()).thenReturn(instanceInfo); + when(instanceInfo.getAppName()).thenReturn("apicatalog"); + + doNothing().when(serviceStartupEventHandler).onServiceStartup("API Catalog Service", 5); + + healthIndicator.onApplicationEvent(new ApiCatalogServiceAvailableEvent(new Object())); + healthIndicator.onApplicationEvent(registeredEvent); + + verify(serviceStartupEventHandler, times(1)).onServiceStartup("API Catalog Service", 5); + } + + @Test + void whenBothEventsReverse_thenOneMessage() { + var registeredEvent = mock(EurekaInstanceRegisteredEvent.class); + + var instanceInfo = mock(InstanceInfo.class); + when(registeredEvent.getInstanceInfo()).thenReturn(instanceInfo); + when(instanceInfo.getAppName()).thenReturn("apicatalog"); + + doNothing().when(serviceStartupEventHandler).onServiceStartup("API Catalog Service", 5); + + healthIndicator.onApplicationEvent(registeredEvent); + healthIndicator.onApplicationEvent(new ApiCatalogServiceAvailableEvent(new Object())); + + verify(serviceStartupEventHandler, times(1)).onServiceStartup("API Catalog Service", 5); + } + + } + } diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/BeanConfig.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/BeanConfig.java index f629c90d58..7bc063f47e 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/BeanConfig.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/BeanConfig.java @@ -10,6 +10,7 @@ package org.zowe.apiml.discovery.config; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.springframework.context.annotation.Bean; @@ -24,6 +25,7 @@ public class BeanConfig { @Bean @Primary + @ConditionalOnMissingBean(name = "modulithConfig") public MessageService messageServiceDiscovery() { MessageService messageService = YamlMessageServiceInstance.getInstance(); messageService.loadMessages("/utility-log-messages.yml"); From 1afe38b6be28d7a394b2da41aa0d4b9db36d9952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= <58428711+pj892031@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:02:48 +0200 Subject: [PATCH 037/152] refactor: Call refresh of static services endpoint via API to reduce REST calls in Modulith (#4242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Gowtham Selvaraj --- .../api/StaticAPIRefreshController.java | 14 ++--- .../staticapi/StaticRegistrationService.java | 17 ++++++ .../StaticRegistrationServiceApi.java | 43 +++++++++++++++ ...ava => StaticRegistrationServiceRest.java} | 5 +- .../api/StaticAPIRefreshControllerTest.java | 10 ++-- .../StaticRegistrationServiceApiTest.java | 54 +++++++++++++++++++ ...=> StaticRegistrationServiceRestTest.java} | 18 +++---- .../zowe/apiml/product/discovery}/Route.java | 9 ++-- .../apiml/product/discovery}/Service.java | 7 ++- .../product/discovery}/ServiceOverride.java | 2 +- .../discovery}/ServiceOverrideData.java | 2 +- .../discovery}/StaticRegistrationResult.java | 2 +- .../discovery/StaticServicesRegistration.java | 22 ++++++++ ...taticDefinitionsRefreshRestController.java | 2 +- ...cDefinitionsRefreshRestControllerTest.java | 2 +- .../metadata/MetadataDefaultsService.java | 2 +- .../apiml/discovery/staticdef/Definition.java | 2 + .../staticdef/ServiceDefinitionProcessor.java | 1 + .../staticdef/StaticApiRestController.java | 1 + .../StaticServicesRegistrationService.java | 5 +- .../metadata/MetadataDefaultsServiceTest.java | 2 +- .../ServiceDefinitionProcessorTest.java | 3 ++ .../StaticApiRestControllerTest.java | 1 + ...StaticServicesRegistrationServiceTest.java | 1 + 24 files changed, 192 insertions(+), 35 deletions(-) create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java rename api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/{StaticAPIService.java => StaticRegistrationServiceRest.java} (94%) create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java rename api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/{StaticAPIServiceTest.java => StaticRegistrationServiceRestTest.java} (91%) rename {discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef => apiml-common/src/main/java/org/zowe/apiml/product/discovery}/Route.java (81%) rename {discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef => apiml-common/src/main/java/org/zowe/apiml/product/discovery}/Service.java (94%) rename {discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef => apiml-common/src/main/java/org/zowe/apiml/product/discovery}/ServiceOverride.java (95%) rename {discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef => apiml-common/src/main/java/org/zowe/apiml/product/discovery}/ServiceOverrideData.java (92%) rename {discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef => apiml-common/src/main/java/org/zowe/apiml/product/discovery}/StaticRegistrationResult.java (95%) create mode 100644 apiml-common/src/main/java/org/zowe/apiml/product/discovery/StaticServicesRegistration.java diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java index 830f10046e..3cbd242a7a 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java @@ -18,18 +18,18 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.zowe.apiml.apicatalog.staticapi.StaticAPIService; +import org.zowe.apiml.apicatalog.staticapi.StaticRegistrationService; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @RequiredArgsConstructor public class StaticAPIRefreshController { - private final StaticAPIService staticAPIService; + private final StaticRegistrationService staticRegistrationService; @PostMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE) public Mono> refreshStaticApis() { - return Mono.fromCallable(staticAPIService::refresh) + return Mono.fromCallable(staticRegistrationService::refresh) .map(staticAPIResponse -> ResponseEntity .status(staticAPIResponse.getStatusCode()) .body(staticAPIResponse.getBody()) @@ -44,8 +44,8 @@ public Mono> refreshStaticApis() { @ConditionalOnBean(name = "modulithConfig") class StaticAPIRefreshControllerModulith extends StaticAPIRefreshController { - public StaticAPIRefreshControllerModulith(StaticAPIService staticAPIService) { - super(staticAPIService); + public StaticAPIRefreshControllerModulith(StaticRegistrationService staticRegistrationService) { + super(staticRegistrationService); } } @@ -55,8 +55,8 @@ public StaticAPIRefreshControllerModulith(StaticAPIService staticAPIService) { @ConditionalOnMissingBean(name = "modulithConfig") class StaticAPIRefreshControllerMicroservice extends StaticAPIRefreshController { - public StaticAPIRefreshControllerMicroservice(StaticAPIService staticAPIService) { - super(staticAPIService); + public StaticAPIRefreshControllerMicroservice(StaticRegistrationService staticRegistrationService) { + super(staticRegistrationService); } } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java new file mode 100644 index 0000000000..4208683866 --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java @@ -0,0 +1,17 @@ +/* + * 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.apicatalog.staticapi; + +public interface StaticRegistrationService { + + StaticAPIResponse refresh(); + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java new file mode 100644 index 0000000000..2704ed9cfb --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java @@ -0,0 +1,43 @@ +/* + * 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.apicatalog.staticapi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; +import org.zowe.apiml.product.discovery.StaticServicesRegistration; + +import static org.apache.hc.core5.http.HttpStatus.SC_OK; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnBean(name = "modulithConfig") +public class StaticRegistrationServiceApi implements StaticRegistrationService { + + private ObjectMapper mapper = new ObjectMapper(); + private final StaticServicesRegistration staticServicesRegistration; + + @Override + public StaticAPIResponse refresh() { + try { + var result = staticServicesRegistration.reloadServices(); + return new StaticAPIResponse(SC_OK, mapper.writeValueAsString(result)); + } catch (JsonProcessingException e) { + log.error("Cannot serialize the list of static API services", e); + throw new IllegalStateException(e); + } + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java similarity index 94% rename from api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java rename to api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java index 16dbe73e17..b46d101b3a 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java @@ -18,6 +18,7 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -29,7 +30,8 @@ @Service @RequiredArgsConstructor @Slf4j -public class StaticAPIService { +@ConditionalOnMissingBean(name = "modulithConfig") +public class StaticRegistrationServiceRest implements StaticRegistrationService { private static final String REFRESH_ENDPOINT = "discovery/api/v1/staticApi"; @@ -47,6 +49,7 @@ public class StaticAPIService { private final DiscoveryConfigProperties discoveryConfigProperties; + @Override public StaticAPIResponse refresh() { List discoveryServiceUrls = getDiscoveryServiceUrls(); for (int i = 0; i < discoveryServiceUrls.size(); i++) { diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java index e5bf992988..202eb81096 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java @@ -24,7 +24,7 @@ import org.zowe.apiml.apicatalog.controllers.handlers.StaticDefinitionControllerExceptionHandler; import org.zowe.apiml.apicatalog.exceptions.ServiceNotFoundException; import org.zowe.apiml.apicatalog.staticapi.StaticAPIResponse; -import org.zowe.apiml.apicatalog.staticapi.StaticAPIService; +import org.zowe.apiml.apicatalog.staticapi.StaticRegistrationServiceRest; import org.zowe.apiml.apicatalog.staticapi.StaticDefinitionGenerator; import static org.hamcrest.Matchers.equalTo; @@ -47,14 +47,14 @@ class StaticAPIRefreshControllerTest { private WebTestClient webTestClient; @MockitoBean - private StaticAPIService staticAPIService; + private StaticRegistrationServiceRest staticServiceRest; @MockitoBean private StaticDefinitionGenerator staticDefinitionGenerator; @Test void givenServiceNotFoundException_whenCallRefreshAPI_thenResponseShouldBe503WithSpecificMessage() { - when(staticAPIService.refresh()).thenThrow( + when(staticServiceRest.refresh()).thenThrow( new ServiceNotFoundException("Exception") ); @@ -70,7 +70,7 @@ void givenServiceNotFoundException_whenCallRefreshAPI_thenResponseShouldBe503Wit @Test void givenRestClientException_whenCallRefreshAPI_thenResponseShouldBe500WithSpecificMessage() { - when(staticAPIService.refresh()).thenThrow( + when(staticServiceRest.refresh()).thenThrow( new RestClientException("Exception") ); @@ -86,7 +86,7 @@ void givenRestClientException_whenCallRefreshAPI_thenResponseShouldBe500WithSpec @Test void givenSuccessStaticResponse_whenCallRefreshAPI_thenResponseCodeShouldBe200() { - when(staticAPIService.refresh()).thenReturn( + when(staticServiceRest.refresh()).thenReturn( new StaticAPIResponse(200, "This is body") ); diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java new file mode 100644 index 0000000000..a7d98cd1fd --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java @@ -0,0 +1,54 @@ +/* + * 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.apicatalog.staticapi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +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.test.util.ReflectionTestUtils; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; +import org.zowe.apiml.product.discovery.StaticServicesRegistration; + +import static org.apache.hc.core5.http.HttpStatus.SC_OK; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class StaticRegistrationServiceApiTest { + + @Mock + private StaticServicesRegistration staticServicesRegistration; + + @Test + void givenService_whenRefresh_thenGenerateResponse() { + var staticServiceApi = new StaticRegistrationServiceApi(staticServicesRegistration); + doReturn(new StaticRegistrationResult()).when(staticServicesRegistration).reloadServices(); + + var response = staticServiceApi.refresh(); + assertEquals(SC_OK, response.getStatusCode()); + assertEquals("{\"errors\":[],\"instances\":[],\"additionalServiceMetadata\":{},\"registeredServices\":[]}", response.getBody()); + } + + @Test + void givenInvalidObject_whenRefresh_thenThrowAnException() throws JsonProcessingException { + var mapper = spy(new ObjectMapper()); + var staticServiceApi = new StaticRegistrationServiceApi(staticServicesRegistration); + ReflectionTestUtils.setField(staticServiceApi, "mapper", mapper); + doThrow(mock(JsonProcessingException.class)).when(mapper).writeValueAsString(any()); + + var exception = assertThrows(IllegalStateException.class, staticServiceApi::refresh); + assertInstanceOf(JsonProcessingException.class, exception.getCause()); + } + +} diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRestTest.java similarity index 91% rename from api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java rename to api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRestTest.java index b91f5138aa..1c64a10f52 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticAPIServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRestTest.java @@ -35,7 +35,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class StaticAPIServiceTest { +class StaticRegistrationServiceRestTest { private static final String REFRESH_ENDPOINT = "discovery/api/v1/staticApi"; @@ -48,7 +48,7 @@ class StaticAPIServiceTest { private static final String DISCOVERY_URL_HTTP = "http://localhost:60004/"; @InjectMocks - private StaticAPIService staticAPIService; + private StaticRegistrationServiceRest staticServiceRest; @Mock private CloseableHttpClient httpClient; @@ -85,7 +85,7 @@ void givenRefreshAPIWithSecureDiscoveryService_thenReturnApiResponseCodeWithBody when(discoveryConfigProperties.getLocations()).thenReturn(new String[]{DISCOVERY_LOCATION}); mockRestTemplateExchange(DISCOVERY_URL); - StaticAPIResponse actualResponse = staticAPIService.refresh(); + StaticAPIResponse actualResponse = staticServiceRest.refresh(); StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); assertEquals(expectedResponse, actualResponse); } @@ -95,7 +95,7 @@ void givenRefreshAPIWithUnSecureDiscoveryService_thenReturnApiResponseCodeWithBo when(discoveryConfigProperties.getLocations()).thenReturn(new String[]{DISCOVERY_LOCATION_HTTP}); mockRestTemplateExchange(DISCOVERY_URL_HTTP); - StaticAPIResponse actualResponse = staticAPIService.refresh(); + StaticAPIResponse actualResponse = staticServiceRest.refresh(); StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); assertEquals(expectedResponse, actualResponse); } @@ -117,7 +117,7 @@ void setup() throws IOException { void whenFirstSucceeds_thenReturnResponseFromFirst() throws IOException { when(discoveryConfigProperties.getLocations()).thenReturn(discoveryLocations); mockRestTemplateExchange(DISCOVERY_LOCATION); - StaticAPIResponse actualResponse = staticAPIService.refresh(); + StaticAPIResponse actualResponse = staticServiceRest.refresh(); StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); assertEquals(expectedResponse, actualResponse); } @@ -127,7 +127,7 @@ void whenFirstFails_thenReturnResponseFromSecond() throws IOException { when(discoveryConfigProperties.getLocations()).thenReturn(discoveryLocations); when(notFoundResponse.getCode()).thenReturn(HttpStatus.NOT_FOUND.value()); mockRestTemplateExchange(DISCOVERY_LOCATION_2); - StaticAPIResponse actualResponse = staticAPIService.refresh(); + StaticAPIResponse actualResponse = staticServiceRest.refresh(); StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); assertEquals(expectedResponse, actualResponse); } @@ -143,7 +143,7 @@ void whenBothFail_thenReturnResponseFromSecond() throws IOException { when(entity.getContent()).thenAnswer(invocation -> new ByteArrayInputStream(BODY.getBytes())); mockRestTemplateExchange(DISCOVERY_LOCATION_3); - StaticAPIResponse actualResponse = staticAPIService.refresh(); + StaticAPIResponse actualResponse = staticServiceRest.refresh(); StaticAPIResponse expectedResponse = new StaticAPIResponse(404, BODY); assertEquals(expectedResponse, actualResponse); } @@ -156,7 +156,7 @@ void whenBothFail_thenReturnResponseFromSecond() throws IOException { void givenNoDiscoveryLocations_whenAttemptRefresh_thenReturn500() { when(discoveryConfigProperties.getLocations()).thenReturn(new String[]{}); - StaticAPIResponse actualResponse = staticAPIService.refresh(); + StaticAPIResponse actualResponse = staticServiceRest.refresh(); StaticAPIResponse expectedResponse = new StaticAPIResponse(500, "Error making static API refresh request to the Discovery Service"); assertEquals(expectedResponse, actualResponse); } @@ -164,7 +164,7 @@ void givenNoDiscoveryLocations_whenAttemptRefresh_thenReturn500() { private void mockRestTemplateExchange(String discoveryUrl) throws IOException { HttpPost post = new HttpPost(discoveryUrl.replace("/eureka", "") + REFRESH_ENDPOINT); - when(httpClient.execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class))).thenAnswer((invocation) -> { + when(httpClient.execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class))).thenAnswer(invocation -> { HttpPost httpRequest = (HttpPost) invocation.getArguments()[0]; URI uri = httpRequest.getUri(); int i = uri.compareTo(post.getUri()); diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Route.java b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/Route.java similarity index 81% rename from discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Route.java rename to apiml-common/src/main/java/org/zowe/apiml/product/discovery/Route.java index 6025ba7823..098341cd93 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Route.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/Route.java @@ -8,17 +8,20 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.staticdef; +package org.zowe.apiml.product.discovery; import lombok.Data; /** - * Represents one routes subservice inside a service. + * Represents one routes sub-service inside a service. */ -@Data class Route { +@Data +public class Route { + /** The beginning of the path at the gateway. */ private String gatewayUrl; /** Continuation of the path at the service after the base path of the service. */ private String serviceRelativeUrl; + } diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Service.java b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/Service.java similarity index 94% rename from discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Service.java rename to apiml-common/src/main/java/org/zowe/apiml/product/discovery/Service.java index e05d7fb182..12f85c6867 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Service.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/Service.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.staticdef; +package org.zowe.apiml.product.discovery; import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Data; @@ -24,7 +24,9 @@ * Each instance can have different base URL (http(s)://hostname:port/contextPath/). * The other URLs are relative to it. */ - @Data class Service { +@Data +public class Service { + private String serviceId; private String title; private String description; @@ -38,4 +40,5 @@ private List apiInfo; private Authentication authentication; private Map customMetadata; + } diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceOverride.java b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/ServiceOverride.java similarity index 95% rename from discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceOverride.java rename to apiml-common/src/main/java/org/zowe/apiml/product/discovery/ServiceOverride.java index e68adf46cc..5f4cf14953 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceOverride.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/ServiceOverride.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.staticdef; +package org.zowe.apiml.product.discovery; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceOverrideData.java b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/ServiceOverrideData.java similarity index 92% rename from discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceOverrideData.java rename to apiml-common/src/main/java/org/zowe/apiml/product/discovery/ServiceOverrideData.java index fa0452ec65..e05443a253 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceOverrideData.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/ServiceOverrideData.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.staticdef; +package org.zowe.apiml.product.discovery; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticRegistrationResult.java b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/StaticRegistrationResult.java similarity index 95% rename from discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticRegistrationResult.java rename to apiml-common/src/main/java/org/zowe/apiml/product/discovery/StaticRegistrationResult.java index 274bf1fa8e..5f4e3ac63e 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticRegistrationResult.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/StaticRegistrationResult.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.discovery.staticdef; +package org.zowe.apiml.product.discovery; import com.netflix.appinfo.InstanceInfo; import lombok.Data; diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/discovery/StaticServicesRegistration.java b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/StaticServicesRegistration.java new file mode 100644 index 0000000000..a145249b3a --- /dev/null +++ b/apiml-common/src/main/java/org/zowe/apiml/product/discovery/StaticServicesRegistration.java @@ -0,0 +1,22 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.product.discovery; + +import com.netflix.appinfo.InstanceInfo; + +import java.util.List; + +public interface StaticServicesRegistration { + + List getStaticInstances(); + StaticRegistrationResult reloadServices(); + +} diff --git a/apiml/src/main/java/org/zowe/apiml/StaticDefinitionsRefreshRestController.java b/apiml/src/main/java/org/zowe/apiml/StaticDefinitionsRefreshRestController.java index 89f02a9809..c1cb14ab2a 100644 --- a/apiml/src/main/java/org/zowe/apiml/StaticDefinitionsRefreshRestController.java +++ b/apiml/src/main/java/org/zowe/apiml/StaticDefinitionsRefreshRestController.java @@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.zowe.apiml.discovery.staticdef.StaticRegistrationResult; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; import org.zowe.apiml.discovery.staticdef.StaticServicesRegistrationService; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; diff --git a/apiml/src/test/java/org/zowe/apiml/StaticDefinitionsRefreshRestControllerTest.java b/apiml/src/test/java/org/zowe/apiml/StaticDefinitionsRefreshRestControllerTest.java index b91aa1348f..c95e969c3c 100644 --- a/apiml/src/test/java/org/zowe/apiml/StaticDefinitionsRefreshRestControllerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/StaticDefinitionsRefreshRestControllerTest.java @@ -17,7 +17,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatusCode; -import org.zowe.apiml.discovery.staticdef.StaticRegistrationResult; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; import org.zowe.apiml.discovery.staticdef.StaticServicesRegistrationService; import reactor.test.StepVerifier; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsService.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsService.java index c2d315fed4..f0c75b84a0 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsService.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsService.java @@ -10,7 +10,7 @@ package org.zowe.apiml.discovery.metadata; -import org.zowe.apiml.discovery.staticdef.ServiceOverrideData; +import org.zowe.apiml.product.discovery.ServiceOverrideData; import org.springframework.stereotype.Service; import java.util.Collections; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Definition.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Definition.java index e25b05db06..15a0373938 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Definition.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/Definition.java @@ -11,6 +11,8 @@ package org.zowe.apiml.discovery.staticdef; import lombok.Data; +import org.zowe.apiml.product.discovery.Service; +import org.zowe.apiml.product.discovery.ServiceOverride; import java.util.List; import java.util.Map; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java index 891d51253a..6a3adf52c0 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java @@ -27,6 +27,7 @@ import org.zowe.apiml.exception.ServiceDefinitionException; import org.zowe.apiml.message.core.Message; import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.discovery.*; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.util.MapUtils; import org.zowe.apiml.util.UrlUtils; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticApiRestController.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticApiRestController.java index 7de13868d5..4733f338f1 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticApiRestController.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticApiRestController.java @@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; import java.util.List; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java index 437964a69e..e631d90647 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationService.java @@ -20,6 +20,9 @@ import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.discovery.EurekaRegistryAvailableListener; import org.zowe.apiml.discovery.metadata.MetadataDefaultsService; +import org.zowe.apiml.product.discovery.ServiceOverrideData; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; +import org.zowe.apiml.product.discovery.StaticServicesRegistration; import java.util.ArrayList; import java.util.List; @@ -33,7 +36,7 @@ */ @Slf4j @Component -public class StaticServicesRegistrationService { +public class StaticServicesRegistrationService implements StaticServicesRegistration { @Value("${apiml.discovery.staticApiDefinitionsDirectories:#{null}}") private String staticApiDefinitionsDirectories; diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java index 814791b3a3..a72b29a8b8 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataDefaultsServiceTest.java @@ -23,7 +23,7 @@ import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.discovery.EurekaInstanceRegisteredListener; import org.zowe.apiml.discovery.staticdef.ServiceDefinitionProcessor; -import org.zowe.apiml.discovery.staticdef.StaticRegistrationResult; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; import org.zowe.apiml.discovery.staticdef.StaticServicesRegistrationService; import java.io.File; diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessorTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessorTest.java index ff631442cf..2e42335b36 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessorTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessorTest.java @@ -20,6 +20,9 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageService; +import org.zowe.apiml.product.discovery.ServiceOverride; +import org.zowe.apiml.product.discovery.ServiceOverrideData; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; import java.net.URISyntaxException; import java.nio.file.Paths; diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticApiRestControllerTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticApiRestControllerTest.java index aa3ffeb64e..1f96e18673 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticApiRestControllerTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticApiRestControllerTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.test.web.servlet.MockMvc; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; import java.util.Base64; import java.util.Collections; diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java index 2eca509c76..fab5853e36 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/staticdef/StaticServicesRegistrationServiceTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.discovery.metadata.MetadataDefaultsService; +import org.zowe.apiml.product.discovery.StaticRegistrationResult; import java.net.URISyntaxException; import java.nio.file.Paths; From 51d6578f675691d0a66004691ec749b520e68ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= <58428711+pj892031@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:09:20 +0200 Subject: [PATCH 038/152] chore: Upgrade Nimbus Jose JWT to 10.0.2 (#4245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Gowtham Selvaraj --- gradle/versions.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 4bce3c145f..86e00b8e63 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -76,7 +76,7 @@ dependencyResolutionManagement { version('netty', '4.2.3.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 version('nettyReactor', '1.2.8') - version('nimbusJoseJwt', '9.48') + version('nimbusJoseJwt', '10.0.2') version('openApiDiff', '2.1.2') version('picocli', '4.7.7') From 84e703c13af31aa0ea046c9edfdb1783950b19e2 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:14:42 +0200 Subject: [PATCH 039/152] chore: Update all non-major dependencies (v3.x.x) (#4229) Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 122 +++----- api-catalog-ui/frontend/package.json | 16 +- gradle/versions.gradle | 22 +- .../package-lock.json | 295 +++++++++--------- zowe-cli-id-federation-plugin/package.json | 20 +- 5 files changed, 219 insertions(+), 256 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index 98a1808aa2..93052b78d9 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -41,7 +41,7 @@ "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", "react-error-boundary": "5.0.0", - "react-hook-form": "7.60.0", + "react-hook-form": "7.61.1", "react-redux": "9.2.0", "react-router": "7.6.3", "react-toastify": "10.0.6", @@ -68,10 +68,10 @@ "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", "@eslint/compat": "1.3.1", - "@eslint/js": "9.31.0", + "@eslint/js": "9.32.0", "@reduxjs/toolkit": "2.8.2", - "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.6.3", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.6.4", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "ajv": "8.17.1", @@ -84,15 +84,15 @@ "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.31.0", + "eslint": "9.32.0", "eslint-config-airbnb": "19.0.4", - "eslint-config-prettier": "9.1.0", + "eslint-config-prettier": "9.1.2", "eslint-plugin-cypress": "4.3.0", "eslint-plugin-flowtype": "8.0.3", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-prettier": "5.5.1", + "eslint-plugin-prettier": "5.5.3", "eslint-plugin-react": "7.37.5", "express": "4.21.2", "globals": "15.15.0", @@ -2970,10 +2970,11 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -3039,9 +3040,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.32.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "license": "MIT", "engines": { @@ -3081,12 +3082,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -6560,9 +6562,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "version": "10.4.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "dependencies": { @@ -6570,9 +6572,9 @@ "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", - "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { @@ -6580,18 +6582,18 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", - "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "version": "6.6.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", + "integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==", "dev": true, "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", - "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", + "picocolors": "^1.1.1", "redent": "^3.0.0" }, "engines": { @@ -6600,20 +6602,6 @@ "yarn": ">=1" } }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -8508,13 +8496,13 @@ } }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -12194,9 +12182,9 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", "dependencies": { @@ -12206,8 +12194,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -12297,9 +12285,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "9.1.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", "bin": { @@ -12540,9 +12528,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", - "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "version": "5.5.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", "dev": true, "license": "MIT", "dependencies": { @@ -12741,19 +12729,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/ajv": { "version": "6.12.6", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ajv/-/ajv-6.12.6.tgz", @@ -13955,14 +13930,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -23915,9 +23891,9 @@ "license": "MIT" }, "node_modules/react-hook-form": { - "version": "7.60.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-hook-form/-/react-hook-form-7.60.0.tgz", - "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", + "version": "7.61.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-hook-form/-/react-hook-form-7.61.1.tgz", + "integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index ee10ddf9fe..63b376a687 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -37,7 +37,7 @@ "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", "react-error-boundary": "5.0.0", - "react-hook-form": "7.60.0", + "react-hook-form": "7.61.1", "react-redux": "9.2.0", "react-router": "7.6.3", "react-toastify": "10.0.6", @@ -89,9 +89,9 @@ "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", "@eslint/compat": "1.3.1", - "@eslint/js": "9.31.0", - "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.6.3", + "@eslint/js": "9.32.0", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.6.4", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@reduxjs/toolkit": "2.8.2", @@ -105,15 +105,15 @@ "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.31.0", + "eslint": "9.32.0", "eslint-config-airbnb": "19.0.4", - "eslint-config-prettier": "9.1.0", + "eslint-config-prettier": "9.1.2", "eslint-plugin-cypress": "4.3.0", "eslint-plugin-flowtype": "8.0.3", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-prettier": "5.5.1", + "eslint-plugin-prettier": "5.5.3", "eslint-plugin-react": "7.37.5", "express": "4.21.2", "globals": "15.15.0", @@ -155,7 +155,7 @@ "lodash": "4.17.21", "semver": "7.7.2", "@babel/traverse": "7.28.0", - "axios": "1.10.0", + "axios": "1.11.0", "swagger-ui-react": { "react-syntax-highlighter": { "refractor": "5.0.0" diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 86e00b8e63..ff4a2794fc 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -5,8 +5,8 @@ dependencyResolutionManagement { version('projectNode', '20.14.0') version('projectNpm', '10.7.0') - version('springBoot', '3.5.3') - version('springBootGraphQl', '3.5.3') + version('springBoot', '3.5.4') + version('springBootGraphQl', '3.5.4') version('springCloudNetflix', '4.3.0') version('springCloudCommons', '4.3.0') version('springCloudCB', '3.3.0') @@ -14,7 +14,7 @@ dependencyResolutionManagement { version('springFramework', '6.2.9') version('springRetry', '2.0.12') - version('modulith', '1.4.1') + version('modulith', '1.4.2') version('jmolecules', '2023.3.2') version('glassfishHk2', '3.1.1') @@ -26,12 +26,12 @@ dependencyResolutionManagement { version('checkerQual', '3.49.5') version('commonsLang3', '3.18.0') version('commonsLogging', '1.3.5') - version('commonsText', '1.13.1') - version('commonsIo', '2.19.0') + version('commonsText', '1.14.0') + version('commonsIo', '2.20.0') version('ehCache', '3.10.8') version('eureka', '2.0.5') version('netflixServo', '0.13.2') - version('googleErrorprone', '2.40.0') + version('googleErrorprone', '2.41.0') version('gradleGitProperties', '2.5.2') // Used in classpath dependencies version('googleGson', '2.13.1') version('guava', '33.4.8-jre') @@ -39,9 +39,9 @@ dependencyResolutionManagement { version('httpClient4', '4.5.14') version('httpClient5', '5.5') version('infinispan', '15.2.5.Final') - version('jacksonCore', '2.19.1') - version('jacksonDatabind', '2.19.1') - version('jacksonDataformatYaml', '2.19.1') + version('jacksonCore', '2.19.2') + version('jacksonDatabind', '2.19.2') + version('jacksonDataformatYaml', '2.19.2') version('janino', '3.1.12') version('jakartaValidation', '3.1.1') version('jakartaInject', '2.0.1') @@ -66,8 +66,8 @@ dependencyResolutionManagement { version('jodaTime', '2.14.0') version('jsonPath', '2.9.0') version('jsonSmart', '2.5.2') - version('junitJupiter', '5.13.3') - version('junitPlatform', '1.13.3') + version('junitJupiter', '5.13.4') + version('junitPlatform', '1.13.4') version('jxpath', '1.4.0') version('lettuce', '6.7.1.RELEASE') // force version in build.gradle file - compatibility with Slf4j diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index 456505ebdc..b1408263c3 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -12,17 +12,17 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.31.0", + "@eslint/js": "9.32.0", "@types/jest": "29.5.14", - "@types/node": "20.19.8", - "@typescript-eslint/eslint-plugin": "8.37.0", - "@typescript-eslint/parser": "8.37.0", - "@zowe/cli": "8.24.5", - "@zowe/cli-test-utils": "8.24.5", - "@zowe/imperative": "8.24.5", + "@types/node": "20.19.9", + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@zowe/cli": "8.25.0", + "@zowe/cli-test-utils": "8.25.0", + "@zowe/imperative": "8.25.0", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.31.0", + "eslint": "9.32.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.1.4", @@ -38,7 +38,7 @@ "madge": "8.0.0", "ts-jest": "29.4.0", "ts-node": "10.9.2", - "typedoc": "0.28.7", + "typedoc": "0.28.8", "typescript": "5.8.3" }, "engines": { @@ -46,7 +46,7 @@ "npm": "=10.9.3" }, "peerDependencies": { - "@zowe/imperative": "8.24.5" + "@zowe/imperative": "8.25.0" } }, "node_modules/@ampproject/remapping": { @@ -691,9 +691,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -740,9 +740,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.32.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "license": "MIT", "engines": { @@ -763,13 +763,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -2060,9 +2060,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.19.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.8.tgz", - "integrity": "sha512-HzbgCY53T6bfu4tT7Aq3TvViJyHjLjPNaAS3HOuMc9pw97KHsUtXNX4L+wu59g1WnjsZSko35MbEqnO58rihhw==", + "version": "20.19.9", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", "dev": true, "license": "MIT", "dependencies": { @@ -2105,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", - "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/type-utils": "8.37.0", - "@typescript-eslint/utils": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2129,7 +2129,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.37.0", + "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2158,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.37.0.tgz", - "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "engines": { @@ -2183,14 +2183,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", - "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.37.0", - "@typescript-eslint/types": "^8.37.0", + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "engines": { @@ -2205,14 +2205,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", - "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,9 +2223,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", - "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", "dev": true, "license": "MIT", "engines": { @@ -2240,15 +2240,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", - "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2278,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.37.0.tgz", - "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", "dev": true, "license": "MIT", "engines": { @@ -2292,16 +2292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", - "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.37.0", - "@typescript-eslint/tsconfig-utils": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2360,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.37.0.tgz", - "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0" + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2384,13 +2384,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.37.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", - "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", + "version": "8.38.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2471,25 +2471,25 @@ "dev": true }, "node_modules/@zowe/cli": { - "version": "8.24.5", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.24.5.tgz", - "integrity": "sha512-JDQJrOVNTkA/YL7fihs/bHiFhJ9DRS8+WT5h5rMCgXRyE3WNSI8qvc8yLC5D1PMKDNSxmaFpERZOrGFwAzK/GA==", + "version": "8.25.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.25.0.tgz", + "integrity": "sha512-5LRIqP8xrEO6m+JvkEF0UFOemoqV1Te6GLZB3fheMIpS9vmkgY454a5nsbPvcSOY5kVOuVHzt9LuMKtjQ2qHdQ==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, "license": "EPL-2.0", "dependencies": { - "@zowe/core-for-zowe-sdk": "8.24.5", - "@zowe/imperative": "8.24.5", - "@zowe/provisioning-for-zowe-sdk": "8.24.5", - "@zowe/zos-console-for-zowe-sdk": "8.24.5", - "@zowe/zos-files-for-zowe-sdk": "8.24.5", - "@zowe/zos-jobs-for-zowe-sdk": "8.24.5", - "@zowe/zos-logs-for-zowe-sdk": "8.24.5", - "@zowe/zos-tso-for-zowe-sdk": "8.24.5", - "@zowe/zos-uss-for-zowe-sdk": "8.24.5", - "@zowe/zos-workflows-for-zowe-sdk": "8.24.5", - "@zowe/zosmf-for-zowe-sdk": "8.24.5", + "@zowe/core-for-zowe-sdk": "8.25.0", + "@zowe/imperative": "8.25.0", + "@zowe/provisioning-for-zowe-sdk": "8.25.0", + "@zowe/zos-console-for-zowe-sdk": "8.25.0", + "@zowe/zos-files-for-zowe-sdk": "8.25.0", + "@zowe/zos-jobs-for-zowe-sdk": "8.25.0", + "@zowe/zos-logs-for-zowe-sdk": "8.25.0", + "@zowe/zos-tso-for-zowe-sdk": "8.25.0", + "@zowe/zos-uss-for-zowe-sdk": "8.25.0", + "@zowe/zos-workflows-for-zowe-sdk": "8.25.0", + "@zowe/zosmf-for-zowe-sdk": "8.25.0", "find-process": "1.4.7", "lodash": "4.17.21", "minimatch": "9.0.5", @@ -2506,9 +2506,9 @@ } }, "node_modules/@zowe/cli-test-utils": { - "version": "8.24.5", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.24.5.tgz", - "integrity": "sha512-UiJizEYQmxEq40RjEYDmpjRFERCpLQ0SPdjl+CNljKi47cQ/uPin94CGTjrYQoVf8kJU0SYprj5shr7dktniag==", + "version": "8.25.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.25.0.tgz", + "integrity": "sha512-RY+BInbQqxpcE4025tyknYub819crbQCRzoJkjtVTLpxvxi6cf2s1+Kkod/lt7oVcjqtp1QV7eGDB+lD8BXPXA==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -3088,9 +3088,9 @@ "license": "MIT" }, "node_modules/@zowe/cli/node_modules/@zowe/core-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-4t8MHBqDCj6e7/GnU31aN3JRwXmHoukM/x7GlcOfvrvU2ADTwY6Eetvr1z7oaLGEhkRB5FewofqnQfvu2Hn++A==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-2LoacIOP/qANnTcSLJLb/PFOZPWu0mhMlB9hC9P2lxX+Ybr0KAMt49f/PMcZRPhDI4wMYzSrlOwci01cmu5r6w==", "dev": true, "dependencies": { "comment-json": "~4.2.3", @@ -3104,9 +3104,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/imperative": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.24.5.tgz", - "integrity": "sha512-wM3K5qEeCBKxrJVg4jBSzRfoZSQg5U82tYfLQuEUZPziN7LUsH04wLs9qy0yx+VEiO6q6EzajAEgyivtmecEDQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.25.0.tgz", + "integrity": "sha512-5rnhsQGhUQESv82yYV9hwRd0emY08EY2OrB1KfYv8RhZwdOfxoFrDIry9KUac0QZgKqyboIUt1TmnPEWx7XyyQ==", "dev": true, "dependencies": { "@types/yargs": "^17.0.32", @@ -3232,9 +3232,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/provisioning-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-mYbuKSb+8Blyo7MgD1VdLKRImiR2d8LY3w79hab8cncKURe9lYPxBawd/2h1xRlsnLUQVT2DfOjnS/4Yvk8pdg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-qdHkRU+Ll8L+3exasfofdlyblNADUtpzaiW7sJ6TFGDOEA3aGNnW7Kksl7RrIEkvRnoRIeEMny0mIhtGr8Uh6A==", "dev": true, "dependencies": { "js-yaml": "^4.1.0" @@ -3259,9 +3259,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-console-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-Ke5VwZNGUO8CRwNiQMgbHerxCrZTigVAi0ahorAYXURs9VW+ciey0zLcWULxuS7FHcf6Fjv5yK0iodLlO639PQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-oVYnx0PGdgF1iBzKlY5+e0szvcXPtXgNebtSQFDsaEgBllymAzG2XcIgWid5GJBhNsdxXBUYQa+0luKEyNmaNg==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3272,9 +3272,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-files-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-E/XuqdjdczrKl0IsUYmR1EpV6qqJRXaFJAvs8PrSklaqVkpFphg2wxq4yZorz8d75eHbtC+AF6Z9CRWQAT0mQg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-S9pW7f80ubcTN9BkOHb+6VRszSVFV7bnKxnO0teOpUdMEhnbk467YC5LyuNSkAT2h0VaR1gcyyEJR/V2r7tisg==", "dev": true, "dependencies": { "lodash": "^4.17.21", @@ -3289,12 +3289,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-jobs-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-dybtaT0Ag2BHK58ShihELAaStiXHmAFjFKIBU8GN6IGTXhEwEcujHfcUTZqOg0dXKxJ7gEABn11QdGo0Z3IRXA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-WbLPadC4VCDV82BjeqAZADGMdRlpYcf/O3kLYyQo5QMyzR1/vOruLI6Vw5Qq6BnGzIvoDZQwBEljXKulazL+FA==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.24.5" + "@zowe/zos-files-for-zowe-sdk": "8.25.0" }, "engines": { "node": ">=18.12.0" @@ -3305,9 +3305,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-logs-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-43MyutusZugBfVWUZS6S0TcgKv5orM3cmLpbKmxzsUxjf1Zk1s9cbelgPgOqrPLKwMAqxy8vLBReWFufhFvMTA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-HkmsoH/vjYA9PRy6ufJ6mhoWC7An8aZt+n2ENhjGmE9Q+PG5h8kn8Y2LvRJR5niYDn668QtTsJfu9kIcH5ofnw==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3318,12 +3318,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-tso-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-LLdm77o5pTzybsICKFtnf4xZA+PTd6edHRzxOImoa7yUnpPrIiddm09ndde+O2Wtyj0B7UNwpLPyaeFS7C50wQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-MpSIVUpZ+t6xsZMVg2+ON4+PruxLu0SNdYHDtbIH+6mDlXEBWSXkwj47xDzxVzhSqfkCJt9sIqb2pJxk+cHbdA==", "dev": true, "dependencies": { - "@zowe/zosmf-for-zowe-sdk": "8.24.5" + "@zowe/zosmf-for-zowe-sdk": "8.25.0" }, "engines": { "node": ">=18.12.0" @@ -3334,9 +3334,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-uss-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-CoUBednyC1q3d5cu32cT1HiG3pE87WGUCKx13wbNuvWVaQ+xT/WmE4qRZbet9+pntvl74zWsQxoq6wi+n/NNvg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-j9NSD+in5NUiSrFGbJAmKnY3wbCqM824bl/966iDCaQFzOD7OvscNG9HOwGKKZLcyp/HQiSc4HX4wyal8lrl6w==", "dev": true, "dependencies": { "ssh2": "^1.15.0" @@ -3349,12 +3349,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-workflows-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-yikJyC05qRiiehBVArVyhA8JcLXEEavoNDPaUjVokgq74UYMP3OtaCiB+ky+NXBrjoF2c83K+68qyx+EKVpgyA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-f14/CnTTHK7rx99elXZRi+bGiYuVwd63f6wksOax5fXczwvbikVdrtskJHc25TVb3TUd4TJYN7Zn9m1FVm1hnA==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.24.5" + "@zowe/zos-files-for-zowe-sdk": "8.25.0" }, "engines": { "node": ">=18.12.0" @@ -3365,9 +3365,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zosmf-for-zowe-sdk": { - "version": "8.24.5", - "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.24.5.tgz", - "integrity": "sha512-moWr2JqZTmxKtaULWm4CvUsVaKpiF9jv6YBTj/1ZNK5Mny98FoOR4iG0EyCiaayjW4IohdDPP4phPX3aySSm6A==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.25.0.tgz", + "integrity": "sha512-qCIEW43LN3YlgzkWz3oDcI1ePqujhVLrJ5oHXbBFzS7do4U4fx/1dntv5kyC4SzQ0t2kYK/JtyWE9jdQ16zHDQ==", "dev": true, "engines": { "node": ">=18.12.0" @@ -6036,9 +6036,9 @@ } }, "node_modules/@zowe/imperative": { - "version": "8.24.5", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.24.5.tgz", - "integrity": "sha512-wM3K5qEeCBKxrJVg4jBSzRfoZSQg5U82tYfLQuEUZPziN7LUsH04wLs9qy0yx+VEiO6q6EzajAEgyivtmecEDQ==", + "version": "8.25.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.25.0.tgz", + "integrity": "sha512-5rnhsQGhUQESv82yYV9hwRd0emY08EY2OrB1KfYv8RhZwdOfxoFrDIry9KUac0QZgKqyboIUt1TmnPEWx7XyyQ==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -7645,9 +7645,9 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", "dependencies": { @@ -7657,8 +7657,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -7786,19 +7786,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@humanwhocodes/retry/-/retry-0.4.3.tgz", @@ -12597,9 +12584,9 @@ } }, "node_modules/typedoc": { - "version": "0.28.7", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.7.tgz", - "integrity": "sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==", + "version": "0.28.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.8.tgz", + "integrity": "sha512-16GfLopc8icHfdvqZDqdGBoS2AieIRP2rpf9mU+MgN+gGLyEQvAO0QgOa6NJ5QNmQi0LFrDY9in4F2fUNKgJKA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index b3327305c5..e4713855e4 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -49,17 +49,17 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.31.0", + "@eslint/js": "9.32.0", "@types/jest": "29.5.14", - "@types/node": "20.19.8", - "@typescript-eslint/eslint-plugin": "8.37.0", - "@typescript-eslint/parser": "8.37.0", - "@zowe/cli": "8.24.5", - "@zowe/cli-test-utils": "8.24.5", - "@zowe/imperative": "8.24.5", + "@types/node": "20.19.9", + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@zowe/cli": "8.25.0", + "@zowe/cli-test-utils": "8.25.0", + "@zowe/imperative": "8.25.0", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.31.0", + "eslint": "9.32.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.1.4", @@ -75,14 +75,14 @@ "madge": "8.0.0", "ts-jest": "29.4.0", "ts-node": "10.9.2", - "typedoc": "0.28.7", + "typedoc": "0.28.8", "typescript": "5.8.3" }, "overrides": { "@babel/traverse": "7.28.0" }, "peerDependencies": { - "@zowe/imperative": "8.24.5" + "@zowe/imperative": "8.25.0" }, "engines": { "npm": "=10.9.3", From c7fdf645ccb07dc2a093f2b6076f080de153848e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= <58428711+pj892031@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:32:33 +0200 Subject: [PATCH 040/152] refactor: Replacement of Apache HTTP client by WebClient in API Catalog (#4244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Gowtham Selvaraj --- api-catalog-services/build.gradle | 1 + .../api/StaticAPIRefreshController.java | 2 +- .../staticapi/StaticRegistrationService.java | 4 +- .../StaticRegistrationServiceApi.java | 7 +- .../StaticRegistrationServiceRest.java | 80 +++---- .../swagger/ApiDocRetrievalServiceRest.java | 51 ++--- .../src/main/resources/application.yml | 2 +- .../api/StaticAPIRefreshControllerTest.java | 3 +- .../StaticRegistrationServiceApiTest.java | 17 +- .../StaticRegistrationServiceRestTest.java | 201 ++++++++++-------- .../apicatalog/swagger/ApiDocServiceTest.java | 57 +++-- .../src/test/resources/application.yml | 2 + apiml-common/build.gradle | 1 + .../routing/transform/TransformService.java | 2 +- apiml-security-common/build.gradle | 3 + .../common/config/WebClientConfig.java | 96 +++++++++ .../security/common/util/ConnectionUtil.java | 131 ++++++++++++ apiml/src/main/resources/application.yml | 1 + apiml/src/test/resources/application.yml | 1 + .../gateway/config/ConnectionsConfig.java | 183 ++-------------- .../apiml/gateway/config/WebSocketConfig.java | 17 +- .../filters/PageRedirectionFilterFactory.java | 2 +- .../src/main/resources/application.yml | 2 + .../gateway/config/ConnectionsConfigTest.java | 92 ++++---- .../src/test/resources/application.yml | 1 + gradle/versions.gradle | 1 + ...tilsTest.java => ConnectionUtilsTest.java} | 4 +- 27 files changed, 531 insertions(+), 433 deletions(-) create mode 100644 apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java create mode 100644 apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java rename integration-tests/src/test/java/org/zowe/apiml/util/{SecurityUtilsTest.java => ConnectionUtilsTest.java} (97%) diff --git a/api-catalog-services/build.gradle b/api-catalog-services/build.gradle index 7e817dcc11..2735463949 100644 --- a/api-catalog-services/build.gradle +++ b/api-catalog-services/build.gradle @@ -73,6 +73,7 @@ dependencies { implementation libs.spring.cloud.starter.eureka.client implementation libs.spring.doc.webflux.ui implementation libs.spring.retry + implementation libs.spring.cloud.gateway.server implementation libs.apache.velocity diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java index 3cbd242a7a..4a8c2d90d3 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshController.java @@ -29,7 +29,7 @@ public class StaticAPIRefreshController { @PostMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE) public Mono> refreshStaticApis() { - return Mono.fromCallable(staticRegistrationService::refresh) + return staticRegistrationService.refresh() .map(staticAPIResponse -> ResponseEntity .status(staticAPIResponse.getStatusCode()) .body(staticAPIResponse.getBody()) diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java index 4208683866..25abe594f0 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationService.java @@ -10,8 +10,10 @@ package org.zowe.apiml.apicatalog.staticapi; +import reactor.core.publisher.Mono; + public interface StaticRegistrationService { - StaticAPIResponse refresh(); + Mono refresh(); } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java index 2704ed9cfb..14785fe6ed 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApi.java @@ -17,6 +17,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.stereotype.Service; import org.zowe.apiml.product.discovery.StaticServicesRegistration; +import reactor.core.publisher.Mono; import static org.apache.hc.core5.http.HttpStatus.SC_OK; @@ -30,13 +31,13 @@ public class StaticRegistrationServiceApi implements StaticRegistrationService { private final StaticServicesRegistration staticServicesRegistration; @Override - public StaticAPIResponse refresh() { + public Mono refresh() { try { var result = staticServicesRegistration.reloadServices(); - return new StaticAPIResponse(SC_OK, mapper.writeValueAsString(result)); + return Mono.just(new StaticAPIResponse(SC_OK, mapper.writeValueAsString(result))); } catch (JsonProcessingException e) { log.error("Cannot serialize the list of static API services", e); - throw new IllegalStateException(e); + return Mono.error(new IllegalStateException(e)); } } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java index b46d101b3a..1760184920 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java @@ -12,21 +12,24 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.io.IOException; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import static org.apache.hc.core5.http.HttpHeaders.ACCEPT; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + @Service @RequiredArgsConstructor @Slf4j @@ -41,8 +44,8 @@ public class StaticRegistrationServiceRest implements StaticRegistrationService @Value("${apiml.discovery.password:password}") private String eurekaPassword; - @Qualifier("secureHttpClientWithKeystore") - private final CloseableHttpClient httpClient; + @Qualifier("webClientClientCert") + private final WebClient webClientClientCert; @Value("${server.attls.enabled:false}") private boolean isAttlsEnabled; @@ -50,48 +53,28 @@ public class StaticRegistrationServiceRest implements StaticRegistrationService private final DiscoveryConfigProperties discoveryConfigProperties; @Override - public StaticAPIResponse refresh() { - List discoveryServiceUrls = getDiscoveryServiceUrls(); - for (int i = 0; i < discoveryServiceUrls.size(); i++) { - - String discoveryServiceUrl = discoveryServiceUrls.get(i); - - try { - HttpPost post = getHttpRequest(discoveryServiceUrl); - var staticApiResponse = httpClient.execute(post, response -> { - final HttpEntity responseEntity = response.getEntity(); - String responseBody = ""; - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity); + public Mono refresh() { + return Flux.fromIterable(getDiscoveryServiceUrls()) + .flatMap(uri -> webClientClientCert + .post().uri(uri) + .header(ACCEPT, APPLICATION_JSON_VALUE) + .headers(headers -> { + boolean isHttp = uri.startsWith("http://"); + if (isHttp && !isAttlsEnabled) { + String basicToken = "Basic " + Base64.getEncoder().encodeToString((eurekaUserid + ":" + eurekaPassword).getBytes()); + headers.add(HttpHeaders.AUTHORIZATION, basicToken); } - return new StaticAPIResponse(response.getCode(), responseBody); - }); - - // Return response if successful or if none have been successful and this is the last URL to try - if (isSuccessful(staticApiResponse) || i == discoveryServiceUrls.size() - 1) { - return staticApiResponse; - } - } catch (IOException e) { - log.debug("Error refreshing static APIs from {}, error message: {}", discoveryServiceUrl, e.getMessage()); - } - } - - return new StaticAPIResponse(500, "Error making static API refresh request to the Discovery Service"); - } - - private boolean isSuccessful(StaticAPIResponse response) { - return HttpStatus.valueOf(response.getStatusCode()).is2xxSuccessful(); - } - - private HttpPost getHttpRequest(String discoveryServiceUrl) { - boolean isHttp = discoveryServiceUrl.startsWith("http://"); - HttpPost post = new HttpPost(discoveryServiceUrl); - post.addHeader("Accept", "application/json"); - if (isHttp && !isAttlsEnabled) { - String basicToken = "Basic " + Base64.getEncoder().encodeToString((eurekaUserid + ":" + eurekaPassword).getBytes()); - post.addHeader("Authorization", basicToken); - } - return post; + }) + .exchangeToMono(response -> response + .bodyToMono(String.class) + .flatMap(body -> (response.statusCode().is2xxSuccessful() || StringUtils.isNotBlank(body)) ? + Mono.just(new StaticAPIResponse(response.statusCode().value(), body)) : Mono.empty() + ) + ) + .doOnError(IOException.class, e -> log.debug("Error refreshing static APIs from {}, error message: {}", uri, e.getMessage())) + ) + .switchIfEmpty(Flux.just(new StaticAPIResponse(500, "Error making static API refresh request to the Discovery Service"))) + .next(); } private List getDiscoveryServiceUrls() { @@ -105,4 +88,5 @@ private List getDiscoveryServiceUrls() { } return discoveryServiceUrls; } + } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java index 481630c25d..88f90380da 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java @@ -14,26 +14,24 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.io.entity.EntityUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cloud.client.ServiceInstance; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; import org.zowe.apiml.apicatalog.model.ApiDocInfo; import org.zowe.apiml.config.ApiInfo; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.function.UnaryOperator; +import static org.apache.hc.core5.http.HttpHeaders.ACCEPT; +import static org.apache.hc.core5.http.HttpStatus.SC_OK; + /** * Retrieves the API documentation for a registered service */ @@ -44,8 +42,8 @@ public class ApiDocRetrievalServiceRest { private static final UnaryOperator exceptionMessage = serviceId -> "No API Documentation was retrieved for the service " + serviceId + "."; - @Qualifier("secureHttpClientWithoutKeystore") - private final CloseableHttpClient secureHttpClientWithoutKeystore; + @Qualifier("webClient") + private final WebClient webClient; @InjectApimlLogger private ApimlLogger apimlLogger = ApimlLogger.empty(); @@ -69,37 +67,22 @@ public Mono retrieveApiDoc(ServiceInstance serviceInstance, ApiInfo * @throws ApiDocNotFoundException if the response is error */ private Mono getApiDocContentByUrl(@NonNull String serviceId, String apiDocUrl) { - HttpGet httpGet = new HttpGet(apiDocUrl); - httpGet.setHeader(org.apache.http.HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); - - // TODO: refactor with reactive client - return Mono.defer(() -> { - try { - return Mono.just(secureHttpClientWithoutKeystore.execute(httpGet, response -> { - String responseBody = ""; - var responseEntity = response.getEntity(); - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); - } - - if (HttpStatus.SC_OK == response.getCode()) { - return responseBody; - } else { - throw new ApiDocNotFoundException( - String.format("No API Documentation was retrieved due to %s server error: %d %s", serviceId, response.getCode(), responseBody) - ); - } - } - )); - } catch (IOException e) { + return webClient.get().uri(apiDocUrl) + .header(ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .onStatus(httpStatusCode -> httpStatusCode.value() != SC_OK, response -> Mono.error( + new ApiDocNotFoundException( + String.format("No API Documentation was retrieved due to %s server error: %d", serviceId, response.statusCode().value()) + ) + )) + .bodyToMono(String.class) + .onErrorResume(IOException.class, e -> { apimlLogger.log("org.zowe.apiml.apicatalog.apiDocHostCommunication", serviceId, e.getMessage()); log.debug("Error retrieving api doc for '{}'", serviceId, e); return Mono.error(new ApiDocNotFoundException( exceptionMessage.apply(serviceId) + " Root cause: " + e.getMessage(), e )); - } - }) - .subscribeOn(Schedulers.boundedElastic()); + }); } } diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index 7b48060114..1915f9f65a 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -9,7 +9,6 @@ spring: ipAddress: ${apiml.service.ipAddress} gateway: mvc.enabled: false - enabled: false output: ansi: enabled: detect @@ -104,6 +103,7 @@ apiml: cacheRefreshUpdateThresholdInMillis: 30000 cacheRefreshInitialDelayInMillis: 30000 cacheRefreshRetryDelayInMillis: 30000 + webClientConfig.enabled: true ############################################################################################## diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java index 202eb81096..9504c918e3 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/StaticAPIRefreshControllerTest.java @@ -26,6 +26,7 @@ import org.zowe.apiml.apicatalog.staticapi.StaticAPIResponse; import org.zowe.apiml.apicatalog.staticapi.StaticRegistrationServiceRest; import org.zowe.apiml.apicatalog.staticapi.StaticDefinitionGenerator; +import reactor.core.publisher.Mono; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -87,7 +88,7 @@ void givenRestClientException_whenCallRefreshAPI_thenResponseShouldBe500WithSpec @Test void givenSuccessStaticResponse_whenCallRefreshAPI_thenResponseCodeShouldBe200() { when(staticServiceRest.refresh()).thenReturn( - new StaticAPIResponse(200, "This is body") + Mono.just(new StaticAPIResponse(200, "This is body")) ); webTestClient.post().uri(API_REFRESH_ENDPOINT).exchange() diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java index a7d98cd1fd..48cdd33faf 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceApiTest.java @@ -19,9 +19,10 @@ import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.product.discovery.StaticRegistrationResult; import org.zowe.apiml.product.discovery.StaticServicesRegistration; +import reactor.test.StepVerifier; import static org.apache.hc.core5.http.HttpStatus.SC_OK; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -35,9 +36,12 @@ void givenService_whenRefresh_thenGenerateResponse() { var staticServiceApi = new StaticRegistrationServiceApi(staticServicesRegistration); doReturn(new StaticRegistrationResult()).when(staticServicesRegistration).reloadServices(); - var response = staticServiceApi.refresh(); - assertEquals(SC_OK, response.getStatusCode()); - assertEquals("{\"errors\":[],\"instances\":[],\"additionalServiceMetadata\":{},\"registeredServices\":[]}", response.getBody()); + StepVerifier.create(staticServiceApi.refresh()) + .assertNext(response -> { + assertEquals(SC_OK, response.getStatusCode()); + assertEquals("{\"errors\":[],\"instances\":[],\"additionalServiceMetadata\":{},\"registeredServices\":[]}", response.getBody()); + }) + .verifyComplete(); } @Test @@ -47,8 +51,9 @@ void givenInvalidObject_whenRefresh_thenThrowAnException() throws JsonProcessing ReflectionTestUtils.setField(staticServiceApi, "mapper", mapper); doThrow(mock(JsonProcessingException.class)).when(mapper).writeValueAsString(any()); - var exception = assertThrows(IllegalStateException.class, staticServiceApi::refresh); - assertInstanceOf(JsonProcessingException.class, exception.getCause()); + StepVerifier.create(staticServiceApi.refresh()) + .expectError(IllegalStateException.class) + .verify(); } } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRestTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRestTest.java index 1c64a10f52..164590ecaa 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRestTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRestTest.java @@ -10,60 +10,53 @@ package org.zowe.apiml.apicatalog.staticapi; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.ClassicHttpRequest; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.zowe.apiml.util.HttpClientMockHelper; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.URI; - +import org.springframework.http.HttpStatusCode; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.hc.core5.http.HttpStatus.SC_OK; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class StaticRegistrationServiceRestTest { - private static final String REFRESH_ENDPOINT = "discovery/api/v1/staticApi"; + private static final String BODY = "This is body"; private static final String DISCOVERY_LOCATION = "https://localhost:60004/eureka/"; - private static final String DISCOVERY_LOCATION_2 = "https://localhost:60005/eureka/"; - private static final String DISCOVERY_LOCATION_3 = "https://localhost:60006/eureka/"; - private static final String DISCOVERY_URL = "https://localhost:60004/"; - private static final String DISCOVERY_LOCATION_HTTP = "http://localhost:60004/eureka/"; - private static final String DISCOVERY_URL_HTTP = "http://localhost:60004/"; + private static final String DISCOVERY_LOCATION_2 = "https://localhost:60005/eureka/"; + private static final String[] discoveryLocations = { DISCOVERY_LOCATION, DISCOVERY_LOCATION_2 }; - @InjectMocks private StaticRegistrationServiceRest staticServiceRest; @Mock - private CloseableHttpClient httpClient; - @Mock - private CloseableHttpResponse okResponse; - @Mock - private CloseableHttpResponse notFoundResponse; + private ExchangeFunction exchangeFunction; + @Mock - private HttpEntity entity; + private ClientResponse clientResponse; @Mock private DiscoveryConfigProperties discoveryConfigProperties; - private final String[] discoveryLocations = {DISCOVERY_LOCATION, DISCOVERY_LOCATION_2}; - private static final String BODY = "This is body"; + @BeforeEach + void init() { + var webCLient = WebClient.builder().exchangeFunction(exchangeFunction).build(); + staticServiceRest = new StaticRegistrationServiceRest(webCLient, discoveryConfigProperties); + } @Nested class WhenRefreshEndpointPresentsResponseTest { @@ -72,104 +65,128 @@ class WhenRefreshEndpointPresentsResponseTest { class GivenSingleUrlTest { @BeforeEach - void setup() throws IOException { - when(okResponse.getCode()).thenReturn(HttpStatus.OK.value()); - - when(okResponse.getEntity()).thenReturn(entity); - when(entity.getContent()).thenReturn(new ByteArrayInputStream(BODY.getBytes())); + void setup() { + doReturn(Mono.just(clientResponse)).when(exchangeFunction).exchange(any()); + doReturn(Mono.empty()).when(clientResponse).releaseBody(); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(BODY)).when(clientResponse).bodyToMono(String.class); } @Test - void givenRefreshAPIWithSecureDiscoveryService_thenReturnApiResponseCodeWithBody() throws IOException { - - when(discoveryConfigProperties.getLocations()).thenReturn(new String[]{DISCOVERY_LOCATION}); - mockRestTemplateExchange(DISCOVERY_URL); - - StaticAPIResponse actualResponse = staticServiceRest.refresh(); - StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); - assertEquals(expectedResponse, actualResponse); + void givenRefreshAPIWithSecureDiscoveryService_thenReturnApiResponseCodeWithBody() { + when(discoveryConfigProperties.getLocations()).thenReturn(new String[] { DISCOVERY_LOCATION }); + + StepVerifier.create(staticServiceRest.refresh()) + .assertNext(actualResponse -> { + StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); + assertEquals(expectedResponse, actualResponse); + }) + .verifyComplete(); } @Test - void givenRefreshAPIWithUnSecureDiscoveryService_thenReturnApiResponseCodeWithBody() throws IOException { - when(discoveryConfigProperties.getLocations()).thenReturn(new String[]{DISCOVERY_LOCATION_HTTP}); - - mockRestTemplateExchange(DISCOVERY_URL_HTTP); - StaticAPIResponse actualResponse = staticServiceRest.refresh(); - StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); - assertEquals(expectedResponse, actualResponse); + void givenRefreshAPIWithUnSecureDiscoveryService_thenReturnApiResponseCodeWithBody() { + when(discoveryConfigProperties.getLocations()).thenReturn(new String[] { DISCOVERY_LOCATION_HTTP }); + + StepVerifier.create(staticServiceRest.refresh()) + .assertNext(actualResponse -> { + StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); + assertEquals(expectedResponse, actualResponse); + }) + .verifyComplete(); } + } @Nested class GivenTwoDiscoveryUrlsTest { + @Nested class WhenOneSucceedsTest { - @BeforeEach - void setup() throws IOException { - when(okResponse.getCode()).thenReturn(HttpStatus.OK.value()); - - when(okResponse.getEntity()).thenReturn(entity); - when(entity.getContent()).thenReturn(new ByteArrayInputStream(BODY.getBytes())); - } @Test - void whenFirstSucceeds_thenReturnResponseFromFirst() throws IOException { + void whenFirstSucceeds_thenReturnResponseFromFirst() { + doReturn(Mono.just(clientResponse)).when(exchangeFunction).exchange(any()); + doReturn(Mono.empty()).when(clientResponse).releaseBody(); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(BODY)).when(clientResponse).bodyToMono(String.class); when(discoveryConfigProperties.getLocations()).thenReturn(discoveryLocations); - mockRestTemplateExchange(DISCOVERY_LOCATION); - StaticAPIResponse actualResponse = staticServiceRest.refresh(); - StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); - assertEquals(expectedResponse, actualResponse); + + StepVerifier.create(staticServiceRest.refresh()) + .assertNext(actualResponse -> { + StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); + assertEquals(expectedResponse, actualResponse); + }) + .verifyComplete(); } @Test - void whenFirstFails_thenReturnResponseFromSecond() throws IOException { + void whenFirstFails_thenReturnResponseFromSecond() { + doAnswer(answer -> { + ClientRequest clientRequest = answer.getArgument(0); + ClientResponse clientResponse = mock(ClientResponse.class); + doReturn(Mono.empty()).when(clientResponse).releaseBody(); + switch (clientRequest.url().getPort()) { + case 60004: + doReturn(HttpStatusCode.valueOf(SC_NOT_FOUND)).when(clientResponse).statusCode(); + doReturn(Mono.just("")).when(clientResponse).bodyToMono(String.class); + break; + case 60005: + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(BODY)).when(clientResponse).bodyToMono(String.class); + break; + default: + fail("Unexpected discovery service with URL: " + clientRequest.url()); + } + return Mono.just(clientResponse); + }).when(exchangeFunction).exchange(any()); when(discoveryConfigProperties.getLocations()).thenReturn(discoveryLocations); - when(notFoundResponse.getCode()).thenReturn(HttpStatus.NOT_FOUND.value()); - mockRestTemplateExchange(DISCOVERY_LOCATION_2); - StaticAPIResponse actualResponse = staticServiceRest.refresh(); - StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); - assertEquals(expectedResponse, actualResponse); + + StepVerifier.create(staticServiceRest.refresh()) + .assertNext(actualResponse -> { + StaticAPIResponse expectedResponse = new StaticAPIResponse(200, BODY); + assertEquals(expectedResponse, actualResponse); + }) + .verifyComplete(); } + } @Nested class WhenBothFailsTest { + @Test - void whenBothFail_thenReturnResponseFromSecond() throws IOException { + void whenBothFail_thenReturnResponseFromSecond() { + doReturn(Mono.just(clientResponse)).when(exchangeFunction).exchange(any()); + doReturn(Mono.empty()).when(clientResponse).releaseBody(); when(discoveryConfigProperties.getLocations()).thenReturn(discoveryLocations); - when(notFoundResponse.getCode()).thenReturn(HttpStatus.NOT_FOUND.value()); - when(notFoundResponse.getEntity()).thenReturn(entity); - when(entity.getContent()).thenAnswer(invocation -> new ByteArrayInputStream(BODY.getBytes())); - mockRestTemplateExchange(DISCOVERY_LOCATION_3); - - StaticAPIResponse actualResponse = staticServiceRest.refresh(); - StaticAPIResponse expectedResponse = new StaticAPIResponse(404, BODY); - assertEquals(expectedResponse, actualResponse); + doReturn(HttpStatusCode.valueOf(SC_NOT_FOUND)).when(clientResponse).statusCode(); + doReturn(Mono.just(BODY)).when(clientResponse).bodyToMono(String.class); + + StepVerifier.create(staticServiceRest.refresh()) + .assertNext(actualResponse -> { + StaticAPIResponse expectedResponse = new StaticAPIResponse(404, BODY); + assertEquals(expectedResponse, actualResponse); + }) + .verifyComplete(); } + } + } - } + } @Test void givenNoDiscoveryLocations_whenAttemptRefresh_thenReturn500() { when(discoveryConfigProperties.getLocations()).thenReturn(new String[]{}); - StaticAPIResponse actualResponse = staticServiceRest.refresh(); - StaticAPIResponse expectedResponse = new StaticAPIResponse(500, "Error making static API refresh request to the Discovery Service"); - assertEquals(expectedResponse, actualResponse); + StepVerifier.create(staticServiceRest.refresh()) + .assertNext(actualResponse -> { + StaticAPIResponse expectedResponse = new StaticAPIResponse(500, "Error making static API refresh request to the Discovery Service"); + assertEquals(expectedResponse, actualResponse); + }) + .verifyComplete(); } - private void mockRestTemplateExchange(String discoveryUrl) throws IOException { - HttpPost post = new HttpPost(discoveryUrl.replace("/eureka", "") + REFRESH_ENDPOINT); - - when(httpClient.execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class))).thenAnswer(invocation -> { - HttpPost httpRequest = (HttpPost) invocation.getArguments()[0]; - URI uri = httpRequest.getUri(); - int i = uri.compareTo(post.getUri()); - - return HttpClientMockHelper.invokeResponseHandler(invocation, i == 0 ? okResponse : notFoundResponse); - }); - } } 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 db94d967d3..e6e7429cfe 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 @@ -11,9 +11,6 @@ package org.zowe.apiml.apicatalog.swagger; import com.netflix.appinfo.InstanceInfo; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -27,8 +24,12 @@ import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.http.HttpStatusCode; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; import org.zowe.apiml.apicatalog.exceptions.ApiDocNotFoundException; import org.zowe.apiml.apicatalog.exceptions.ApiVersionNotFoundException; import org.zowe.apiml.apicatalog.model.ApiDocInfo; @@ -37,7 +38,6 @@ import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; -import org.zowe.apiml.util.HttpClientMockHelper; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -48,6 +48,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +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.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -84,19 +86,24 @@ class ViaRestCall { private ApimlLogger apimlLogger; @Mock - private CloseableHttpClient httpClient; + private ExchangeFunction exchangeFunction; @Mock - private CloseableHttpResponse response; + private ClientResponse clientResponse; + + private WebClient webClient; private AtomicReference lastApiInfo = new AtomicReference<>(); @BeforeEach void setup() { lastApiInfo.set(null); + webClient = spy(WebClient.builder().exchangeFunction(exchangeFunction).build()); + doReturn(Mono.just(clientResponse)).when(exchangeFunction).exchange(any()); + lenient().doReturn(Mono.empty()).when(clientResponse).releaseBody(); + lenient().doReturn(Mono.empty()).when(clientResponse).bodyToMono(String.class); - HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); - var apiDocRetrievalServiceRest = new ApiDocRetrievalServiceRest(httpClient); + var apiDocRetrievalServiceRest = new ApiDocRetrievalServiceRest(webClient); apiDocService = new ApiDocService( discoveryClient, new GatewayClient(GW_SERVICE_ADDRESS), @@ -129,7 +136,8 @@ void givenValidApiInfo_thenReturnApiDoc() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) .assertNext(actualApiDoc -> { @@ -158,16 +166,14 @@ void givenNoApiDocFoundForService() { @Test void givenServerErrorWhenRequestingSwaggerUrl() { - String responseBody = "Server not found"; - when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_INTERNAL_SERVER_ERROR, responseBody); + 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); - assertEquals("No API Documentation was retrieved due to " + SERVICE_ID + " server error: 500 " + responseBody, exception.getMessage()); + assertEquals("No API Documentation was retrieved due to " + SERVICE_ID + " server error: 500", exception.getMessage()); } } @@ -210,7 +216,8 @@ void givenNoSwaggerUrl_thenReturnSubstituteApiDoc() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutSwaggerUrl(), true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) .assertNext(actualApiDoc -> { @@ -234,7 +241,8 @@ void givenApiDocUrlInRouting_thenCreateApiDocUrlFromRoutingAndReturnApiDoc() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) .assertNext(actualApiDoc -> { @@ -253,7 +261,8 @@ void shouldCreateApiDocUrlFromRoutingAndUseHttp() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), false))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveApiDoc(SERVICE_ID, SERVICE_VERSION_V)) .assertNext(actualApiDoc -> { @@ -271,7 +280,9 @@ void givenServerCommunicationErrorWhenRequestingSwaggerUrl_thenLogCustomError() .thenReturn(Collections.singletonList(getStandardInstance(getStandardMetadata(), true))); var exception = new IOException("Unable to reach the host"); - HttpClientMockHelper.whenExecuteThenThrow(httpClient, exception); + doReturn(HttpStatusCode.valueOf(SC_INTERNAL_SERVER_ERROR)).when(clientResponse).statusCode(); + doReturn(Mono.error(exception)).when(exchangeFunction).exchange(any()); + Mono apiDocMono = apiDocService.retrieveDefaultApiDoc(SERVICE_ID); assertThrows(ApiDocNotFoundException.class, apiDocMono::block); @@ -296,7 +307,8 @@ void givenDefaultApiDoc_thenReturnIt() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(metadata, true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) .assertNext(actualApiDoc -> { @@ -323,7 +335,8 @@ void givenNoDefaultApiDoc_thenReturnHighestVersion() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(metadata, true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) .assertNext(actualApiDoc -> { @@ -348,7 +361,8 @@ void givenNoDefaultApiDocAndDifferentVersionFormat_thenReturnHighestVersion() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithMultipleApiInfoWithDifferentVersionFormat(), true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) .assertNext(actualApiDoc -> { @@ -373,7 +387,8 @@ void givenNoApiDocs_thenReturnNull() { when(discoveryClient.getInstances(SERVICE_ID)) .thenReturn(Collections.singletonList(getStandardInstance(getMetadataWithoutApiInfo(), true))); - HttpClientMockHelper.mockResponse(response, HttpStatus.SC_OK, responseBody); + doReturn(HttpStatusCode.valueOf(SC_OK)).when(clientResponse).statusCode(); + doReturn(Mono.just(responseBody)).when(clientResponse).bodyToMono(String.class); var elapsed = StepVerifier.create(apiDocService.retrieveDefaultApiDoc(SERVICE_ID)) .assertNext(actualApiDoc -> { diff --git a/api-catalog-services/src/test/resources/application.yml b/api-catalog-services/src/test/resources/application.yml index f5bed0e151..2666f90cf0 100644 --- a/api-catalog-services/src/test/resources/application.yml +++ b/api-catalog-services/src/test/resources/application.yml @@ -87,6 +87,8 @@ apiml: cacheRefreshInitialDelayInMillis: 30000 cacheRefreshRetryDelayInMillis: 30000 + webClientConfig.enabled: true + ############################################################################################## server: diff --git a/apiml-common/build.gradle b/apiml-common/build.gradle index 7c1062222c..fb710e2bb1 100644 --- a/apiml-common/build.gradle +++ b/apiml-common/build.gradle @@ -8,6 +8,7 @@ dependencies { implementation libs.spring.boot.starter.actuator implementation libs.spring.boot.starter.web implementation libs.spring.cloud.starter.eureka.client + compileOnly libs.netty.reactor.http implementation libs.eureka.core implementation libs.apache.commons.lang3 diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index b441305e69..b92af1420d 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -132,7 +132,7 @@ public String retrieveApiBasePath(String serviceId, } // Make base path version a template so user can understand base path when looking at different API versions - String templatedVersionRoute = route.getGatewayUrl().replaceAll("/v\\d", "/{api-version}"); + String templatedVersionRoute = route.getGatewayUrl().replaceAll("/v\\d+", "/{api-version}"); return String.format("/%s/%s", serviceId, diff --git a/apiml-security-common/build.gradle b/apiml-security-common/build.gradle index 857804ec41..4710ab1546 100644 --- a/apiml-security-common/build.gradle +++ b/apiml-security-common/build.gradle @@ -5,6 +5,9 @@ dependencies { implementation libs.spring.boot.starter.security implementation libs.reactor implementation libs.spring.webflux + implementation libs.guava + compileOnly libs.netty.reactor.http + compileOnly libs.spring.cloud.gateway.server implementation libs.apache.commons.lang3 implementation libs.http.client5 diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java new file mode 100644 index 0000000000..68ddf3da9b --- /dev/null +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java @@ -0,0 +1,96 @@ +/* + * 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.security.common.config; + +import io.netty.handler.ssl.SslContext; +import io.netty.resolver.DefaultAddressResolverGroup; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.cloud.gateway.config.HttpClientCustomizer; +import org.springframework.cloud.gateway.config.HttpClientFactory; +import org.springframework.cloud.gateway.config.HttpClientProperties; +import org.springframework.cloud.gateway.config.HttpClientSslConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; +import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.security.HttpsConfigError; +import org.zowe.apiml.security.common.util.ConnectionUtil; +import reactor.netty.http.client.HttpClient; + +import java.util.List; + +@Slf4j +@Configuration +@RequiredArgsConstructor +@ConditionalOnProperty(name = "apiml.webClientConfig.enabled", havingValue = "true") +public class WebClientConfig { + + private final HttpConfig config; + + private static final ApimlLogger apimlLog = ApimlLogger.of(WebClientConfig.class, YamlMessageServiceInstance.getInstance()); + + @Bean + HttpClientFactory gatewayHttpClientFactory( + HttpClientProperties properties, + ServerProperties serverProperties, List customizers, + HttpClientSslConfigurer sslConfigurer + ) { + SslContext sslContext; + try { + sslContext = ConnectionUtil.getSslContext(config, false); + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); + throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config.httpsConfig()); + } + return new HttpClientFactory(properties, serverProperties, sslConfigurer, customizers) { + @Override + protected HttpClient createInstance() { + return super.createInstance() + .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)) + .resolver(DefaultAddressResolverGroup.INSTANCE); + } + }; + } + + HttpClient getHttpClient(HttpClient httpClient, boolean useClientCert) { + try { + return ConnectionUtil.getHttpClient(config, httpClient, useClientCert); + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); + throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config.httpsConfig()); + } + } + + @Bean + @Primary + WebClient webClient(HttpClient httpClient) { + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, false))) + .build(); + } + + @Bean + WebClient webClientClientCert(HttpClient httpClient) { + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, true))) + .build(); + } + +} diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java new file mode 100644 index 0000000000..e096e0f9ba --- /dev/null +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java @@ -0,0 +1,131 @@ +/* + * 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.security.common.util; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.security.SecurityUtils; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientSecurityUtils; +import reactor.netty.tcp.SslProvider; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509KeyManager; +import java.io.IOException; +import java.net.Socket; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import com.google.common.annotations.VisibleForTesting; + +@UtilityClass +@Slf4j +public class ConnectionUtil { + + /** + * @return io.netty.handler.ssl.SslContext for http client. + */ + public SslContext getSslContext(HttpConfig config, boolean setKeystore) throws CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException { + SslContextBuilder builder = SslContextBuilder.forClient(); + + KeyStore trustStore = SecurityUtils.loadKeyStore( + config.getTrustStoreType(), config.getTrustStorePath(), config.getTrustStorePassword()); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + builder.trustManager(trustManagerFactory); + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (setKeystore) { + log.info("Loading keystore: {}: {}", config.getKeyStoreType(), config.getKeyStorePath()); + KeyStore keyStore = SecurityUtils.loadKeyStore( + config.getKeyStoreType(), config.getKeyStorePath(), config.getKeyStorePassword()); + keyManagerFactory.init(keyStore, config.getKeyStorePassword()); + builder.keyManager(x509KeyManagerSelectedAlias(config, keyManagerFactory)); + } else { + KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); + emptyKeystore.load(null, null); + keyManagerFactory.init(emptyKeystore, null); + builder.keyManager(keyManagerFactory); + } + + if (config.isVerifySslCertificatesOfServices() && config.isNonStrictVerifySslCertificatesOfServices()) { + builder.endpointIdentificationAlgorithm(null); + } + + return builder.build(); + } + + public HttpClient getHttpClient(HttpConfig config, HttpClient httpClient, boolean useClientCert) throws UnrecoverableKeyException, CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException { + var sslContextBuilder = SslProvider.builder().sslContext(ConnectionUtil.getSslContext(config, useClientCert)); + if (!config.isNonStrictVerifySslCertificatesOfServices()) { + sslContextBuilder.handlerConfigurator(HttpClientSecurityUtils.HOSTNAME_VERIFICATION_CONFIGURER); + } + return httpClient.secure(sslContextBuilder.build()); + } + + @VisibleForTesting + public X509KeyManager x509KeyManagerSelectedAlias(HttpConfig config, KeyManagerFactory keyManagerFactory) { + return new ConnectionUtil.X509KeyManagerSelectedAlias(keyManagerFactory, config.getKeyAlias()); + } + + public static class X509KeyManagerSelectedAlias implements X509KeyManager { + + private final X509KeyManager originalKm; + private final String keyAlias; + + public X509KeyManagerSelectedAlias(KeyManagerFactory keyManagerFactory, String keyAlias) { + this.originalKm = (X509KeyManager) keyManagerFactory.getKeyManagers()[0]; + this.keyAlias = keyAlias; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return originalKm.getClientAliases(keyType, issuers); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + if (keyAlias != null) { + return keyAlias; + } + return originalKm.chooseClientAlias(keyType, issuers, socket); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return originalKm.getServerAliases(keyType, issuers); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + if (keyAlias != null) { + return keyAlias; + } + return originalKm.chooseServerAlias(keyType, issuers, socket); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return originalKm.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return originalKm.getPrivateKey(alias); + } + + } + +} diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 8b89f01ac9..028dda004d 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -242,6 +242,7 @@ apiml: verify: https://${apiml.service.hostname}:10013/zss/saf/verify health: protected: true + webClientConfig.enabled: true server: http2: enabled: false diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml index d319ffb562..d4b4698263 100644 --- a/apiml/src/test/resources/application.yml +++ b/apiml/src/test/resources/application.yml @@ -101,6 +101,7 @@ apiml: urls: authenticate: https://mock-services:10013/zss/saf/authenticate verify: https://localhost:10013/zss/saf/verify + webClientConfig.enabled: true caching: storage: mode: infinispan diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index 492579af56..14e75fab45 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -10,19 +10,11 @@ package org.zowe.apiml.gateway.config; -import com.google.common.annotations.VisibleForTesting; -import com.netflix.appinfo.ApplicationInfoManager; -import com.netflix.appinfo.EurekaInstanceConfig; -import com.netflix.appinfo.HealthCheckHandler; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.appinfo.LeaseInfo; +import com.netflix.appinfo.*; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; import io.github.resilience4j.timelimiter.TimeLimiterConfig; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.resolver.DefaultAddressResolverGroup; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; @@ -35,15 +27,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder; import org.springframework.cloud.client.circuitbreaker.Customizer; import org.springframework.cloud.context.config.annotation.RefreshScope; -import org.springframework.cloud.gateway.config.HttpClientCustomizer; -import org.springframework.cloud.gateway.config.HttpClientFactory; import org.springframework.cloud.gateway.config.HttpClientProperties; -import org.springframework.cloud.gateway.config.HttpClientSslConfigurer; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.netflix.eureka.CloudEurekaClient; import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; @@ -57,12 +45,9 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.Primary; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestClient; import org.springframework.web.cors.reactive.CorsWebFilter; -import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.WebFilter; import org.springframework.web.util.UriComponentsBuilder; import org.zowe.apiml.config.AdditionalRegistration; @@ -75,36 +60,18 @@ import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.web.HttpConfig; import org.zowe.apiml.security.HttpsConfigError; -import org.zowe.apiml.security.SecurityUtils; +import org.zowe.apiml.security.common.util.ConnectionUtil; import org.zowe.apiml.util.CorsUtils; import reactor.netty.http.client.HttpClient; -import reactor.netty.http.client.HttpClientSecurityUtils; -import reactor.netty.tcp.SslProvider; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509KeyManager; import java.net.MalformedURLException; -import java.net.Socket; import java.net.URL; -import java.security.KeyStore; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static org.springframework.cloud.netflix.eureka.EurekaClientConfigBean.DEFAULT_ZONE; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.REGISTRATION_TYPE; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; - +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; //TODO this configuration should be removed as redundancy of the HttpConfig in the apiml-common @Configuration @@ -132,15 +99,17 @@ public class ConnectionsConfig { */ @Bean NettyRoutingFilterApiml createNettyRoutingFilterApiml(HttpClient httpClient, ObjectProvider> headersFiltersProvider, HttpClientProperties properties) { - return new NettyRoutingFilterApiml(getHttpClient(httpClient, false), getHttpClient(httpClient, true), headersFiltersProvider, properties); - } - - public HttpClient getHttpClient(HttpClient httpClient, boolean useClientCert) { - var sslContextBuilder = SslProvider.builder().sslContext(getSslContext(useClientCert)); - if (!config.isNonStrictVerifySslCertificatesOfServices()) { - sslContextBuilder.handlerConfigurator(HttpClientSecurityUtils.HOSTNAME_VERIFICATION_CONFIGURER); + try { + return new NettyRoutingFilterApiml( + ConnectionUtil.getHttpClient(config, httpClient, false), + ConnectionUtil.getHttpClient(config, httpClient, true), + headersFiltersProvider, properties + ); + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); + throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config.httpsConfig()); } - return httpClient.secure(sslContextBuilder.build()); } /** @@ -167,50 +136,6 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro }; } - @VisibleForTesting - X509KeyManager x509KeyManagerSelectedAlias(KeyManagerFactory keyManagerFactory) { - return new X509KeyManagerSelectedAlias(keyManagerFactory, config.getKeyAlias()); - } - - /** - * @return io.netty.handler.ssl.SslContext for http client. - */ - SslContext getSslContext(boolean setKeystore) { - try { - SslContextBuilder builder = SslContextBuilder.forClient(); - - KeyStore trustStore = SecurityUtils.loadKeyStore( - config.getTrustStoreType(), config.getTrustStorePath(), config.getTrustStorePassword()); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - builder.trustManager(trustManagerFactory); - - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - if (setKeystore) { - log.info("Loading keystore: {}: {}", config.getKeyStoreType(), config.getKeyStorePath()); - KeyStore keyStore = SecurityUtils.loadKeyStore( - config.getKeyStoreType(), config.getKeyStorePath(), config.getKeyStorePassword()); - keyManagerFactory.init(keyStore, config.getKeyStorePassword()); - builder.keyManager(x509KeyManagerSelectedAlias(keyManagerFactory)); - } else { - KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); - emptyKeystore.load(null, null); - keyManagerFactory.init(emptyKeystore, null); - builder.keyManager(keyManagerFactory); - } - - if (config.isVerifySslCertificatesOfServices() && config.isNonStrictVerifySslCertificatesOfServices()) { - builder.endpointIdentificationAlgorithm(null); - } - - return builder.build(); - } catch (Exception e) { - apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); - throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, - HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config.httpsConfig()); - } - } - @Bean(destroyMethod = "shutdown", name = "eurekaClient") @RefreshScope @ConditionalOnMissingBean(EurekaClient.class) @@ -339,38 +264,6 @@ Customizer defaultCustomizer() { .build()).build()); } - @Bean - HttpClientFactory gatewayHttpClientFactory( - HttpClientProperties properties, - ServerProperties serverProperties, List customizers, - HttpClientSslConfigurer sslConfigurer - ) { - SslContext sslContext = getSslContext(false); - return new HttpClientFactory(properties, serverProperties, sslConfigurer, customizers) { - @Override - protected HttpClient createInstance() { - return super.createInstance() - .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)) - .resolver(DefaultAddressResolverGroup.INSTANCE); - } - }; - } - - @Bean - @Primary - WebClient webClient(HttpClient httpClient) { - return WebClient.builder() - .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, false))) - .build(); - } - - @Bean - WebClient webClientClientCert(HttpClient httpClient) { - return WebClient.builder() - .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, true))) - .build(); - } - @Bean CorsUtils corsUtils() { return new CorsUtils(corsEnabled, null); @@ -503,52 +396,4 @@ interface NonDelegated { } - static class X509KeyManagerSelectedAlias implements X509KeyManager { - - private final X509KeyManager originalKm; - private final String keyAlias; - - X509KeyManagerSelectedAlias(KeyManagerFactory keyManagerFactory, String keyAlias) { - this.originalKm = (X509KeyManager) keyManagerFactory.getKeyManagers()[0]; - this.keyAlias = keyAlias; - } - - @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { - return originalKm.getClientAliases(keyType, issuers); - } - - @Override - public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { - if (keyAlias != null) { - return keyAlias; - } - return originalKm.chooseClientAlias(keyType, issuers, socket); - } - - @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { - return originalKm.getServerAliases(keyType, issuers); - } - - @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { - if (keyAlias != null) { - return keyAlias; - } - return originalKm.chooseServerAlias(keyType, issuers, socket); - } - - @Override - public X509Certificate[] getCertificateChain(String alias) { - return originalKm.getCertificateChain(alias); - } - - @Override - public PrivateKey getPrivateKey(String alias) { - return originalKm.getPrivateKey(alias); - } - - } - } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java index d773c7f2bc..33134e78c4 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java @@ -18,6 +18,10 @@ import org.springframework.web.reactive.socket.client.WebSocketClient; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.zowe.apiml.gateway.websocket.ApimlRequestUpgradeStrategy; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.security.HttpsConfigError; +import org.zowe.apiml.security.common.util.ConnectionUtil; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.WebsocketClientSpec; @@ -25,10 +29,19 @@ @Configuration public class WebSocketConfig { + private static final ApimlLogger apimlLog = ApimlLogger.empty(); + @Bean @Primary - WebSocketClient webSocketClient(ConnectionsConfig connectionsConfig, HttpClient httpClient) { - var secureClient = connectionsConfig.getHttpClient(httpClient, false); + WebSocketClient webSocketClient(HttpConfig config, HttpClient httpClient) { + HttpClient secureClient; + try { + secureClient = ConnectionUtil.getHttpClient(config, httpClient, false); + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); + throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config.httpsConfig()); + } var spec = WebsocketClientSpec.builder() .handlePing(true); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index ac97301582..594a14b4e7 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -122,7 +122,7 @@ private boolean isMatching(RoutedService route, URI uri) { private Mono processNewLocationUrl(ServerWebExchange exchange, Config config, Optional instance) { var response = exchange.getResponse(); - var isRedirect = Optional.ofNullable(response.getStatusCode()).map(HttpStatusCode::is3xxRedirection).orElse(false); + boolean isRedirect = Optional.ofNullable(response.getStatusCode()).map(HttpStatusCode::is3xxRedirection).orElse(false); if (!isRedirect) { return Mono.empty(); } diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 5103206087..5e07950224 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -116,6 +116,7 @@ apiml: nonStrictVerifySslCertificatesOfServices: false health: protected: true + webClientConfig.enabled: true server: http2: enabled: false @@ -167,6 +168,7 @@ management: base-path: /application exposure: include: health,info,gateway + --- spring.config.activate.on-profile: debug diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java index 42feefa4b7..360ce10865 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java @@ -21,6 +21,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; @@ -34,38 +36,25 @@ import org.springframework.web.server.WebFilter; import org.zowe.apiml.gateway.GatewayServiceApplication; import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.security.common.util.ConnectionUtil; import reactor.netty.http.client.HttpClient; import reactor.netty.tcp.SslProvider; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.X509KeyManager; +import java.io.IOException; import java.net.MalformedURLException; import java.net.Socket; -import java.security.Principal; -import java.security.PrivateKey; +import java.security.*; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; class ConnectionsConfigTest { @@ -128,27 +117,32 @@ class UsingX509KeyManagerSelectedAlias { @MockitoSpyBean private ConnectionsConfig connectionsConfig; + @Autowired + private HttpConfig httpConfig; + @Test - void whenAliasIsSet_thenReturnItByX509KeyManagerSelectedAlias() { + void whenAliasIsSet_thenReturnItByX509KeyManagerSelectedAlias() throws UnrecoverableKeyException, CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException { AtomicReference returnValue = new AtomicReference<>(); - doAnswer(answer -> { - if (returnValue.get() == null) { - returnValue.set(spy((X509KeyManager) answer.callRealMethod())); - } - return returnValue.get(); - }).when(connectionsConfig).x509KeyManagerSelectedAlias(any()); - - var sslContext = connectionsConfig.getSslContext(true); - var sslProvider = SslProvider.builder().sslContext(sslContext).build(); - var httpClient = HttpClient.create().secure(sslProvider); - reset(returnValue.get()); - httpClient.get() - .uri(String.format("https://localhost:%d/", port)) - .response().block(); - assertNotNull(SslDetectorConfig.sslInfoHolder.get()); - - verify(returnValue.get(), atLeastOnce()).chooseClientAlias(any(), any(), any()); - assertEquals(keyAlias, returnValue.get().chooseClientAlias(null, null, null)); + try (MockedStatic connectionUtilMockedStatic = mockStatic(ConnectionUtil.class, InvocationOnMock::callRealMethod)) { + connectionUtilMockedStatic.when(() -> ConnectionUtil.x509KeyManagerSelectedAlias(any(), any())).then(answer -> { + if (returnValue.get() == null) { + returnValue.set(spy((X509KeyManager) answer.callRealMethod())); + } + return returnValue.get(); + }); + + var sslContext = ConnectionUtil.getSslContext(httpConfig, true); + var sslProvider = SslProvider.builder().sslContext(sslContext).build(); + var httpClient = HttpClient.create().secure(sslProvider); + reset(returnValue.get()); + httpClient.get() + .uri(String.format("https://localhost:%d/", port)) + .response().block(); + assertNotNull(SslDetectorConfig.sslInfoHolder.get()); + + verify(returnValue.get(), atLeastOnce()).chooseClientAlias(any(), any(), any()); + assertEquals(keyAlias, returnValue.get().chooseClientAlias(null, null, null)); + } } } @@ -156,16 +150,14 @@ void whenAliasIsSet_thenReturnItByX509KeyManagerSelectedAlias() { @Nested class Negative { - @Autowired - private ConnectionsConfig connectionsConfig; @MockitoSpyBean private HttpConfig httpConfig; @Test - void whenAliasIsInvalid_thenNoCertificateProvided() { + void whenAliasIsInvalid_thenNoCertificateProvided() throws UnrecoverableKeyException, CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException { when(httpConfig.getKeyAlias()).thenReturn("invalid"); - var sslContext = connectionsConfig.getSslContext(true); + var sslContext = ConnectionUtil.getSslContext(httpConfig,true); var sslProvider = SslProvider.builder().sslContext(sslContext).build(); var httpClient = HttpClient.create().secure(sslProvider); httpClient.get() @@ -197,7 +189,7 @@ class Wrapper { void whenGetClientAliases_thenRecall() { doReturn(ALIASES).when(origKeyManager).getClientAliases(KEY_TYPE, ISSUERS); assertSame(ALIASES, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) .getClientAliases(KEY_TYPE, ISSUERS) ); verify(origKeyManager).getClientAliases(KEY_TYPE, ISSUERS); @@ -207,7 +199,7 @@ void whenGetClientAliases_thenRecall() { void givenNoAlias_whenChooseClientAlias_thenRecall() { doReturn(ALIAS).when(origKeyManager).chooseClientAlias(KEY_TYPES, ISSUERS, SOCKET); assertSame(ALIAS, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, null) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, null) .chooseClientAlias(KEY_TYPES, ISSUERS, SOCKET) ); verify(origKeyManager).chooseClientAlias(KEY_TYPES, ISSUERS, SOCKET); @@ -216,7 +208,7 @@ void givenNoAlias_whenChooseClientAlias_thenRecall() { @Test void givenAlias_whenChooseClientAlias_thenReturnAlias() { assertSame(CONFIG_ALIAS, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) .chooseClientAlias(KEY_TYPES, ISSUERS, SOCKET) ); verify(origKeyManager, never()).chooseClientAlias(KEY_TYPES, ISSUERS, SOCKET); @@ -226,7 +218,7 @@ void givenAlias_whenChooseClientAlias_thenReturnAlias() { void whenGetServerAliases_thenRecall() { doReturn(ALIASES).when(origKeyManager).getServerAliases(KEY_TYPE, ISSUERS); assertSame(ALIASES, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) .getServerAliases(KEY_TYPE, ISSUERS) ); verify(origKeyManager).getServerAliases(KEY_TYPE, ISSUERS); @@ -236,7 +228,7 @@ void whenGetServerAliases_thenRecall() { void givenNoAlias_whenChooseServerAlias_thenRecall() { doReturn(ALIAS).when(origKeyManager).chooseServerAlias(KEY_TYPE, ISSUERS, SOCKET); assertSame(ALIAS, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, null) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, null) .chooseServerAlias(KEY_TYPE, ISSUERS, SOCKET) ); verify(origKeyManager).chooseServerAlias(KEY_TYPE, ISSUERS, SOCKET); @@ -245,7 +237,7 @@ void givenNoAlias_whenChooseServerAlias_thenRecall() { @Test void givenAlias_whenChooseServerAlias_thenReturnAlias() { assertSame(CONFIG_ALIAS, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) .chooseServerAlias(KEY_TYPE, ISSUERS, SOCKET) ); verify(origKeyManager, never()).chooseServerAlias(KEY_TYPE, ISSUERS, SOCKET); @@ -255,7 +247,7 @@ void givenAlias_whenChooseServerAlias_thenReturnAlias() { void whenGetCertificateChain_thenRecall() { doReturn(CERTIFICATES).when(origKeyManager).getCertificateChain(ALIAS); assertSame(CERTIFICATES, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) .getCertificateChain(ALIAS) ); verify(origKeyManager).getCertificateChain(ALIAS); @@ -265,7 +257,7 @@ void whenGetCertificateChain_thenRecall() { void whenGetPrivateKey_thenRecall() { doReturn(PRIVATE_KEY).when(origKeyManager).getPrivateKey(ALIAS); assertSame(PRIVATE_KEY, - new ConnectionsConfig.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) + new ConnectionUtil.X509KeyManagerSelectedAlias(origKeyManagerFactory, CONFIG_ALIAS) .getPrivateKey(ALIAS) ); verify(origKeyManager).getPrivateKey(ALIAS); diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml index 3152dc7914..f4adab4ed0 100644 --- a/gateway-service/src/test/resources/application.yml +++ b/gateway-service/src/test/resources/application.yml @@ -16,6 +16,7 @@ apiml: gateway: serviceRegistryEnabled: false forwardClientCertEnabled: false + webClientConfig.enabled: true server: port: ${apiml.service.port} diff --git a/gradle/versions.gradle b/gradle/versions.gradle index ff4a2794fc..db0f728e52 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -136,6 +136,7 @@ dependencyResolutionManagement { library('spring_cloud_starter_eureka_client', 'org.springframework.cloud', 'spring-cloud-starter-netflix-eureka-client').versionRef('springCloudNetflix') library('spring_cloud_starter_eureka_server', 'org.springframework.cloud', 'spring-cloud-starter-netflix-eureka-server').versionRef('springCloudNetflix') library('spring_cloud_starter_gateway', 'org.springframework.cloud', 'spring-cloud-starter-gateway-server-webflux').versionRef('springCloudGateway') + library('spring_cloud_gateway_server', 'org.springframework.cloud', 'spring-cloud-gateway-server').versionRef('springCloudGateway') library('spring_cloud_commons', 'org.springframework.cloud', 'spring-cloud-commons').versionRef('springCloudCommons') library('spring_cloud_circuit_breaker', 'org.springframework.cloud', 'spring-cloud-starter-circuitbreaker-reactor-resilience4j').versionRef('springCloudCB') diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtilsTest.java b/integration-tests/src/test/java/org/zowe/apiml/util/ConnectionUtilsTest.java similarity index 97% rename from integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtilsTest.java rename to integration-tests/src/test/java/org/zowe/apiml/util/ConnectionUtilsTest.java index b68ed66f8d..9264466c53 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtilsTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/ConnectionUtilsTest.java @@ -17,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -class SecurityUtilsTest { +class ConnectionUtilsTest { @Nested class JwtParser { @@ -34,4 +34,4 @@ void givenNoKey_whenParseJwtStringUnsecure_thenParseIt(String jwt, String name) } -} \ No newline at end of file +} From 00e663e4bf69b1369becbf796057f796f78a9711 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:16:35 +0200 Subject: [PATCH 041/152] fix: multi-tenancy deployment in single-service(modulith) mode (#4249) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 182 +++++++++--------- api-catalog-ui/frontend/.env | 1 + api-catalog-ui/frontend/.env.production | 1 + .../frontend/cypress.modulith.config.js | 3 +- .../cypress/e2e/dashboard/dashboard.cy.js | 8 +- .../cypress/e2e/detail-page/detail-page.cy.js | 7 +- .../detail-page/service-version-compare.cy.js | 5 +- .../frontend/cypress/e2e/login/login-ok.cy.js | 2 +- api-catalog-ui/frontend/package.json | 4 +- .../java/org/zowe/apiml/ModulithConfig.java | 138 +++++++------ .../org/zowe/apiml/WebSecurityConfig.java | 1 - .../org/zowe/apiml/ModulithConfigTest.java | 79 ++++++++ build.gradle | 10 - .../src/main/resources/infinispan.xml | 1 + .../zowe/apiml/services/BasicInfoService.java | 2 +- config/local/apiml-service-2.yml | 2 + .../gateway/config/ConnectionsConfig.java | 2 + .../apiml/gateway/config/RegistryConfig.java | 2 + .../gateway/services/ServicesInfoService.java | 2 +- integration-tests/build.gradle | 33 +--- .../gateway/CentralRegistryTest.java | 2 +- .../providers/SafLoginTest.java | 1 + .../integration/zos/ServicesInfoTest.java | 55 ++---- .../apiml/zaas/security/login/Providers.java | 3 +- 24 files changed, 292 insertions(+), 254 deletions(-) create mode 100644 apiml/src/test/java/org/zowe/apiml/ModulithConfigTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 62fb547316..a5488534dd 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -495,47 +495,6 @@ jobs: - uses: ./.github/actions/teardown - Oauth2Integration: - needs: PublishJibContainers - runs-on: ubuntu-latest - container: ubuntu:latest - timeout-minutes: 10 - services: - gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - zaas-service: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SECURITY_OIDC_CLIENTID: ${{ secrets.OKTA_CLIENT_ID }} - APIML_SECURITY_OIDC_CLIENTSECRET: ${{ secrets.OKTA_CLIENT_PASSWORD }} - APIML_SECURITY_OIDC_ENABLED: true - APIML_SECURITY_OIDC_REGISTRY: zowe.okta.com - APIML_SECURITY_OIDC_JWKS_URI: ${{ secrets.OKTA_JWKSET_URI }} - APIML_SECURITY_OIDC_IDENTITYMAPPERUSER: APIMTST - APIML_SECURITY_OIDC_IDENTITYMAPPERURL: https://gateway-service:10010/zss/api/v1/certificate/dn - discovery-service: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - mock-services: - image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - discoverable-client: - image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} - - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - uses: ./.github/actions/setup - - - name: Run CI Tests - run: > - ./gradlew :integration-tests:runOidcTests --info -Denvironment.config=-docker -Dokta.client.id=${{ secrets.OKTA_CLIENT_ID }} - -Doidc.test.user=${{ secrets.OIDC_TEST_USER }} -Doidc.test.pass=${{ secrets.OIDC_TEST_PASS }} - -Doidc.test.alt_user=${{ secrets.OKTA_WINNIE_USER }} -Doidc.test.alt_pass=${{ secrets.OKTA_WINNIE_PASS }} - -DidpConfiguration.host=${{secrets.OKTA_HOST}} - - - uses: ./.github/actions/teardown - GatewayProxy: needs: PublishJibContainers runs-on: ubuntu-latest @@ -609,54 +568,6 @@ jobs: - uses: ./.github/actions/teardown - GatewayServiceRouting: - needs: PublishJibContainers - runs-on: ubuntu-latest - container: ubuntu:latest - timeout-minutes: 10 - - services: - discovery-service: - image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_APIMLID: apiml1 - zaas-service: - image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} - central-gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - discoverable-client: - image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} - mock-services: - image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - uses: ./.github/actions/setup - - - name: Run CI Tests - run: > - ./gradlew :integration-tests:runGatewayServiceRoutingTest --info -Denvironment.config=-docker - -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} - - - name: Dump CGW jacoco data - run: > - java -jar ./scripts/jacococli.jar dump --address gateway-service --port 6300 --destfile ./results/gateway-service.exec - - - name: Store results - uses: actions/upload-artifact@v4 - if: always() - with: - name: GatewayServiceRouting-${{ env.JOB_ID }} - path: | - integration-tests/build/reports/** - results/** - - - uses: ./.github/actions/teardown - GatewayCentralRegistry: needs: PublishJibContainers runs-on: ubuntu-latest @@ -755,6 +666,88 @@ jobs: - uses: ./.github/actions/teardown + CentralRegistryModulith: + needs: PublishJibContainers + runs-on: ubuntu-latest + container: ubuntu:latest + timeout-minutes: 10 + + services: + # Domain apiml instance which registers it's gateway in central's discovery service + apiml: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_HOSTNAME: apiml + APIML_SERVICE_APIMLID: domain-apiml + APIML_SERVICE_EXTERNALURL: https://apiml:10010 + APIML_GATEWAY_REGISTRY_ENABLED: false + APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER + APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka/ + APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://central-gateway-service:10011/eureka + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: / + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: / + APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true + APIML_SECURITY_X509_CERTIFICATESURL: https://central-gateway-service:10010/gateway/certificates + logbackService: ZWEAGW1 + APIML_HEALTH_PROTECTED: false + APIML_SECURITY_X509_ENABLED: true + + + # Central apiml instance with central gateway registry + central-gateway-service: + image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_APIMLID: central-apiml + APIML_SERVICE_HOSTNAME: central-gateway-service + APIML_SERVICE_EXTERNALURL: https://central-gateway-service:10010 + APIML_GATEWAY_REGISTRY_ENABLED: true + APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER + APIML_SERVICE_DISCOVERYSERVICEURLS: https://central-gateway-service:10011/eureka + APIML_DISCOVERY_ALLPEERSURLS: https://central-gateway-service:10011/eureka + logbackService: ZWEAGW2 + APIML_SECURITY_X509_ENABLED: true + APIML_HEALTH_PROTECTED: false + APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true + APIML_SECURITY_X509_CERTIFICATESURL: https://central-gateway-service:10010/gateway/certificates + + discoverable-client: + image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - uses: ./.github/actions/setup + + - name: Run Startup Check + if: always() + timeout-minutes: 4 + run: > + ./gradlew runStartUpCheck --info -Denvironment.config=-docker-modulith -Ddiscoverableclient.instances=0 -Denvironment.modulith=true -Dgateway.instances=2 -Dgateway.host=central-gateway-service + + - name: Run CI Tests + timeout-minutes: 4 + run: > + ./gradlew :integration-tests:runGatewayCentralRegistryTest --info + -Denvironment.config=-docker-modulith -Denvironment.modulith=true + -Ddiscovery.additionalHost=central-gateway-service + -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Store results + uses: actions/upload-artifact@v4 + if: always() + with: + name: ModulithCentralRegistry-${{ env.JOB_ID }} + path: | + integration-tests/build/reports/** + + - uses: ./.github/actions/teardown + CITestsRegistration: needs: PublishJibContainers runs-on: ubuntu-latest @@ -1763,6 +1756,9 @@ jobs: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_HEALTH_PROTECTED: false + caching-service: + image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} @@ -2014,7 +2010,7 @@ jobs: PublishResults: needs: [ - CITests,CITestsWithInfinispan,CITestsZaas,GatewayProxy,GatewayServiceRouting,CITestsHA, + CITests,CITestsWithInfinispan,CITestsZaas,GatewayProxy,CITestsHA, CITestsModulith,CITestsModulithHA ] runs-on: ubuntu-latest @@ -2052,14 +2048,10 @@ jobs: with: name: CITestsHA-${{ env.JOB_ID }}-websocket-chaotic path: citestschaoticha - - uses: actions/download-artifact@v4 - with: - name: GatewayServiceRouting-${{ env.JOB_ID }} - path: GatewayServiceRouting - name: Code coverage and publish results run: > - ./gradlew --scan coverage sonar -Dresults="containercitests/results,containercitestsmodulith/results,citestswithinfinispan/results,GatewayProxy/results,citestschaoticha/results,GatewayServiceRouting/results,ContainerCITestsZaas/results" + ./gradlew --scan coverage sonar -Dresults="containercitests/results,containercitestsmodulith/results,citestswithinfinispan/results,GatewayProxy/results,citestschaoticha/results,ContainerCITestsZaas/results" -Psonar.host.url=$SONAR_HOST_URL -Dsonar.token=$SONAR_TOKEN -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index c025b36394..c7b0653ec1 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -9,3 +9,4 @@ REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 REACT_APP_ZOWE_BUILD_INFO=3.2.25-SNAPSHOT + diff --git a/api-catalog-ui/frontend/.env.production b/api-catalog-ui/frontend/.env.production index 9c0c96aa70..e323b2e059 100644 --- a/api-catalog-ui/frontend/.env.production +++ b/api-catalog-ui/frontend/.env.production @@ -4,3 +4,4 @@ REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 REACT_APP_GATEWAY_URL= REACT_APP_CA_ENV=false +GENERATE_SOURCEMAP=false diff --git a/api-catalog-ui/frontend/cypress.modulith.config.js b/api-catalog-ui/frontend/cypress.modulith.config.js index 24ac4fd172..4503d3a33f 100644 --- a/api-catalog-ui/frontend/cypress.modulith.config.js +++ b/api-catalog-ui/frontend/cypress.modulith.config.js @@ -34,7 +34,6 @@ module.exports = defineConfig({ setupNodeEvents(on, config) { // eslint-disable-next-line global-require return require('./cypress/plugins/index.js')(on, config); - }, - excludeSpecPattern: ['cypress/e2e/detail-page/multiple-gateway-services.cy.js'], + } }, }); diff --git a/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js b/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js index 8ff652ec02..f767db6a6d 100644 --- a/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/dashboard/dashboard.cy.js @@ -10,7 +10,6 @@ /* eslint-disable no-undef */ /// -const isModulith = Cypress.env('modulith'); describe('>>> Dashboard test', () => { it('dashboard test', () => { @@ -48,12 +47,9 @@ describe('>>> Dashboard test', () => { cy.get('#search > div > div > input').as('search').type('API Gateway'); - let expectedGatewaysCount = 2; - if (isModulith) { - expectedGatewaysCount = 1; - } + const expectedGatewaysCount = 2; - cy.get('.grid-tile').should('have.length', expectedGatewaysCount); // FIXME modulith does not support multitenancy yet + cy.get('.grid-tile').should('have.length', expectedGatewaysCount); cy.get('.clear-text-search').click(); diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js index 1d80758b91..3ff2d25220 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/detail-page.cy.js @@ -11,8 +11,6 @@ /// -const isModulith = Cypress.env('modulith'); - describe('>>> Detail page test', () => { it('Detail page test', () => { cy.login(Cypress.env('username'), Cypress.env('password')); @@ -118,10 +116,7 @@ describe('>>> Detail page test', () => { cy.get('#search > div > div > input').as('search').type('API Gateway'); - let expectedGatewaysCount = 2; - if (isModulith) { - expectedGatewaysCount = 1; - } + const expectedGatewaysCount = 2; cy.get('.grid-tile').should('have.length', expectedGatewaysCount).should('contain', 'API Gateway'); // FIXME in modulith multi tenancy is not working }); diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index 8ed9cfb62f..eb6702362d 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -12,8 +12,6 @@ /// -const isModulith = Cypress.env('modulith'); - // api-diff-form is now a floating window. const PATH_TO_VERSION_SELECTORS = '.api-diff-form > div:nth-child(2) > div > div'; const PATH_TO_VERSION_SELECTORS2 = '.api-diff-form > div:nth-child(4) > div > div'; @@ -40,8 +38,7 @@ describe('>>> Service version compare Test', () => { 'exist' ); - // FIXME modulith mode does not support multi tenancy yet - let expectedServicesCount = 16; + const expectedServicesCount = 17; cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div .find('a.MuiTab-root') // Find all the anchor elements within the div diff --git a/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js b/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js index 749bbbfa91..d0b421fc14 100644 --- a/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/login/login-ok.cy.js @@ -27,7 +27,7 @@ describe('>>> Login ok page test', () => { cy.get('li[data-testid="logout"]').click(); cy.contains('API Catalog'); - cy.contains('Version: '); + cy.contains('Please enter your mainframe username and password'); cy.getCookie('apimlAuthenticationToken').should('not.exist'); }); diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 63b376a687..83592e9fed 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -62,8 +62,8 @@ "postbuild": "rimraf --glob build/**/*.map", "test": "react-app-rewired test --runInBand --silent --watchAll=false --env=jsdom components/* utils/* reducers/* epics/* actions/* selectors/* ErrorBoundary/* helpers/* --reporters=default --reporters=jest-html-reporter --coverage", "cy:open": "cypress open", - "cy:e2e:ci": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --env moudlith=false,catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/apicatalog/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", - "cy:e2e:ci:modulith": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --config-file cypress.modulith.config.js --env modulith=true,catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/gateway/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", + "cy:e2e:ci": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --env catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/apicatalog/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", + "cy:e2e:ci:modulith": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --config-file cypress.modulith.config.js --env catalogHomePage=https://gateway-service:10010/apicatalog/ui/v1,loginUrl=https://gateway-service:10010/gateway/api/v1/auth/login,gatewayOktaRedirect=https://gateway-service:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Fgateway-service%3A10010%2Fapplication%2Fversion --browser chrome --headless", "cy:e2e:localhost": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headless", "cy:e2e:localhost-debug": "DEBUG=cypress:net* cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headless", "cy:e2e:localhost-headful": "cypress run --spec \"cypress/e2e/**/*.cy.js\" --browser chrome --headed", diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index 67b3ef77f9..b0efc14082 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -19,13 +19,7 @@ import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; import jakarta.annotation.PostConstruct; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; +import jakarta.servlet.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.Context; @@ -56,30 +50,31 @@ import org.zowe.apiml.apicatalog.ApiCatalogServiceAvailableEvent; import org.zowe.apiml.config.ApplicationInfo; import org.zowe.apiml.discovery.ApimlInstanceRegistry; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.filter.PreFluxFilter; +import org.zowe.apiml.gateway.services.ServicesInfoService; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.services.BasicInfoService; +import org.zowe.apiml.services.ServiceInfo; +import org.zowe.apiml.zaas.security.login.Providers; import org.zowe.apiml.zaas.security.service.JwtSecurity; import reactor.core.publisher.Flux; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Timer; -import java.util.TimerTask; +import java.util.*; + +import static org.zowe.apiml.services.ServiceInfoUtils.getInstances; +import static org.zowe.apiml.services.ServiceInfoUtils.getStatus; @EnableScheduling @Configuration @RequiredArgsConstructor @EnableConfigurationProperties -@DependsOn(value = { "gatewayHealthIndicator" }) +@DependsOn(value = {"gatewayHealthIndicator"}) @Slf4j public class ModulithConfig { @@ -114,12 +109,12 @@ ApplicationInfo applicationInfo() { private InstanceInfo getInstanceInfo(String serviceId) { var leaseInfo = LeaseInfo.Builder.newBuilder() - .setDurationInSecs(Integer.MAX_VALUE) - .setRegistrationTimestamp(System.currentTimeMillis()) - .setRenewalTimestamp(System.currentTimeMillis()) - .setRenewalIntervalInSecs(Integer.MAX_VALUE) - .setServiceUpTimestamp(System.currentTimeMillis()) - .build(); + .setDurationInSecs(Integer.MAX_VALUE) + .setRegistrationTimestamp(System.currentTimeMillis()) + .setRenewalTimestamp(System.currentTimeMillis()) + .setRenewalIntervalInSecs(Integer.MAX_VALUE) + .setServiceUpTimestamp(System.currentTimeMillis()) + .build(); var scheme = https ? "https" : "http"; @@ -133,31 +128,31 @@ private InstanceInfo getInstanceInfo(String serviceId) { String homePagePath = metadata.getOrDefault("apiml.homePagePath", "/"); return InstanceInfo.Builder.newBuilder() - .setInstanceId(String.format("%s:%s:%d", hostname, serviceId, port)) - .setAppName(serviceId) - .setHostName(hostname) - .setHomePageUrl(null, String.format("%s://%s:%d%s", scheme, hostname, port, homePagePath)) - .setStatus(InstanceInfo.InstanceStatus.UP) - .setIPAddr(ipAddress) - .setPort(port) - .setSecurePort(port) - .enablePort(InstanceInfo.PortType.SECURE, https) - .enablePort(InstanceInfo.PortType.UNSECURE, !https) - .setVIPAddress(serviceId) - .setDataCenterInfo(() -> DataCenterInfo.Name.MyOwn) - .setLeaseInfo(leaseInfo) - .setLastUpdatedTimestamp(System.currentTimeMillis()) - .setMetadata(metadata) - .setVIPAddress(serviceId) - .build(); + .setInstanceId(String.format("%s:%s:%d", hostname, serviceId, port)) + .setAppName(serviceId) + .setHostName(hostname) + .setHomePageUrl(null, String.format("%s://%s:%d%s", scheme, hostname, port, homePagePath)) + .setStatus(InstanceInfo.InstanceStatus.UP) + .setIPAddr(ipAddress) + .setPort(port) + .setSecurePort(port) + .enablePort(InstanceInfo.PortType.SECURE, https) + .enablePort(InstanceInfo.PortType.UNSECURE, !https) + .setVIPAddress(serviceId) + .setDataCenterInfo(() -> DataCenterInfo.Name.MyOwn) + .setLeaseInfo(leaseInfo) + .setLastUpdatedTimestamp(System.currentTimeMillis()) + .setMetadata(metadata) + .setVIPAddress(serviceId) + .build(); } static ApimlInstanceRegistry getRegistry() { return Optional.ofNullable(EurekaServerContextHolder.getInstance()) - .map(EurekaServerContextHolder::getServerContext) - .map(EurekaServerContext::getRegistry) - .map(ApimlInstanceRegistry.class::cast) - .orElse(null); + .map(EurekaServerContextHolder::getServerContext) + .map(EurekaServerContext::getRegistry) + .map(ApimlInstanceRegistry.class::cast) + .orElse(null); } @PostConstruct @@ -197,7 +192,8 @@ public void run() { @Scheduled(initialDelay = 3000, fixedRate = 20_000) // TODO find better solution but DON'T JUST REMOVE! public void periodicJwtInit() { var jwtSec = applicationContext.getBean(JwtSecurity.class); - if (!jwtSec.getZosmfListener().isZosmfReady()) { + var providers = applicationContext.getBean(Providers.class); + if (providers.isZosfmUsed() && !jwtSec.getZosmfListener().isZosmfReady()) { jwtSec.getZosmfListener().getZosmfRegisteredListener().onEvent(new CacheRefreshedEvent()); } } @@ -242,12 +238,12 @@ public List getInstances(String serviceId) { return Collections.emptyList(); } return Optional.ofNullable(registry.getApplication(StringUtils.upperCase(serviceId))) - .map(Application::getInstances) - .orElse(Collections.emptyList()) - .stream() - .map(EurekaServiceInstance::new) - .map(ServiceInstance.class::cast) - .toList(); + .map(Application::getInstances) + .orElse(Collections.emptyList()) + .stream() + .map(EurekaServiceInstance::new) + .map(ServiceInstance.class::cast) + .toList(); } @Override @@ -257,10 +253,10 @@ public List getServices() { return Collections.emptyList(); } return registry.getApplications().getRegisteredApplications() - .stream() - .map(Application::getName) - .distinct() - .toList(); + .stream() + .map(Application::getName) + .distinct() + .toList(); } }; } @@ -283,12 +279,34 @@ MessageService messageService() { return messageService; } + @Bean + public BasicInfoService basicInfoService(DiscoveryClient discoveryClient, EurekaMetadataParser eurekaMetadataParser) { + + return new BasicInfoService(null, eurekaMetadataParser) { + @Override + public List getServicesInfo() { + var serviceInfos = new ArrayList(); + for (var serviceId : discoveryClient.getServices()) { + var instances = discoveryClient.getInstances(serviceId); + var instanceInfos = ServicesInfoService.extractInstanceInfo(instances); + serviceInfos.add(ServiceInfo.builder() + .serviceId(serviceId) + .status(getStatus(instanceInfos)) + .apiml(getApiml(instanceInfos)) + .instances(getInstances(instanceInfos)) + .build()); + } + return serviceInfos; + } + }; + } + @Bean @Primary TomcatReactiveWebServerFactory tomcatReactiveWebServerWithFiltersFactory( - HttpHandler httpHandler, - List preFluxFilters, - List servletContextAwareListeners) { + HttpHandler httpHandler, + List preFluxFilters, + List servletContextAwareListeners) { return new TomcatReactiveWebServerFactory() { @Override @@ -315,13 +333,13 @@ protected void configureContext(Context context) { */ @Bean WebServerFactoryCustomizer internalPortCustomizer( - @Value("${apiml.internal-discovery.port:10011}") int internalDiscoveryPort) { + @Value("${apiml.internal-discovery.port:10011}") int internalDiscoveryPort) { return factory -> { var connector = new Connector(); try { Method method = TomcatReactiveWebServerFactory.class.getDeclaredMethod("customizeConnector", - Connector.class); + Connector.class); method.setAccessible(true); method.invoke(factory, connector); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e) { @@ -341,7 +359,7 @@ static class ServletWithFilters extends TomcatHttpHandlerAdapter { private final FilterChain filterChain; public ServletWithFilters(HttpHandler httpHandler, TomcatHttpHandlerAdapter servlet, - Collection filters) { + Collection filters) { super(httpHandler); this.servlet = servlet; diff --git a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java index 47e0099583..ee8511891d 100644 --- a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java @@ -550,7 +550,6 @@ SecurityWebFilterChain gatewayAuthenticatedEndpoints(ServerHttpSecurity http, Au .anyExchange().authenticated() ) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) - .addFilterAfter(new CategorizeCertsWebFilter(publicKeyCertificatesBase64, certificateValidator), SecurityWebFiltersOrder.FIRST) .addFilterAfter(new TokenAuthFilter(localTokenProvider, authConfigurationProperties, authExceptionHandlerReactive), SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAfter(new BasicLoginFilter(compoundAuthProvider, failedAuthenticationWebHandler), SecurityWebFiltersOrder.AUTHENTICATION) .build(); diff --git a/apiml/src/test/java/org/zowe/apiml/ModulithConfigTest.java b/apiml/src/test/java/org/zowe/apiml/ModulithConfigTest.java new file mode 100644 index 0000000000..6acb695082 --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/ModulithConfigTest.java @@ -0,0 +1,79 @@ +/* + * 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; + +import com.netflix.appinfo.InstanceInfo; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.zowe.apiml.auth.Authentication; +import org.zowe.apiml.auth.AuthenticationScheme; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; +import org.zowe.apiml.services.ServiceInfo; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ModulithConfigTest { + + private static final String CLIENT_SERVICE_ID = "discoverableclient"; + + @Test + void givenServiceExistInDiscoveryClient_thenReturnServiceInfo() { + + var discoveryClient = mock(DiscoveryClient.class); + + when(discoveryClient.getServices()) + .thenReturn(Arrays.asList(CLIENT_SERVICE_ID)); + var ii = new InstanceInfo(CLIENT_SERVICE_ID, null, null, "192.168.0.1", null, new InstanceInfo.PortWrapper(true, 9090), + null, null, null, null, null, null, null, 0, null, "hostname", InstanceInfo.InstanceStatus.UP, null, null, null, null, null, + null, null, null, null); + var serviceInstance = new EurekaServiceInstance(ii); + when(discoveryClient.getInstances(CLIENT_SERVICE_ID)) + .thenReturn(Arrays.asList(serviceInstance)); + ModulithConfig mc = new ModulithConfig(null, null, null, null, null, null); + var eurekaParser = mock(EurekaMetadataParser.class); + when(eurekaParser.parseAuthentication(any())).thenReturn(new Authentication(AuthenticationScheme.ZOWE_JWT, "appl")); + var basicInfoService = mc.basicInfoService(discoveryClient, eurekaParser); + List servicesInfo = basicInfoService.getServicesInfo(); + + + assertEquals(1, servicesInfo.size()); + assertThat(servicesInfo, contains( + hasProperty("serviceId", is(CLIENT_SERVICE_ID)) + )); + } + + @Test + void givenNoServiceFoundInDiscoveryClient_thenReturnEmptyList() { + + var discoveryClient = mock(DiscoveryClient.class); + + when(discoveryClient.getServices()) + .thenReturn(Collections.emptyList()); + + ModulithConfig mc = new ModulithConfig(null, null, null, null, null, null); + var eurekaParser = mock(EurekaMetadataParser.class); + var basicInfoService = mc.basicInfoService(discoveryClient, eurekaParser); + List servicesInfo = basicInfoService.getServicesInfo(); + assertThat(servicesInfo, emptyIterable()); + } + +} diff --git a/build.gradle b/build.gradle index d9daf46254..fbceaf60ff 100644 --- a/build.gradle +++ b/build.gradle @@ -207,16 +207,6 @@ task runGatewayProxyTest(dependsOn: [":integration-tests:runGatewayProxyTest"]) group "Integration tests" } -task runGatewayServiceRoutingTest(dependsOn: [":integration-tests:runGatewayServiceRoutingTest"]) { - description "Run tests verifying gateway can locate service and translate auth scheme" - group "Integration tests" -} - -task runOidcTests(dependsOn: [":integration-tests:runOidcTests"]) { - description "Run tests verifying integration with oidc provider(okta)" - group "Integration tests" -} - task runIdPrefixReplacerTests(dependsOn: [":integration-tests:runIdPrefixReplacerTests"]) { description "Run Integration Test verifying the service ID prefix replacer mechanism" group "Integration tests" diff --git a/caching-service/src/main/resources/infinispan.xml b/caching-service/src/main/resources/infinispan.xml index 2fc1b34026..94c8b6e13b 100644 --- a/caching-service/src/main/resources/infinispan.xml +++ b/caching-service/src/main/resources/infinispan.xml @@ -90,6 +90,7 @@ org.zowe.apiml.security.common.token.TokenAuthentication$Type java.util.HashMap java.util.Arrays$ArrayList + java.security.cert.Certificate$CertificateRep diff --git a/common-service-core/src/main/java/org/zowe/apiml/services/BasicInfoService.java b/common-service-core/src/main/java/org/zowe/apiml/services/BasicInfoService.java index 397c27e418..b5089ac331 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/services/BasicInfoService.java +++ b/common-service-core/src/main/java/org/zowe/apiml/services/BasicInfoService.java @@ -72,7 +72,7 @@ private ServiceInfo getServiceInfo(Application application) { * - getApiInfos * - getApiInfos */ - private ServiceInfo.Apiml getApiml(List appInstances) { + public ServiceInfo.Apiml getApiml(List appInstances) { return ServiceInfo.Apiml.builder() .apiInfo(getApiInfos(appInstances)) .service(getService(appInstances)) diff --git a/config/local/apiml-service-2.yml b/config/local/apiml-service-2.yml index 51114d5aba..cc4b5cf480 100644 --- a/config/local/apiml-service-2.yml +++ b/config/local/apiml-service-2.yml @@ -25,6 +25,8 @@ apiml: health: protected: false gateway: + registry: + enabled: true servicesToLimitRequestRate: discoverableclient cookieNameForRateLimiter: apimlAuthenticationToken service: diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index 14e75fab45..d233fa92ad 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -216,6 +216,8 @@ private CloudEurekaClient registerInTheApimlInstance(EurekaClientConfig config, EurekaClientConfigBean configBean = new EurekaClientConfigBean(); BeanUtils.copyProperties(config, configBean); configBean.setServiceUrl(urls); + configBean.setRegisterWithEureka(true); + configBean.setFetchRegistry(true); EurekaInstanceConfig eurekaInstanceConfig = appManager.getEurekaInstanceConfig(); InstanceInfo newInfo = create(eurekaInstanceConfig); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java index 64d8498568..f18131f95b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java @@ -12,6 +12,7 @@ import com.netflix.discovery.EurekaClient; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; @@ -25,6 +26,7 @@ public class RegistryConfig { @Bean + @ConditionalOnMissingBean public BasicInfoService basicInfoService(EurekaClient eurekaClient, EurekaMetadataParser eurekaMetadataParser) { return new BasicInfoService(eurekaClient, eurekaMetadataParser); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java index 627ac59183..85c9adfc6a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java @@ -143,7 +143,7 @@ private ServiceInfo.Apiml getApiml(List serviceInstances) { .build(); } - private List extractInstanceInfo(List serviceInstances) { + public static List extractInstanceInfo(List serviceInstances) { return serviceInstances.stream() .filter(EurekaServiceInstance.class::isInstance) .map(EurekaServiceInstance.class::cast) diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index d56d1cb155..6483d48bc2 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -449,25 +449,13 @@ task runGatewayProxyTest(type: Test) { systemProperties System.getProperties() useJUnitPlatform { includeTags( - 'GatewayProxyTest' - ) - } -} - -task runGatewayServiceRoutingTest(type: Test) { - group "integration tests" - description "Run tests verifying central gateway can locate service and translate auth scheme" - - outputs.cacheIf { false } - - systemProperties System.getProperties() - useJUnitPlatform { - includeTags( + 'GatewayProxyTest', 'GatewayServiceRouting' ) } } + task runGatewayCentralRegistryTest(type: Test) { group "integration tests" description "Run tests verifying central gateway central registry endpoint" @@ -482,20 +470,6 @@ task runGatewayCentralRegistryTest(type: Test) { } } -task runOidcTests(type: Test) { - group "integration tests" - description "Run tests verifying integration with oauth2 provider(okta)" - - outputs.cacheIf { false } - - systemProperties System.getProperties() - useJUnitPlatform { - includeTags( - 'OktaOauth2Test' - ) - } -} - task runIdPrefixReplacerTests(type: Test) { group "integration tests" description "Run Integration Test verifying the service ID prefix replacer mechanism" @@ -536,7 +510,8 @@ task runZaasTest(type: Test) { systemProperties System.getProperties() useJUnitPlatform { includeTags( - 'ZaasTest' + 'ZaasTest', + 'OktaOauth2Test' ) } } diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java index d05757f40a..544f754b61 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java @@ -158,7 +158,7 @@ private ValidatableResponse listCentralRegistry(String apimlId, String apiId, St @SneakyThrows private ValidatableResponse listEurekaApps() { - URI eurekaApps = new URL(discoveryConf.getScheme(), discoveryConf.getHost(), discoveryConf.getPort(), "/eureka/apps") + URI eurekaApps = new URL(discoveryConf.getScheme(), discoveryConf.getAdditionalHost(), discoveryConf.getPort(), "/eureka/apps") .toURI(); return with().given() diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java index 8862542b96..29b22ffeed 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/SafLoginTest.java @@ -89,6 +89,7 @@ void givenExpiredAccountCredentialsInBody(URI loginUrl) { .when() .post(loginUrl) .then() + .log().ifValidationFails() .statusCode(is(SC_UNAUTHORIZED)) .body( "messages.find { it.messageNumber == 'ZWEAT412E' }.messageContent", containsString("expire") diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zos/ServicesInfoTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zos/ServicesInfoTest.java index 3659c15fc4..f6a2b147fe 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zos/ServicesInfoTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zos/ServicesInfoTest.java @@ -11,16 +11,12 @@ package org.zowe.apiml.integration.zos; import io.restassured.RestAssured; +import io.restassured.config.RestAssuredConfig; import org.apache.http.message.BasicNameValuePair; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; import org.zowe.apiml.util.SecurityUtils; import org.zowe.apiml.util.TestWithStartedInstances; import org.zowe.apiml.util.categories.GeneralAuthenticationTest; @@ -32,12 +28,8 @@ import java.util.stream.Stream; 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.apache.http.HttpStatus.SC_UNAUTHORIZED; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.apache.http.HttpStatus.*; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.core.Is.is; import static org.zowe.apiml.util.SecurityUtils.GATEWAY_TOKEN_COOKIE_NAME; import static org.zowe.apiml.util.http.HttpRequestUtils.getUriFromGateway; @@ -104,32 +96,27 @@ void givenNoAuthentication(String endpoint) { @Nested class GivenClientCertificateCallDirectlyTowardsGateway { - @ParameterizedTest(name = "givenClientCertificate_returns200WithoutSafCheck {index} {0} ") - @ValueSource(strings = { - ROUTED_SERVICE_NOT_VERSIONED, - ROUTED_SERVICE_NOT_VERSIONED + "/" + API_CATALOG_SERVICE_ID - }) - void returns200WithoutSafCheck(String endpoint) { - given() - .config(SslContext.clientCertValid) - .when() - .get(getUriFromGateway(endpoint)) - .then() - .statusCode(is(SC_OK)); + + static Stream endpointCertPairs() { + return Stream.of( + Arguments.of(ROUTED_SERVICE_NOT_VERSIONED, "clientCertValid", SslContext.clientCertValid, SC_OK), + Arguments.of(ROUTED_SERVICE_NOT_VERSIONED, "clientCertApiml", SslContext.clientCertApiml, SC_OK), + Arguments.of(ROUTED_SERVICE_NOT_VERSIONED, "selfSignedUntrusted", SslContext.selfSignedUntrusted, SC_UNAUTHORIZED), + Arguments.of(ROUTED_SERVICE_NOT_VERSIONED + "/" + API_CATALOG_SERVICE_ID, "clientCertValid", SslContext.clientCertValid, SC_OK), + Arguments.of(ROUTED_SERVICE_NOT_VERSIONED + "/" + API_CATALOG_SERVICE_ID, "clientCertApiml", SslContext.clientCertApiml, SC_OK), + Arguments.of(ROUTED_SERVICE_NOT_VERSIONED + "/" + API_CATALOG_SERVICE_ID, "selfSignedUntrusted", SslContext.selfSignedUntrusted, SC_UNAUTHORIZED) + ); } - @ParameterizedTest(name = "givenClientCertificate_returns401WithUntrustedCert {index} {0} ") - @ValueSource(strings = { - ROUTED_SERVICE_NOT_VERSIONED, - ROUTED_SERVICE_NOT_VERSIONED + "/" + API_CATALOG_SERVICE_ID - }) - void returns401WithUntrustedCert(String endpoint) { + @ParameterizedTest(name = "given {1} when request {0} return {3} index:{index} returns200WithoutSafCheck") + @MethodSource("endpointCertPairs") + void givenEndpointAccessWithClientCertificateOnly(String endpoint, String certName, RestAssuredConfig config, int statusCode) { given() - .config(SslContext.selfSignedUntrusted) - .when() + .config(config) + .when() .get(getUriFromGateway(endpoint)) - .then() - .statusCode(is(SC_UNAUTHORIZED)); + .then() + .statusCode(is(statusCode)); } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/login/Providers.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/login/Providers.java index 644ddabfe5..679edd5a77 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/login/Providers.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/login/Providers.java @@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.security.authentication.AuthenticationServiceException; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.zaas.security.config.CompoundAuthProvider; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import org.zowe.apiml.message.log.ApimlLogger; @@ -34,7 +35,7 @@ public class Providers { private final ZosmfService zosmfService; @InjectApimlLogger - private ApimlLogger apimlLog = ApimlLogger.empty(); + private ApimlLogger apimlLog = ApimlLogger.of(Providers.class, YamlMessageServiceInstance.getInstance()); /** * This method decides whether the Zosmf service is available. From 7d9c1ca7e8fa6f016e11cd9bb8a548ac12ab18db Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 1 Aug 2025 00:50:15 +0000 Subject: [PATCH 042/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.2.25'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 26d0144d8e..087fa47f52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.25-SNAPSHOT +version=3.2.25 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From f7374e19092610fa6a2297d0c535133cdfbb23e6 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 1 Aug 2025 00:50:16 +0000 Subject: [PATCH 043/152] [Gradle Release plugin] Create new version: 'v3.2.26-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 087fa47f52..e29c418a0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.25 +version=3.2.26-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 83bceae2300cc38502d6b28d80a877f4e0573977 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 1 Aug 2025 00:50:18 +0000 Subject: [PATCH 044/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index c7b0653ec1..076efc6c0a 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.2.25-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.2.26-SNAPSHOT From 5dcff448537d70c07b7c8610b5482212e60c9cd7 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 4 Aug 2025 10:30:38 +0200 Subject: [PATCH 045/152] chore: add modulith on z/os integration test tasks (#4253) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- integration-tests/build.gradle | 93 ++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 6483d48bc2..751c37d6a3 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -258,6 +258,99 @@ task runAllIntegrationTestsForZoweHaTestingOnZos(type: Test) { outputs.cacheIf { false } } +task runAllIntegrationTestsForZoweModulithNonHaTestingOnZos(type: Test) { + // This task is intended to run on z/OS systems with some limitations: + // Only 1 Gateway (Non-HA mode) + // z/OSMF Authentication provider only + // No support for SAF ID Tokens + + group "Integration tests" + description "Run all integration tests for Zowe Non-HA testing on z/OS with modulith mode (limited)" + + def targetSystem = System.getenv("ZOS_TARGET_SYS") ? "-" + System.getenv("ZOS_TARGET_SYS") : "" + systemProperty "environment.config", targetSystem + systemProperty "environment.zos.target", "true" + systemProperty "environment.modulith", "true" + systemProperties System.properties + systemProperties.remove('java.endorsed.dirs') + + useJUnitPlatform { + excludeTags( + 'StartupCheck', + 'EnvironmentCheck', + 'AdditionalLocalTest', + 'TestsNotMeantForZowe', + 'DiscoverableClientDependentTest', + 'HATest', + 'ChaoticHATest', + 'OktaOauth2Test', + 'MultipleRegistrationsTest', + 'NotForMainframeTest', + 'ApiCatalogStandaloneTest', + 'SAFProviderTest', + 'GatewayProxyTest', + 'GatewayCentralRegistry', + 'SafIdTokenTest', + 'ZaasTest' // These tests require ZAAS as a separate service with its own port and specific paths that are not part of the public API + ) + } + + debugOptions { + port = 5005 + suspend = true + server = true + } + outputs.upToDateWhen { false } +} + +task runAllIntegrationTestsForZoweModulithHaTestingOnZos(type: Test) { + // This task is intended to run on z/OS systems with some limitations: + // More than 1 Gateway (HA mode) + // z/OSMF Authentication provider only + // No support for SAF ID Tokens + + group "Integration tests" + description "Run all integration tests for Zowe HA testing on z/OS with modulith mode (limited)" + + def targetSystem = System.getenv("ZOS_TARGET_SYS") ? "-" + System.getenv("ZOS_TARGET_SYS") : "" + systemProperty "environment.config", targetSystem + systemProperty "environment.zos.target", "true" + systemProperty "environment.ha", true + systemProperty "environment.modulith", true + systemProperties System.properties + systemProperties.remove('java.endorsed.dirs') + + useJUnitPlatform { + excludeTags( + 'StartupCheck', + 'EnvironmentCheck', + 'AdditionalLocalTest', + 'TestsNotMeantForZowe', + 'DiscoverableClientDependentTest', + 'OktaOauth2Test', + 'MultipleRegistrationsTest', + 'NotForMainframeTest', + 'ApiCatalogStandaloneTest', + 'SAFProviderTest', + 'GatewayProxyTest', + 'GatewayCentralRegistry', + 'SafIdTokenTest', + 'ChaoticHATest', + 'GraphQLTest', + 'ZaasTest' // These tests require ZAAS as a separate service with its own port and specific paths that are not part of the public API + ) + } + + debugOptions { + port = 5005 + suspend = true + server = true + } + + outputs.upToDateWhen { false } + outputs.cacheIf { false } +} + task runAllIntegrationTestsForZoweTesting(type: Test) { group "Integration tests" description "Run all integration tests for Zowe testing" From db84b2ffaa4550ca5edfd4ba102277a33219f30b Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 5 Aug 2025 12:50:23 +0200 Subject: [PATCH 046/152] fix: update max request header size (#4257) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- apiml/src/main/resources/application.yml | 1 + apiml/src/test/resources/application.yml | 1 + .../src/main/resources/application.yml | 1 + .../gateway/acceptance/RequestLimitTest.java | 46 +++++++++++++++++++ .../src/test/resources/application.yml | 1 + 5 files changed, 50 insertions(+) create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RequestLimitTest.java diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 028dda004d..ed713c64f1 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -257,6 +257,7 @@ server: trustStore: keystore/localhost/localhost.truststore.p12 trustStorePassword: password trustStoreType: PKCS12 + max-http-request-header-size: 48000 caching: storage: diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml index d4b4698263..fd32179e63 100644 --- a/apiml/src/test/resources/application.yml +++ b/apiml/src/test/resources/application.yml @@ -124,6 +124,7 @@ server: trustStore: ../keystore/localhost/localhost.truststore.p12 trustStorePassword: password trustStoreType: PKCS12 + max-http-request-header-size: 48000 spring: application: diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 5e07950224..11f0387f6b 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -131,6 +131,7 @@ server: trustStore: keystore/localhost/localhost.truststore.p12 trustStorePassword: password trustStoreType: PKCS12 + max-http-request-header-size: 48000 logbackServiceName: ZWEAGW1 diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RequestLimitTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RequestLimitTest.java new file mode 100644 index 0000000000..5dd042aa3b --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RequestLimitTest.java @@ -0,0 +1,46 @@ +/* + * 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.acceptance; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithBasePath; +import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; + +import static io.restassured.RestAssured.given; +import static org.apache.hc.core5.http.HttpStatus.SC_OK; +import static org.hamcrest.Matchers.is; + +/** + * In z/OS tokens can be large if generated by z/OSMF and the user has many groups + */ +@MicroservicesAcceptanceTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class RequestLimitTest extends AcceptanceTestWithBasePath { + + private static final String HEADER_10KB = StringUtils.repeat("1", 10_000); + + @Test + void whenLargeRequestHeader_then200() { + given() + .header("X-BIG-HEADER-1", HEADER_10KB) + .header("x-BIG-HEADER-2", HEADER_10KB) + .header("x-BIG-HEADER-3", HEADER_10KB) + .header("x-BIG-HEADER-4", HEADER_10KB) + .when() + .get(basePath + "/application/version") + .then() + .statusCode(is(SC_OK)); + + } + +} diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml index f4adab4ed0..b5ea677787 100644 --- a/gateway-service/src/test/resources/application.yml +++ b/gateway-service/src/test/resources/application.yml @@ -34,6 +34,7 @@ server: clientKeyStore: ../keystore/client_cert/client-certs.p12 clientCN: apimtst # case-sensitive clientKeyStorePassword: password + max-http-request-header-size: 48000 #For testing forwarded headers with (un)trusted proxies test: From bad8221b88931da7b8f7432d10958557651ed238 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:12:28 +0200 Subject: [PATCH 047/152] chore: Update all non-major dependencies (v3.x.x) (#4248) Signed-off-by: Renovate Bot Signed-off-by: ac892247 Co-authored-by: Renovate Bot Co-authored-by: ac892247 Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 116 +++-- api-catalog-ui/frontend/package.json | 11 +- gradle/versions.gradle | 10 +- .../package-lock.json | 448 +++++++++--------- zowe-cli-id-federation-plugin/package.json | 18 +- 5 files changed, 303 insertions(+), 300 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index 93052b78d9..d73262238f 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -41,7 +41,7 @@ "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", "react-error-boundary": "5.0.0", - "react-hook-form": "7.61.1", + "react-hook-form": "7.62.0", "react-redux": "9.2.0", "react-router": "7.6.3", "react-toastify": "10.0.6", @@ -55,7 +55,7 @@ "rxjs": "7.8.2", "sass": "1.89.2", "stream": "0.0.3", - "swagger-ui-react": "5.27.0", + "swagger-ui-react": "5.27.1", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -77,7 +77,7 @@ "ajv": "8.17.1", "ansi-regex": "6.1.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001727", + "caniuse-lite": "1.0.30001731", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", @@ -106,7 +106,7 @@ "jest-mock": "29.7.0", "jest-watch-typeahead": "2.2.2", "json-schema": "0.4.0", - "mini-css-extract-plugin": "2.9.2", + "mini-css-extract-plugin": "2.9.3", "nodemon": "3.1.10", "nth-check": "2.1.1", "prettier": "3.6.2", @@ -118,7 +118,7 @@ "redux-mock-store": "1.5.5", "rimraf": "6.0.1", "source-map-explorer": "2.5.3", - "start-server-and-test": "2.0.12", + "start-server-and-test": "2.0.13", "tmpl": "1.0.5", "undici": "6.19.8", "yaml": "2.8.0" @@ -2680,6 +2680,23 @@ "node": ">= 6" } }, + "node_modules/@cypress/request/node_modules/form-data": { + "version": "3.0.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@cypress/request/node_modules/qs": { "version": "6.14.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/qs/-/qs-6.14.0.tgz", @@ -8506,6 +8523,22 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios/node_modules/form-data": { + "version": "3.0.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axios/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9374,9 +9407,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001731", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", @@ -13929,22 +13962,6 @@ "node": ">= 6" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/format/-/format-0.2.2.tgz", @@ -15445,14 +15462,16 @@ } }, "node_modules/httpsnippet/node_modules/form-data": { - "version": "3.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "version": "3.0.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -19243,15 +19262,16 @@ } }, "node_modules/jsdom/node_modules/form-data": { - "version": "3.0.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-3.0.3.tgz", - "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "version": "3.0.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.35" }, "engines": { @@ -20300,9 +20320,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "version": "2.9.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.3.tgz", + "integrity": "sha512-tRA0+PsS4kLVijnN1w9jUu5lkxBwUk9E8SbgEB5dBJqchE6pVYdawROG6uQtpmAri7tdCK9i7b1bULeVWqS6Ag==", "dev": true, "license": "MIT", "dependencies": { @@ -23891,9 +23911,9 @@ "license": "MIT" }, "node_modules/react-hook-form": { - "version": "7.61.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-hook-form/-/react-hook-form-7.61.1.tgz", - "integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==", + "version": "7.62.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -28587,9 +28607,9 @@ "license": "MIT" }, "node_modules/start-server-and-test": { - "version": "2.0.12", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/start-server-and-test/-/start-server-and-test-2.0.12.tgz", - "integrity": "sha512-U6QiS5qsz+DN5RfJJrkAXdooxMDnLZ+n5nR8kaX//ZH19SilF6b58Z3zM9zTfrNIkJepzauHo4RceSgvgUSX9w==", + "version": "2.0.13", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/start-server-and-test/-/start-server-and-test-2.0.13.tgz", + "integrity": "sha512-G42GCIUjBv/nDoK+QsO+nBdX2Cg3DSAKhSic2DN0GLlK4Q+63TkOeN1cV9PHZKnVOzDKGNVZGCREjpvAIAOdiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -28600,7 +28620,7 @@ "execa": "5.1.1", "lazy-ass": "1.6.0", "ps-tree": "1.2.0", - "wait-on": "8.0.3" + "wait-on": "8.0.4" }, "bin": { "server-test": "src/bin/start.js", @@ -29627,9 +29647,9 @@ } }, "node_modules/swagger-ui-react": { - "version": "5.27.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.27.0.tgz", - "integrity": "sha512-KQ1NPzRfpVICvYHmVZCmw79VJK9NYvT8+f9dTRE2ZOkZAG/hlBprCk0x1AC9ERiaPb2Wrwxuq94PkZoMM+J6fQ==", + "version": "5.27.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz", + "integrity": "sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.27.1", @@ -31145,13 +31165,13 @@ } }, "node_modules/wait-on": { - "version": "8.0.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/wait-on/-/wait-on-8.0.3.tgz", - "integrity": "sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==", + "version": "8.0.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/wait-on/-/wait-on-8.0.4.tgz", + "integrity": "sha512-8f9LugAGo4PSc0aLbpKVCVtzayd36sSCp4WLpVngkYq6PK87H79zt77/tlCU6eKCLqR46iFvcl0PU5f+DmtkwA==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.8.2", + "axios": "^1.11.0", "joi": "^17.13.3", "lodash": "^4.17.21", "minimist": "^1.2.8", diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 83592e9fed..488a27f6cd 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -37,7 +37,7 @@ "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", "react-error-boundary": "5.0.0", - "react-hook-form": "7.61.1", + "react-hook-form": "7.62.0", "react-redux": "9.2.0", "react-router": "7.6.3", "react-toastify": "10.0.6", @@ -51,7 +51,7 @@ "rxjs": "7.8.2", "sass": "1.89.2", "stream": "0.0.3", - "swagger-ui-react": "5.27.0", + "swagger-ui-react": "5.27.1", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -98,7 +98,7 @@ "ajv": "8.17.1", "ansi-regex": "6.1.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001727", + "caniuse-lite": "1.0.30001731", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", @@ -127,7 +127,7 @@ "jest-mock": "29.7.0", "jest-watch-typeahead": "2.2.2", "json-schema": "0.4.0", - "mini-css-extract-plugin": "2.9.2", + "mini-css-extract-plugin": "2.9.3", "nodemon": "3.1.10", "nth-check": "2.1.1", "prettier": "3.6.2", @@ -139,7 +139,7 @@ "redux-mock-store": "1.5.5", "rimraf": "6.0.1", "source-map-explorer": "2.5.3", - "start-server-and-test": "2.0.12", + "start-server-and-test": "2.0.13", "tmpl": "1.0.5", "undici": "6.19.8", "yaml": "2.8.0" @@ -156,6 +156,7 @@ "semver": "7.7.2", "@babel/traverse": "7.28.0", "axios": "1.11.0", + "form-data": "3.0.4", "swagger-ui-react": { "react-syntax-highlighter": { "refractor": "5.0.0" diff --git a/gradle/versions.gradle b/gradle/versions.gradle index db0f728e52..593d59d04b 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -65,7 +65,7 @@ dependencyResolutionManagement { version('jjwt', '0.12.6') version('jodaTime', '2.14.0') version('jsonPath', '2.9.0') - version('jsonSmart', '2.5.2') + version('jsonSmart', '2.6.0') version('junitJupiter', '5.13.4') version('junitPlatform', '1.13.4') version('jxpath', '1.4.0') @@ -76,7 +76,7 @@ dependencyResolutionManagement { version('netty', '4.2.3.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 version('nettyReactor', '1.2.8') - version('nimbusJoseJwt', '10.0.2') + version('nimbusJoseJwt', '10.4') version('openApiDiff', '2.1.2') version('picocli', '4.7.7') @@ -84,7 +84,7 @@ dependencyResolutionManagement { version('restAssured', '5.5.5') version('rhino', '1.8.0') version('springDoc', '2.8.9') - version('swaggerCore', '2.2.34') + version('swaggerCore', '2.2.35') version('swaggerInflector', '2.0.13') version('swagger2Parser', '1.0.75') version('swagger3Parser', '2.1.31') @@ -101,13 +101,13 @@ dependencyResolutionManagement { version('gradleTestLogger', '4.0.0') version('testLogger', '4.0.0') version('micronautPlatform', '4.9.0') - version('micronaut', '4.9.8') + version('micronaut', '4.9.9') version('micronautPlugin', '4.5.4') version('shadow', '8.1.1') version('checkstyle', '10.17.0') version('jacoco', '0.8.11') version('gradle', '8.6') - version('commonsCompress', '1.27.1') + version('commonsCompress', '1.28.0') version('bucket4j', '8.14.0') version('xstream', '1.4.21') diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index b1408263c3..da9c3949e5 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -15,11 +15,11 @@ "@eslint/js": "9.32.0", "@types/jest": "29.5.14", "@types/node": "20.19.9", - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "@zowe/cli": "8.25.0", - "@zowe/cli-test-utils": "8.25.0", - "@zowe/imperative": "8.25.0", + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@zowe/cli": "8.26.0", + "@zowe/cli-test-utils": "8.26.0", + "@zowe/imperative": "8.26.0", "copyfiles": "2.4.1", "env-cmd": "10.1.0", "eslint": "9.32.0", @@ -36,17 +36,17 @@ "jest-junit": "16.0.0", "jest-stare": "2.5.2", "madge": "8.0.0", - "ts-jest": "29.4.0", + "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.8", - "typescript": "5.8.3" + "typedoc": "0.28.9", + "typescript": "5.9.2" }, "engines": { "node": "=20.19.4", "npm": "=10.9.3" }, "peerDependencies": { - "@zowe/imperative": "8.25.0" + "@zowe/imperative": "8.26.0" } }, "node_modules/@ampproject/remapping": { @@ -777,16 +777,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.7.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@gerrit0/mini-shiki/-/mini-shiki-3.7.0.tgz", - "integrity": "sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==", + "version": "3.9.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", + "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.7.0", - "@shikijs/langs": "^3.7.0", - "@shikijs/themes": "^3.7.0", - "@shikijs/types": "^3.7.0", + "@shikijs/engine-oniguruma": "^3.9.2", + "@shikijs/langs": "^3.9.2", + "@shikijs/themes": "^3.9.2", + "@shikijs/types": "^3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -1642,40 +1642,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.7.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz", - "integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==", + "version": "3.9.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", + "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0", + "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.7.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/langs/-/langs-3.7.0.tgz", - "integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==", + "version": "3.9.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/langs/-/langs-3.9.2.tgz", + "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/themes": { - "version": "3.7.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/themes/-/themes-3.7.0.tgz", - "integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==", + "version": "3.9.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/themes/-/themes-3.9.2.tgz", + "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/types": { - "version": "3.7.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/types/-/types-3.7.0.tgz", - "integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==", + "version": "3.9.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/types/-/types-3.9.2.tgz", + "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", "dev": true, "license": "MIT", "dependencies": { @@ -2105,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", - "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/type-utils": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2129,9 +2129,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.38.0", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -2158,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.38.0.tgz", - "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -2179,18 +2179,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", - "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.38.0", - "@typescript-eslint/types": "^8.38.0", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -2201,18 +2201,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", - "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,9 +2223,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", - "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -2236,19 +2236,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", - "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2261,7 +2261,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { @@ -2278,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.38.0.tgz", - "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", "engines": { @@ -2292,16 +2292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", - "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.38.0", - "@typescript-eslint/tsconfig-utils": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2317,7 +2317,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -2360,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2380,17 +2380,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.38.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", - "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "version": "8.39.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2471,25 +2471,25 @@ "dev": true }, "node_modules/@zowe/cli": { - "version": "8.25.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.25.0.tgz", - "integrity": "sha512-5LRIqP8xrEO6m+JvkEF0UFOemoqV1Te6GLZB3fheMIpS9vmkgY454a5nsbPvcSOY5kVOuVHzt9LuMKtjQ2qHdQ==", + "version": "8.26.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.26.0.tgz", + "integrity": "sha512-4swTYaPN1IUsRnoccB/MPDaMnnCVxqin9lNY3Eys42SySAppLyKDvHRiNeSxikx2ekLsf8BoyVnRe/E2tmYtJw==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, "license": "EPL-2.0", "dependencies": { - "@zowe/core-for-zowe-sdk": "8.25.0", - "@zowe/imperative": "8.25.0", - "@zowe/provisioning-for-zowe-sdk": "8.25.0", - "@zowe/zos-console-for-zowe-sdk": "8.25.0", - "@zowe/zos-files-for-zowe-sdk": "8.25.0", - "@zowe/zos-jobs-for-zowe-sdk": "8.25.0", - "@zowe/zos-logs-for-zowe-sdk": "8.25.0", - "@zowe/zos-tso-for-zowe-sdk": "8.25.0", - "@zowe/zos-uss-for-zowe-sdk": "8.25.0", - "@zowe/zos-workflows-for-zowe-sdk": "8.25.0", - "@zowe/zosmf-for-zowe-sdk": "8.25.0", + "@zowe/core-for-zowe-sdk": "8.26.0", + "@zowe/imperative": "8.26.0", + "@zowe/provisioning-for-zowe-sdk": "8.26.0", + "@zowe/zos-console-for-zowe-sdk": "8.26.0", + "@zowe/zos-files-for-zowe-sdk": "8.26.0", + "@zowe/zos-jobs-for-zowe-sdk": "8.26.0", + "@zowe/zos-logs-for-zowe-sdk": "8.26.0", + "@zowe/zos-tso-for-zowe-sdk": "8.26.0", + "@zowe/zos-uss-for-zowe-sdk": "8.26.0", + "@zowe/zos-workflows-for-zowe-sdk": "8.26.0", + "@zowe/zosmf-for-zowe-sdk": "8.26.0", "find-process": "1.4.7", "lodash": "4.17.21", "minimatch": "9.0.5", @@ -2506,9 +2506,9 @@ } }, "node_modules/@zowe/cli-test-utils": { - "version": "8.25.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.25.0.tgz", - "integrity": "sha512-RY+BInbQqxpcE4025tyknYub819crbQCRzoJkjtVTLpxvxi6cf2s1+Kkod/lt7oVcjqtp1QV7eGDB+lD8BXPXA==", + "version": "8.26.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.26.0.tgz", + "integrity": "sha512-5w3YFep2YnQvhYGEFl3toLYEQtb3bvcEwMlVM28ncXgjUIZG0ys8F6YGZKRxHxf/Ytmygf526WuWwE/bZGVcew==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -3088,9 +3088,9 @@ "license": "MIT" }, "node_modules/@zowe/cli/node_modules/@zowe/core-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-2LoacIOP/qANnTcSLJLb/PFOZPWu0mhMlB9hC9P2lxX+Ybr0KAMt49f/PMcZRPhDI4wMYzSrlOwci01cmu5r6w==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-PRgbyxzdugGNyeLX+G8uNNE6qqlnkI9YGyvK9ka5uE4ODFGEDGOHGfn695AHt8gUYVXSXJrBVDevvzJV38iKkQ==", "dev": true, "dependencies": { "comment-json": "~4.2.3", @@ -3104,16 +3104,16 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/imperative": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.25.0.tgz", - "integrity": "sha512-5rnhsQGhUQESv82yYV9hwRd0emY08EY2OrB1KfYv8RhZwdOfxoFrDIry9KUac0QZgKqyboIUt1TmnPEWx7XyyQ==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.26.0.tgz", + "integrity": "sha512-hW+eXxmUgZ0qBfYXzx1Ywk+4uk7uxCaeO8tgWCByX8OrbsgJkv71wKJ7zrQaobENOh6Cqk66EZqJZgx/a5M7rA==", "dev": true, "dependencies": { "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "cli-table3": "^0.6.3", "comment-json": "~4.2.3", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "dataobject-parser": "^1.2.25", "dayjs": "1.11.13", "deepmerge": "^4.3.1", @@ -3232,9 +3232,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/provisioning-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-qdHkRU+Ll8L+3exasfofdlyblNADUtpzaiW7sJ6TFGDOEA3aGNnW7Kksl7RrIEkvRnoRIeEMny0mIhtGr8Uh6A==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-HETHGOKaCUk13GYbB11MfQzzkMDIS/tM4les5hn084j+t3u39ObBiiHUNMJp4WmkRIS01DICp7Mu1iEdoORoYg==", "dev": true, "dependencies": { "js-yaml": "^4.1.0" @@ -3259,9 +3259,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-console-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-oVYnx0PGdgF1iBzKlY5+e0szvcXPtXgNebtSQFDsaEgBllymAzG2XcIgWid5GJBhNsdxXBUYQa+0luKEyNmaNg==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-tnx/7QxB3/1SS//x7+mKRpbHnoylSAyYPQqg35nOLmpw+HmOeEt7Z9ZWyWrZcNUf6a2TnV8mwEIc4Y2+IAIbZg==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3272,9 +3272,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-files-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-S9pW7f80ubcTN9BkOHb+6VRszSVFV7bnKxnO0teOpUdMEhnbk467YC5LyuNSkAT2h0VaR1gcyyEJR/V2r7tisg==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-F71t7N3AKIQaGG/k6YXRJS0y95Mf9Yffke+hM4B9eFozoh5YvuHCRl9YBQyY4DJo418ZN/c5v6WyWSmatE0Oug==", "dev": true, "dependencies": { "lodash": "^4.17.21", @@ -3289,12 +3289,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-jobs-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-WbLPadC4VCDV82BjeqAZADGMdRlpYcf/O3kLYyQo5QMyzR1/vOruLI6Vw5Qq6BnGzIvoDZQwBEljXKulazL+FA==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-DLscbxUJLX/kOdivTfNJiNWq9b9kBzoR7NVYXIsB4HzuM0bcstqFQgD+muXweacgtC5Qk+9mFqHQgGp3hoDpzQ==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.25.0" + "@zowe/zos-files-for-zowe-sdk": "8.26.0" }, "engines": { "node": ">=18.12.0" @@ -3305,9 +3305,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-logs-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-HkmsoH/vjYA9PRy6ufJ6mhoWC7An8aZt+n2ENhjGmE9Q+PG5h8kn8Y2LvRJR5niYDn668QtTsJfu9kIcH5ofnw==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-eIFX0IQpBX3VqfgdapiyjjTB5BRpiCUTwZvbDxfKY2cMSsXSq+ZN+l1sIxzHswAdbWa4hHIrYhR4JavjCJiXuQ==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3318,12 +3318,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-tso-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-MpSIVUpZ+t6xsZMVg2+ON4+PruxLu0SNdYHDtbIH+6mDlXEBWSXkwj47xDzxVzhSqfkCJt9sIqb2pJxk+cHbdA==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-d6fHTteYUHz4ckgiYq6D1/7/1TXpCsZwM158s2tplnU4Z7ec+ug+0dUDL1PEeCtOWB7qcJwuCwClStbfkCsc8w==", "dev": true, "dependencies": { - "@zowe/zosmf-for-zowe-sdk": "8.25.0" + "@zowe/zosmf-for-zowe-sdk": "8.26.0" }, "engines": { "node": ">=18.12.0" @@ -3334,9 +3334,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-uss-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-j9NSD+in5NUiSrFGbJAmKnY3wbCqM824bl/966iDCaQFzOD7OvscNG9HOwGKKZLcyp/HQiSc4HX4wyal8lrl6w==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-4JiTfSbRq/ETPZ6ZCAshiKpAxc1Wa0YxAzUxYladueZyTFm9CVQQUUD73VERwMHJqtV6gAN5RTlAinOLHDXs8Q==", "dev": true, "dependencies": { "ssh2": "^1.15.0" @@ -3349,12 +3349,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-workflows-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-f14/CnTTHK7rx99elXZRi+bGiYuVwd63f6wksOax5fXczwvbikVdrtskJHc25TVb3TUd4TJYN7Zn9m1FVm1hnA==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-S59PqhSguIHSbE+geoV2UTqsDgJs0TFTKF36HFc/LQANfE2VwE/webkngq4Zgrmo2Nhdl2+z0mi/lSP7HPAabg==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.25.0" + "@zowe/zos-files-for-zowe-sdk": "8.26.0" }, "engines": { "node": ">=18.12.0" @@ -3365,9 +3365,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zosmf-for-zowe-sdk": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.25.0.tgz", - "integrity": "sha512-qCIEW43LN3YlgzkWz3oDcI1ePqujhVLrJ5oHXbBFzS7do4U4fx/1dntv5kyC4SzQ0t2kYK/JtyWE9jdQ16zHDQ==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.26.0.tgz", + "integrity": "sha512-KnK9jD+qFddImtrhB+LlSJiHCvStFhOMwPWbraGWnRoVXMH+X5bGxRdaiKZBLZRjygM4d6uU3ptcw/xSwr54Ng==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3626,9 +3626,9 @@ } }, "node_modules/@zowe/cli/node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -6036,9 +6036,9 @@ } }, "node_modules/@zowe/imperative": { - "version": "8.25.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.25.0.tgz", - "integrity": "sha512-5rnhsQGhUQESv82yYV9hwRd0emY08EY2OrB1KfYv8RhZwdOfxoFrDIry9KUac0QZgKqyboIUt1TmnPEWx7XyyQ==", + "version": "8.26.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.26.0.tgz", + "integrity": "sha512-hW+eXxmUgZ0qBfYXzx1Ywk+4uk7uxCaeO8tgWCByX8OrbsgJkv71wKJ7zrQaobENOh6Cqk66EZqJZgx/a5M7rA==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -6046,7 +6046,7 @@ "chalk": "^4.1.2", "cli-table3": "^0.6.3", "comment-json": "~4.2.3", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "dataobject-parser": "^1.2.25", "dayjs": "1.11.13", "deepmerge": "^4.3.1", @@ -7474,22 +7474,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.13", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", @@ -8054,39 +8038,6 @@ "node": ">=16.0.0" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/filing-cabinet": { "version": "5.0.2", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/filing-cabinet/-/filing-cabinet-5.0.2.tgz", @@ -8400,6 +8351,28 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/has-flag/-/has-flag-4.0.0.tgz", @@ -8942,25 +8915,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/jest/-/jest-29.7.0.tgz", @@ -10385,6 +10339,13 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-gyp": { "version": "10.2.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/node-gyp/-/node-gyp-10.2.0.tgz", @@ -12406,15 +12367,15 @@ } }, "node_modules/ts-jest": { - "version": "29.4.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ts-jest/-/ts-jest-29.4.0.tgz", - "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", + "version": "29.4.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", - "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", @@ -12584,13 +12545,13 @@ } }, "node_modules/typedoc": { - "version": "0.28.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.8.tgz", - "integrity": "sha512-16GfLopc8icHfdvqZDqdGBoS2AieIRP2rpf9mU+MgN+gGLyEQvAO0QgOa6NJ5QNmQi0LFrDY9in4F2fUNKgJKA==", + "version": "0.28.9", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.9.tgz", + "integrity": "sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.7.0", + "@gerrit0/mini-shiki": "^3.9.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", @@ -12604,7 +12565,7 @@ "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { @@ -12634,9 +12595,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -12654,6 +12615,20 @@ "dev": true, "license": "MIT" }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/underscore": { "version": "1.13.7", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/underscore/-/underscore-1.13.7.tgz", @@ -12927,6 +12902,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index e4713855e4..07ca66bc62 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -52,11 +52,11 @@ "@eslint/js": "9.32.0", "@types/jest": "29.5.14", "@types/node": "20.19.9", - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "@zowe/cli": "8.25.0", - "@zowe/cli-test-utils": "8.25.0", - "@zowe/imperative": "8.25.0", + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@zowe/cli": "8.26.0", + "@zowe/cli-test-utils": "8.26.0", + "@zowe/imperative": "8.26.0", "copyfiles": "2.4.1", "env-cmd": "10.1.0", "eslint": "9.32.0", @@ -73,16 +73,16 @@ "jest-junit": "16.0.0", "jest-stare": "2.5.2", "madge": "8.0.0", - "ts-jest": "29.4.0", + "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.8", - "typescript": "5.8.3" + "typedoc": "0.28.9", + "typescript": "5.9.2" }, "overrides": { "@babel/traverse": "7.28.0" }, "peerDependencies": { - "@zowe/imperative": "8.25.0" + "@zowe/imperative": "8.26.0" }, "engines": { "npm": "=10.9.3", From 15d679adc0e35585efc55f0549459c433c6b1b36 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 6 Aug 2025 09:54:23 +0200 Subject: [PATCH 048/152] fix: prefer legacy component properties in modulith mode (#4258) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- apiml-package/src/main/resources/bin/start.sh | 152 +++++++++--------- .../src/main/resources/manifest.yaml | 2 +- zaas-package/src/main/resources/bin/start.sh | 2 +- 3 files changed, 77 insertions(+), 79 deletions(-) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 9e1f8e412c..442ad191eb 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -325,78 +325,84 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ ${ADD_OPENS} \ ${LOGBACK} \ -Dapiml.cache.storage.location=${ZWE_zowe_workspaceDirectory}/api-mediation/${ZWE_haInstance_id:-localhost} \ - -Dapiml.connection.idleConnectionTimeoutSeconds=${ZWE_configs_apiml_connection_idleConnectionTimeoutSeconds:-${ZWE_components_gateway_apiml_connection_idleConnectionTimeoutSeconds:-5}} \ - -Dapiml.connection.timeout=${ZWE_configs_apiml_connection_timeout:-${ZWE_components_gateway_apiml_connection_timeout:-60000}} \ - -Dapiml.connection.timeToLive=${ZWE_configs_apiml_connection_timeToLive:-${ZWE_components_gateway_apiml_connection_timeToLive:-10000}} \ + -Dapiml.catalog.customStyle.backgroundColor=${ZWE_components_apicatalog_apiml_catalog_customStyle_backgroundColor:-${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-}} \ + -Dapiml.catalog.customStyle.docLink=${ZWE_components_apicatalog_apiml_catalog_customStyle_docLink:-${ZWE_configs_apiml_catalog_customStyle_docLink:-}} \ + -Dapiml.catalog.customStyle.fontFamily=${ZWE_components_apicatalog_apiml_catalog_customStyle_fontFamily:-${ZWE_configs_apiml_catalog_customStyle_fontFamily:-}} \ + -Dapiml.catalog.customStyle.headerColor=${ZWE_components_apicatalog_apiml_catalog_customStyle_headerColor:-${ZWE_configs_apiml_catalog_customStyle_headerColor:-}} \ + -Dapiml.catalog.customStyle.logo=${ZWE_components_apicatalog_apiml_catalog_customStyle_logo:-${ZWE_configs_apiml_catalog_customStyle_logo:-}} \ + -Dapiml.catalog.customStyle.textColor=${ZWE_components_apicatalog_apiml_catalog_customStyle_textColor:-${ZWE_configs_apiml_catalog_customStyle_textColor:-}} \ + -Dapiml.catalog.customStyle.titlesColor=${ZWE_components_apicatalog_apiml_catalog_customStyle_titlesColor:-${ZWE_configs_apiml_catalog_customStyle_titlesColor:-}} \ + -Dapiml.catalog.hide.serviceInfo=${ZWE_components_apicatalog_apiml_catalog_hide_serviceInfo:-${ZWE_configs_apiml_catalog_hide_serviceInfo:-false}} \ + -Dapiml.connection.idleConnectionTimeoutSeconds=${ZWE_components_gateway_apiml_connection_idleConnectionTimeoutSeconds:-${ZWE_configs_apiml_connection_idleConnectionTimeoutSeconds:-5}} \ + -Dapiml.connection.timeout=${ZWE_components_gateway_apiml_connection_timeout:-${ZWE_configs_apiml_connection_timeout:-60000}} \ + -Dapiml.connection.timeToLive=${ZWE_components_gateway_apiml_connection_timeToLive:-${ZWE_configs_apiml_connection_timeToLive:-10000}} \ -Dapiml.discovery.allPeersUrls=${ZWE_DISCOVERY_SERVICES_LIST} \ -Dapiml.discovery.password=password \ - -Dapiml.discovery.serviceIdPrefixReplacer=${ZWE_configs_apiml_discovery_serviceIdPrefixReplacer:-${ZWE_components_discovery_apiml_discovery_serviceIdPrefixReplacer}} \ + -Dapiml.discovery.serviceIdPrefixReplacer=${ZWE_components_discovery_apiml_discovery_serviceIdPrefixReplacer:-${ZWE_configs_apiml_discovery_serviceIdPrefixReplacer}} \ -Dapiml.discovery.staticApiDefinitionsDirectories=${ZWE_STATIC_DEFINITIONS_DIR:-} \ -Dapiml.discovery.userid=eureka \ - -Dapiml.gateway.cachePeriodSec=${ZWE_configs_apiml_gateway_registry_cachePeriodSec:-${ZWE_components_gateway_apiml_gateway_registry_cachePeriodSec:-120}} \ + -Dapiml.gateway.cachePeriodSec=${ZWE_components_gateway_apiml_gateway_registry_cachePeriodSec:-${ZWE_configs_apiml_gateway_registry_cachePeriodSec:-120}} \ -Dapiml.gateway.cookieNameForRateLimit=${cookieName:-apimlAuthenticationToken} \ - -Dapiml.gateway.maxSimultaneousRequests=${ZWE_configs_gateway_registry_maxSimultaneousRequests:-${ZWE_components_gateway_gateway_registry_maxSimultaneousRequests:-20}} \ - -Dapiml.gateway.rateLimiterCapacity=${ZWE_configs_apiml_gateway_rateLimiterCapacity:-${ZWE_components_gateway_apiml_gateway_rateLimiterCapacity:-20}} \ - -Dapiml.gateway.rateLimiterRefillDuration=${ZWE_configs_apiml_gateway_rateLimiterRefillDuration:-${ZWE_components_gateway_apiml_gateway_rateLimiterRefillDuration:-1}} \ - -Dapiml.gateway.rateLimiterTokens=${ZWE_configs_apiml_gateway_rateLimiterTokens:-${ZWE_components_gateway_apiml_gateway_rateLimiterTokens:-20}} \ - -Dapiml.gateway.refresh-interval-ms=${ZWE_configs_gateway_registry_refreshIntervalMs:-${ZWE_components_gateway_gateway_registry_refreshIntervalMs:-30000}} \ - -Dapiml.gateway.registry.enabled=${ZWE_configs_apiml_gateway_registry_enabled:-${ZWE_components_gateway_apiml_gateway_registry_enabled:-false}} \ - -Dapiml.gateway.registry.metadata-key-allow-list=${ZWE_configs_gateway_registry_metadataKeyAllowList:-${ZWE_components_gateway_gateway_registry_metadataKeyAllowList:-}} \ - -Dapiml.gateway.servicesToLimitRequestRate=${ZWE_configs_apiml_gateway_servicesToLimitRequestRate:-${ZWE_components_gateway_apiml_gateway_servicesToLimitRequestRate:-}} \ - -Dapiml.health.protected=${ZWE_configs_apiml_health_protected:-${ZWE_components_gateway_apiml_health_protected:-true}} \ + -Dapiml.gateway.maxSimultaneousRequests=${ZWE_components_gateway_gateway_registry_maxSimultaneousRequests:-${ZWE_configs_gateway_registry_maxSimultaneousRequests:-20}} \ + -Dapiml.gateway.rateLimiterCapacity=${ZWE_components_gateway_apiml_gateway_rateLimiterCapacity:-${ZWE_configs_apiml_gateway_rateLimiterCapacity:-20}} \ + -Dapiml.gateway.rateLimiterRefillDuration=${ZWE_components_gateway_apiml_gateway_rateLimiterRefillDuration:-${ZWE_configs_apiml_gateway_rateLimiterRefillDuration:-1}} \ + -Dapiml.gateway.rateLimiterTokens=${ZWE_components_gateway_apiml_gateway_rateLimiterTokens:-${ZWE_configs_apiml_gateway_rateLimiterTokens:-20}} \ + -Dapiml.gateway.refresh-interval-ms=${ZWE_components_gateway_gateway_registry_refreshIntervalMs:-${ZWE_configs_gateway_registry_refreshIntervalMs:-30000}} \ + -Dapiml.gateway.registry.enabled=${ZWE_components_gateway_apiml_gateway_registry_enabled:-${ZWE_configs_apiml_gateway_registry_enabled:-false}} \ + -Dapiml.gateway.registry.metadata-key-allow-list=${ZWE_components_gateway_gateway_registry_metadataKeyAllowList:-${ZWE_configs_gateway_registry_metadataKeyAllowList:-}} \ + -Dapiml.gateway.servicesToLimitRequestRate=${ZWE_components_gateway_apiml_gateway_servicesToLimitRequestRate:-${ZWE_configs_apiml_gateway_servicesToLimitRequestRate:-}} \ + -Dapiml.health.protected=${ZWE_components_gateway_apiml_health_protected:-${ZWE_configs_apiml_health_protected:-true}} \ -Dapiml.httpclient.ssl.enabled-protocols=${client_enabled_protocols} \ - -Dapiml.internal-discovery.port=${ZWE_configs_internal_discovery_port:-${ZWE_components_discovery_port:-7553}} \ + -Dapiml.internal-discovery.port=${ZWE_components_discovery_port:-${ZWE_configs_internal_discovery_port:-7553}} \ -Dapiml.logs.location=${ZWE_zowe_logDirectory} \ - -Dapiml.security.allowTokenRefresh=${ZWE_configs_apiml_security_allowtokenrefresh:-${ZWE_components_gateway_apiml_security_allowtokenrefresh:-false}} \ + -Dapiml.security.allowTokenRefresh=${ZWE_components_gateway_apiml_security_allowtokenrefresh:-${ZWE_configs_apiml_security_allowtokenrefresh:-false}} \ -Dapiml.security.auth.cookieProperties.cookieName=${cookieName:-apimlAuthenticationToken} \ - -Dapiml.security.auth.jwt.customAuthHeader=${ZWE_configs_apiml_security_auth_jwt_customAuthHeader:-${ZWE_components_gateway_apiml_security_auth_jwt_customAuthHeader:-}} \ - -Dapiml.security.auth.passticket.customAuthHeader=${ZWE_configs_apiml_security_auth_passticket_customAuthHeader:-${ZWE_components_gateway_apiml_security_auth_passticket_customAuthHeader:-}} \ - -Dapiml.security.auth.passticket.customUserHeader=${ZWE_configs_apiml_security_auth_passticket_customUserHeader:-${ZWE_components_gateway_apiml_security_auth_passticket_customUserHeader:-}} \ - -Dapiml.security.auth.provider=${ZWE_configs_apiml_security_auth_provider:-${ZWE_components_gateway_apiml_security_auth_provider:-zosmf}} \ - -Dapiml.security.auth.zosmf.jwtAutoconfiguration=${ZWE_configs_apiml_security_auth_zosmf_jwtAutoconfiguration:-${ZWE_components_gateway_apiml_security_auth_zosmf_jwtAutoconfiguration:-jwt}} \ - -Dapiml.security.auth.zosmf.serviceId=${ZWE_configs_apiml_security_auth_zosmf_serviceId:-${ZWE_components_gateway_apiml_security_auth_zosmf_serviceId:-ibmzosmf}} \ - -Dapiml.security.authorization.endpoint.enabled=${ZWE_configs_apiml_security_authorization_endpoint_enabled:-${ZWE_components_gateway_apiml_security_authorization_endpoint_enabled:-false}} \ - -Dapiml.security.authorization.endpoint.url=${ZWE_configs_apiml_security_authorization_endpoint_url:-${ZWE_components_gateway_apiml_security_authorization_endpoint_url:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/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_configs_apiml_security_authorization_resourceClass:-${ZWE_components_gateway_apiml_security_authorization_resourceClass:-ZOWE}} \ - -Dapiml.security.authorization.resourceNamePrefix=${ZWE_configs_apiml_security_authorization_resourceNamePrefix:-${ZWE_components_gateway_apiml_security_authorization_resourceNamePrefix:-APIML.}} \ - -Dapiml.security.jwtInitializerTimeout=${ZWE_configs_apiml_security_jwtInitializerTimeout:-${ZWE_components_gateway_apiml_security_jwtInitializerTimeout:-5}} \ - -Dapiml.security.oidc.enabled=${ZWE_configs_apiml_security_oidc_enabled:-${ZWE_components_gateway_apiml_security_oidc_enabled:-false}} \ - -Dapiml.security.oidc.identityMapperUrl=${ZWE_configs_apiml_security_oidc_identityMapperUrl:-${ZWE_components_gateway_apiml_security_oidc_identityMapperUrl:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/certificate/dn"}} \ - -Dapiml.security.oidc.identityMapperUser=${ZWE_configs_apiml_security_oidc_identityMapperUser:-${ZWE_components_gateway_apiml_security_oidc_identityMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ - -Dapiml.security.oidc.jwks.refreshInternalHours=${ZWE_configs_apiml_security_oidc_jwks_refreshInternalHours:-${ZWE_components_gateway_apiml_security_oidc_jwks_refreshInternalHours:-1}} \ - -Dapiml.security.oidc.jwks.uri=${ZWE_configs_apiml_security_oidc_jwks_uri:-${ZWE_components_gateway_apiml_security_oidc_jwks_uri:-}} \ - -Dapiml.security.oidc.registry=${ZWE_configs_apiml_security_oidc_registry:-${ZWE_components_gateway_apiml_security_oidc_registry:-}} \ - -Dapiml.security.oidc.userInfo.uri=${ZWE_configs_apiml_security_oidc_userInfo_uri:-${ZWE_components_gateway_apiml_security_oidc_userInfo_uri:-}} \ - -Dapiml.security.oidc.validationType=${ZWE_configs_apiml_security_oidc_validationType:-${ZWE_components_gateway_apiml_security_oidc_validationType:-"JWK"}} \ - -Dapiml.security.personalAccessToken.enabled=${ZWE_configs_apiml_security_personalAccessToken_enabled:-${ZWE_components_gateway_apiml_security_personalAccessToken_enabled:-false}} \ - -Dapiml.security.saf.provider=${ZWE_configs_apiml_security_saf_provider:-${ZWE_components_gateway_apiml_security_saf_provider:-"rest"}} \ - -Dapiml.security.saf.urls.authenticate=${ZWE_configs_apiml_security_saf_urls_authenticate:-${ZWE_components_gateway_apiml_security_saf_urls_authenticate:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf/authenticate"}} \ - -Dapiml.security.saf.urls.verify=${ZWE_configs_apiml_security_saf_urls_verify:-${ZWE_components_gateway_apiml_security_saf_urls_verify:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf/verify"}} \ + -Dapiml.security.auth.jwt.customAuthHeader=${ZWE_components_gateway_apiml_security_auth_jwt_customAuthHeader:-${ZWE_configs_apiml_security_auth_jwt_customAuthHeader:-}} \ + -Dapiml.security.auth.passticket.customAuthHeader=${ZWE_components_gateway_apiml_security_auth_passticket_customAuthHeader:-${ZWE_configs_apiml_security_auth_passticket_customAuthHeader:-}} \ + -Dapiml.security.auth.passticket.customUserHeader=${ZWE_components_gateway_apiml_security_auth_passticket_customUserHeader:-${ZWE_configs_apiml_security_auth_passticket_customUserHeader:-}} \ + -Dapiml.security.auth.provider=${ZWE_components_gateway_apiml_security_auth_provider:-${ZWE_configs_apiml_security_auth_provider:-zosmf}} \ + -Dapiml.security.auth.zosmf.jwtAutoconfiguration=${ZWE_components_gateway_apiml_security_auth_zosmf_jwtAutoconfiguration:-${ZWE_configs_apiml_security_auth_zosmf_jwtAutoconfiguration:-jwt}} \ + -Dapiml.security.auth.zosmf.serviceId=${ZWE_components_gateway_apiml_security_auth_zosmf_serviceId:-${ZWE_configs_apiml_security_auth_zosmf_serviceId:-ibmzosmf}} \ + -Dapiml.security.authorization.endpoint.enabled=${ZWE_components_gateway_apiml_security_authorization_endpoint_enabled:-${ZWE_configs_apiml_security_authorization_endpoint_enabled:-false}} \ + -Dapiml.security.authorization.endpoint.url=${ZWE_components_gateway_apiml_security_authorization_endpoint_url:-${ZWE_configs_apiml_security_authorization_endpoint_url:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf-auth"}} \ + -Dapiml.security.authorization.provider=${ZWE_components_gateway_apiml_security_authorization_provider:-${ZWE_configs_apiml_security_authorization_provider:-"native"}} \ + -Dapiml.security.authorization.resourceClass=${ZWE_components_gateway_apiml_security_authorization_resourceClass:-${ZWE_configs_apiml_security_authorization_resourceClass:-ZOWE}} \ + -Dapiml.security.authorization.resourceNamePrefix=${ZWE_components_gateway_apiml_security_authorization_resourceNamePrefix:-${ZWE_configs_apiml_security_authorization_resourceNamePrefix:-APIML.}} \ + -Dapiml.security.jwtInitializerTimeout=${ZWE_components_gateway_apiml_security_jwtInitializerTimeout:-${ZWE_configs_apiml_security_jwtInitializerTimeout:-5}} \ + -Dapiml.security.oidc.enabled=${ZWE_components_gateway_apiml_security_oidc_enabled:-${ZWE_configs_apiml_security_oidc_enabled:-false}} \ + -Dapiml.security.oidc.identityMapperUrl=${ZWE_components_gateway_apiml_security_oidc_identityMapperUrl:-${ZWE_configs_apiml_security_oidc_identityMapperUrl:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/certificate/dn"}} \ + -Dapiml.security.oidc.identityMapperUser=${ZWE_components_gateway_apiml_security_oidc_identityMapperUser:-${ZWE_configs_apiml_security_oidc_identityMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ + -Dapiml.security.oidc.jwks.refreshInternalHours=${ZWE_components_gateway_apiml_security_oidc_jwks_refreshInternalHours:-${ZWE_configs_apiml_security_oidc_jwks_refreshInternalHours:-1}} \ + -Dapiml.security.oidc.jwks.uri=${ZWE_components_gateway_apiml_security_oidc_jwks_uri:-${ZWE_configs_apiml_security_oidc_jwks_uri:-}} \ + -Dapiml.security.oidc.registry=${ZWE_components_gateway_apiml_security_oidc_registry:-${ZWE_configs_apiml_security_oidc_registry:-}} \ + -Dapiml.security.oidc.userInfo.uri=${ZWE_components_gateway_apiml_security_oidc_userInfo_uri:-${ZWE_configs_apiml_security_oidc_userInfo_uri:-}} \ + -Dapiml.security.oidc.validationType=${ZWE_components_gateway_apiml_security_oidc_validationType:-${ZWE_configs_apiml_security_oidc_validationType:-"JWK"}} \ + -Dapiml.security.personalAccessToken.enabled=${ZWE_components_gateway_apiml_security_personalAccessToken_enabled:-${ZWE_configs_apiml_security_personalAccessToken_enabled:-false}} \ + -Dapiml.security.saf.provider=${ZWE_components_gateway_apiml_security_saf_provider:-${ZWE_configs_apiml_security_saf_provider:-"rest"}} \ + -Dapiml.security.saf.urls.authenticate=${ZWE_components_gateway_apiml_security_saf_urls_authenticate:-${ZWE_configs_apiml_security_saf_urls_authenticate:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf/authenticate"}} \ + -Dapiml.security.saf.urls.verify=${ZWE_components_gateway_apiml_security_saf_urls_verify:-${ZWE_configs_apiml_security_saf_urls_verify:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/saf/verify"}} \ -Dapiml.security.ssl.nonStrictVerifySslCertificatesOfServices=${nonStrictVerifySslCertificatesOfServices:-false} \ -Dapiml.security.ssl.verifySslCertificatesOfServices=${verifySslCertificatesOfServices} \ - -Dapiml.security.useInternalMapper=${ZWE_configs_apiml_security_useInternalMapper:-${ZWE_components_gateway_apiml_security_useInternalMapper:-true}} \ - -Dapiml.security.x509.acceptForwardedCert=${ZWE_configs_apiml_security_x509_acceptForwardedCert:-${ZWE_components_gateway_apiml_security_x509_acceptForwardedCert:-false}} \ - -Dapiml.security.x509.acceptForwardedCert=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-true}}} \ - -Dapiml.security.x509.certificatesUrls=${CERTIFICATES_URLS} \ - -Dapiml.security.x509.certificatesUrls=${ZWE_configs_apiml_security_x509_certificatesUrls:-${ZWE_configs_apiml_security_x509_certificatesUrl:-${ZWE_components_gateway_apiml_security_x509_certificatesUrls:-${ZWE_components_gateway_apiml_security_x509_certificatesUrl}}}} \ - -Dapiml.security.x509.enabled=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-false}} \ - -Dapiml.security.x509.externalMapperUrl=${ZWE_configs_apiml_security_x509_externalMapperUrl:-${ZWE_components_gateway_apiml_security_x509_externalMapperUrl:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/certificate/x509/map"}} \ - -Dapiml.security.x509.externalMapperUser=${ZWE_configs_apiml_security_x509_externalMapperUser:-${ZWE_components_gateway_apiml_security_x509_externalMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ - -Dapiml.security.x509.registry.allowedUsers=${ZWE_configs_apiml_security_x509_registry_allowedUsers:-${ZWE_components_gateway_apiml_security_x509_registry_allowedUsers:-}} \ - -Dapiml.security.zosmf.applid=${ZWE_configs_apiml_security_zosmf_applid:-${ZWE_components_gateway_apiml_security_zosmf_applid:-IZUDFLT}} \ - -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-${ZWE_components_gateway_apiml_service_allowEncodedSlashes:-true}} \ - -Dapiml.service.apimlId=${ZWE_configs_apimlId:-${ZWE_components_gateway_apimlId:-}} \ - -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-${ZWE_components_gateway_apiml_service_corsEnabled:-false}} \ + -Dapiml.security.useInternalMapper=${ZWE_components_gateway_apiml_security_useInternalMapper:-${ZWE_configs_apiml_security_useInternalMapper:-true}} \ + -Dapiml.security.x509.acceptForwardedCert=${ZWE_components_gateway_apiml_security_x509_acceptForwardedCert:-${ZWE_configs_apiml_security_x509_acceptForwardedCert:-false}} \ + -Dapiml.security.x509.certificatesUrls=${ZWE_components_gateway_apiml_security_x509_certificatesUrls:-${ZWE_components_gateway_apiml_security_x509_certificatesUrl:-${ZWE_configs_apiml_security_x509_certificatesUrls:-${ZWE_configs_apiml_security_x509_certificatesUrl}}}} \ + -Dapiml.security.x509.enabled=${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_configs_apiml_security_x509_enabled:-false}} \ + -Dapiml.security.x509.externalMapperUrl=${ZWE_components_gateway_apiml_security_x509_externalMapperUrl:-${ZWE_configs_apiml_security_x509_externalMapperUrl:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/certificate/x509/map"}} \ + -Dapiml.security.x509.externalMapperUser=${ZWE_components_gateway_apiml_security_x509_externalMapperUser:-${ZWE_configs_apiml_security_x509_externalMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ + -Dapiml.security.x509.registry.allowedUsers=${ZWE_components_gateway_apiml_security_x509_registry_allowedUsers:-${ZWE_configs_apiml_security_x509_registry_allowedUsers:-}} \ + -Dapiml.security.zosmf.applid=${ZWE_components_gateway_apiml_security_zosmf_applid:-${ZWE_configs_apiml_security_zosmf_applid:-IZUDFLT}} \ + -Dapiml.service.allowEncodedSlashes=${ZWE_components_gateway_apiml_service_allowEncodedSlashes:-${ZWE_configs_apiml_service_allowEncodedSlashes:-true}} \ + -Dapiml.service.apimlId=${ZWE_components_gateway_apimlId:-${ZWE_configs_apimlId:-}} \ + -Dapiml.service.corsEnabled=${ZWE_components_gateway_apiml_service_corsEnabled:-${ZWE_configs_apiml_service_corsEnabled:-false}} \ -Dapiml.service.externalUrl="${externalProtocol}://${ZWE_zowe_externalDomains_0}:${ZWE_zowe_externalPort}" \ - -Dapiml.service.forwardClientCertEnabled=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-false}} \ + -Dapiml.service.forwardClientCertEnabled=${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_configs_apiml_security_x509_enabled:-false}} \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ -Dapiml.service.port=${ZWE_components_gateway_port:-${ZWE_configs_port:-7554}} \ -Dapiml.zoweManifest=${ZWE_zowe_runtimeDirectory}/manifest.json \ - -Dcaching.storage.evictionStrategy=${ZWE_configs_storage_evictionStrategy:-${ZWE_components_caching_service_storage_evictionStrategy:-reject}} \ - -Dcaching.storage.infinispan.initialHosts=${ZWE_configs_storage_infinispan_initialHosts:-${ZWE_components_caching_service_storage_infinispan_initialHosts:-"localhost[7600]"}} \ - -Dcaching.storage.mode=${ZWE_configs_storage_mode:-${ZWE_components_caching_service_storage_mode:-infinispan}} \ - -Dcaching.storage.size=${ZWE_configs_storage_size:-${ZWE_components_caching_service_storage_size:-10000}} \ + -Dcaching.storage.evictionStrategy=${ZWE_components_caching_service_storage_evictionStrategy:-${ZWE_configs_storage_evictionStrategy:-reject}} \ + -Dcaching.storage.infinispan.initialHosts=${ZWE_components_caching_service_storage_infinispan_initialHosts:-${ZWE_configs_storage_infinispan_initialHosts:-"localhost[7600]"}} \ + -Dcaching.storage.mode=${ZWE_components_caching_service_storage_mode:-${ZWE_configs_storage_mode:-infinispan}} \ + -Dcaching.storage.size=${ZWE_components_caching_service_storage_size:-${ZWE_configs_storage_size:-10000}} \ -Dcaching.storage.vsam.name=${VSAM_FILE_NAME} \ -Deureka.client.serviceUrl.defaultZone=${ZWE_DISCOVERY_SERVICES_LIST} \ -Dfile.encoding=UTF-8 \ @@ -406,18 +412,18 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ -Djavax.net.debug=${ZWE_configs_sslDebug:-${ZWE_components_gateway_sslDebug:-${ZWE_components_discovery_sslDebug:-""}}} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ - -Djgroups.bind.address=${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_components_caching_service_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}}} \ - -Djgroups.bind.port=${ZWE_configs_storage_infinispan_jgroups_port:-${ZWE_components_caching_service_storage_infinispan_jgroups_port:-7600}} \ - -Djgroups.keyExchange.port=${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-${ZWE_components_caching_service_storage_infinispan_jgroups_keyExchange_port:-7601}} \ - -Djgroups.tcp.diag.enabled=${ZWE_configs_storage_infinispan_jgroups_tcp_diag_enabled:-${ZWE_components_caching_service_storage_infinispan_jgroups_tcp_diag_enabled:-false}} \ + -Djgroups.bind.address=${ZWE_components_caching_service_storage_infinispan_jgroups_host:-${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}}} \ + -Djgroups.bind.port=${ZWE_components_caching_service_storage_infinispan_jgroups_port:-${ZWE_configs_storage_infinispan_jgroups_port:-7600}} \ + -Djgroups.keyExchange.port=${ZWE_components_caching_service_storage_infinispan_jgroups_keyExchange_port:-${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-7601}} \ + -Djgroups.tcp.diag.enabled=${ZWE_components_caching_service_storage_infinispan_jgroups_tcp_diag_enabled:-${ZWE_configs_storage_infinispan_jgroups_tcp_diag_enabled:-false}} \ -Dloader.path=${APIML_LOADER_PATH} \ -Dlogging.charset.console=${ZOWE_CONSOLE_LOG_CHARSET} \ -Dserver.address=${ZWE_configs_zowe_network_server_listenAddresses_0:-${ZWE_zowe_network_server_listenAddresses_0:-"0.0.0.0"}} \ - -Dserver.maxConnectionsPerRoute=${ZWE_configs_server_maxConnectionsPerRoute:-${ZWE_components_gateway_server_maxConnectionsPerRoute:-100}} \ - -Dserver.maxTotalConnections=${ZWE_configs_server_maxTotalConnections:-${ZWE_components_gateway_server_maxTotalConnections:-1000}} \ + -Dserver.maxConnectionsPerRoute=${ZWE_components_gateway_server_maxConnectionsPerRoute:-${ZWE_configs_server_maxConnectionsPerRoute:-100}} \ + -Dserver.maxTotalConnections=${ZWE_components_gateway_server_maxTotalConnections:-${ZWE_configs_server_maxTotalConnections:-1000}} \ -Dserver.ssl.ciphers=${server_ciphers} \ -Dserver.ssl.enabled-protocols=${server_enabled_protocols} \ - -Dserver.ssl.enabled=${ZWE_configs_server_ssl_enabled:-${ZWE_components_gateway_server_ssl_enabled:-${ZWE_components_discovery_server_ssl_enabled:-true}}} \ + -Dserver.ssl.enabled=${ZWE_components_gateway_server_ssl_enabled:-${ZWE_configs_server_ssl_enabled:-true}} \ -Dserver.ssl.keyAlias="${key_alias}" \ -Dserver.ssl.keyPassword="${key_pass}" \ -Dserver.ssl.keyStore="${keystore_location}" \ @@ -427,18 +433,10 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dserver.ssl.trustStore="${truststore_location}" \ -Dserver.ssl.trustStorePassword="${truststore_pass}" \ -Dserver.ssl.trustStoreType="${truststore_type}" \ - -Dserver.webSocket.asyncWriteTimeout=${ZWE_configs_server_webSocket_asyncWriteTimeout:-${ZWE_components_gateway_server_webSocket_asyncWriteTimeout:-60000}} \ - -Dserver.webSocket.connectTimeout=${ZWE_configs_server_webSocket_connectTimeout:-${ZWE_components_gateway_server_webSocket_connectTimeout:-45000}} \ - -Dserver.webSocket.maxIdleTimeout=${ZWE_configs_server_webSocket_maxIdleTimeout:-${ZWE_components_gateway_server_webSocket_maxIdleTimeout:-3600000}} \ - -Dserver.webSocket.requestBufferSize=${ZWE_configs_server_webSocket_requestBufferSize:-${ZWE_components_gateway_server_webSocket_requestBufferSize:-8192}} \ - -Dapiml.catalog.hide.serviceInfo=${ZWE_configs_apiml_catalog_hide_serviceInfo:-${ZWE_components_apicatalog_apiml_catalog_hide_serviceInfo:-false}} \ - -Dapiml.catalog.customStyle.logo=${ZWE_configs_apiml_catalog_customStyle_logo:-${ZWE_components_apicatalog_apiml_catalog_customStyle_logo:-}} \ - -Dapiml.catalog.customStyle.fontFamily=${ZWE_configs_apiml_catalog_customStyle_fontFamily:-${ZWE_components_apicatalog_apiml_catalog_customStyle_fontFamily:-}} \ - -Dapiml.catalog.customStyle.backgroundColor=${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_backgroundColor:-}} \ - -Dapiml.catalog.customStyle.titlesColor=${ZWE_configs_apiml_catalog_customStyle_titlesColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_titlesColor:-}} \ - -Dapiml.catalog.customStyle.headerColor=${ZWE_configs_apiml_catalog_customStyle_headerColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_headerColor:-}} \ - -Dapiml.catalog.customStyle.textColor=${ZWE_configs_apiml_catalog_customStyle_textColor:-${ZWE_components_apicatalog_apiml_catalog_customStyle_textColor:-}} \ - -Dapiml.catalog.customStyle.docLink=${ZWE_configs_apiml_catalog_customStyle_docLink:-${ZWE_components_apicatalog_apiml_catalog_customStyle_docLink:-}} \ + -Dserver.webSocket.asyncWriteTimeout=${ZWE_components_gateway_server_webSocket_asyncWriteTimeout:-${ZWE_configs_server_webSocket_asyncWriteTimeout:-60000}} \ + -Dserver.webSocket.connectTimeout=${ZWE_components_gateway_server_webSocket_connectTimeout:-${ZWE_configs_server_webSocket_connectTimeout:-45000}} \ + -Dserver.webSocket.maxIdleTimeout=${ZWE_components_gateway_server_webSocket_maxIdleTimeout:-${ZWE_configs_server_webSocket_maxIdleTimeout:-3600000}} \ + -Dserver.webSocket.requestBufferSize=${ZWE_components_gateway_server_webSocket_requestBufferSize:-${ZWE_configs_server_webSocket_requestBufferSize:-8192}} \ -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-} \ -jar "${JAR_FILE}" & diff --git a/apiml-package/src/main/resources/manifest.yaml b/apiml-package/src/main/resources/manifest.yaml index bb1f46e7e7..745a9fdd1d 100644 --- a/apiml-package/src/main/resources/manifest.yaml +++ b/apiml-package/src/main/resources/manifest.yaml @@ -35,7 +35,7 @@ configs: debug: false sslDebug: "" apimlId: - apiml: # TODO Is this needed? + apiml: service: # Enables forwarding client certificate from request to next gateway in a special request header forwardClientCertEnabled: false diff --git a/zaas-package/src/main/resources/bin/start.sh b/zaas-package/src/main/resources/bin/start.sh index d7f0d72ae2..06fe863bbc 100755 --- a/zaas-package/src/main/resources/bin/start.sh +++ b/zaas-package/src/main/resources/bin/start.sh @@ -354,7 +354,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${ZAAS_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.security.x509.enabled=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-false}} \ -Dapiml.security.x509.externalMapperUrl=${ZWE_configs_apiml_security_x509_externalMapperUrl:-${ZWE_components_gateway_apiml_security_x509_externalMapperUrl:-"${internalProtocol:-https}://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_gateway_port:-7554}/zss/api/v1/certificate/x509/map"}} \ -Dapiml.security.x509.externalMapperUser=${ZWE_configs_apiml_security_x509_externalMapperUser:-${ZWE_components_gateway_apiml_security_x509_externalMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ - -Dapiml.security.x509.acceptForwardedCert=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-true}}} \ + -Dapiml.security.x509.acceptForwardedCert=${ZWE_configs_apiml_security_x509_enabled:-${ZWE_components_gateway_apiml_security_x509_enabled:-true}} \ -Dapiml.security.x509.certificatesUrls=${CERTIFICATES_URLS} \ -Dapiml.security.authorization.provider=${ZWE_configs_apiml_security_authorization_provider:-${ZWE_components_gateway_apiml_security_authorization_provider:-"native"}} \ -Dapiml.security.authorization.endpoint.enabled=${ZWE_configs_apiml_security_authorization_endpoint_enabled:-${ZWE_components_gateway_apiml_security_authorization_endpoint_enabled:-false}} \ From 70c57954b0243b9786570aad541c9efa02ec7488 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Wed, 6 Aug 2025 14:38:41 +0200 Subject: [PATCH 049/152] Update CHANGELOG.md (#4233) Signed-off-by: Jakub Balhar Co-authored-by: Andrew Jandacek Signed-off-by: Gowtham Selvaraj --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5a2f51b77..d6964645cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ __Breaking changes in API ML__ | Configuration of keyrings now requires transformation from safkeyring://// to safkeyring:// | If your Zowe configuration contains safkeyring:////, change this part to safkeyring://. | Support access to z/OSMF only through /ibmzosmf route. V3 will not support access through the /zosmf route | If you use z/OSMF via {apimlUrl}/zosmf/{zosmfEndpoint} you need to move to {apimlUrl}/ibmzosmf/{zosmfEndpoint}. | Error code change for nonexistent services | Nonexistent service returns 404 with error code ZWEAO404E +| Service ids with underscore in service id won't be routed | Replace underscor with another character like - or remove it altogether from the service id | __New features and enhancements in API ML__ From 91d0d80123b2f03dc580f8538e263f033c5baf2f Mon Sep 17 00:00:00 2001 From: Elena Kubantseva Date: Wed, 6 Aug 2025 15:59:33 +0200 Subject: [PATCH 050/152] fix: Caching Service failing to start when not all ports avaliable on the system (#4260) Signed-off-by: Elena Kubantseva Signed-off-by: Gowtham Selvaraj --- caching-service/src/main/resources/infinispan.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/caching-service/src/main/resources/infinispan.xml b/caching-service/src/main/resources/infinispan.xml index 94c8b6e13b..d14a4c09bb 100644 --- a/caching-service/src/main/resources/infinispan.xml +++ b/caching-service/src/main/resources/infinispan.xml @@ -49,14 +49,12 @@ - - Date: Wed, 6 Aug 2025 18:04:30 +0200 Subject: [PATCH 051/152] feat: adding PATCH in the list of CORS allowed methods (#4254) Signed-off-by: nxhafa Signed-off-by: Gowtham Selvaraj --- .../java/org/zowe/apiml/util/CorsUtils.java | 18 +-- .../org/zowe/apiml/util/CorsUtilsTest.java | 132 ++++++++---------- .../gateway/config/ConnectionsConfig.java | 4 +- .../gateway/config/ConnectionsConfigTest.java | 56 +++++++- .../config/ServiceCorsUpdaterTest.java | 7 +- .../service/CorsMetadataProcessorTest.java | 7 +- 6 files changed, 128 insertions(+), 96 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 97e0f83184..1a3b0d9a72 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,9 +10,7 @@ package org.zowe.apiml.util; -import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.util.TriConsumer; -import org.springframework.http.HttpMethod; import org.springframework.web.cors.CorsConfiguration; import java.util.Arrays; @@ -22,20 +20,15 @@ import java.util.function.BiConsumer; import java.util.regex.Pattern; -@RequiredArgsConstructor public class CorsUtils { - private static final List allowedCorsHttpMethods; + private final List allowedCorsHttpMethods; private final boolean corsEnabled; - 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"); - static { - allowedCorsHttpMethods = Collections.unmodifiableList(Arrays.asList( - HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name(), - HttpMethod.DELETE.name(), HttpMethod.PUT.name(), HttpMethod.OPTIONS.name() - )); + public CorsUtils(boolean corsEnabled, List corsAllowedMethods) { + this.corsEnabled = corsEnabled; + this.allowedCorsHttpMethods = corsAllowedMethods; } public boolean isCorsEnabledForService(Map metadata) { @@ -69,8 +62,6 @@ private CorsConfiguration setAllowedOriginsForService(Map metada config.setAllowCredentials(true); config.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL)); config.setAllowedMethods(allowedCorsHttpMethods); - } else { - config.setAllowedOrigins(allowedOrigins); } return config; } @@ -79,7 +70,6 @@ public void registerDefaultCorsConfiguration(BiConsumer pathsToEnable; - config.setAllowedOrigins(allowedOrigins); if (corsEnabled) { config.setAllowCredentials(true); config.addAllowedOriginPattern(CorsConfiguration.ALL); //NOSONAR this is a replication of existing code 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 cfce1dab54..c1532b9d79 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,19 +13,17 @@ 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.web.cors.CorsConfiguration; -import java.util.*; -import java.util.function.BiConsumer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; class CorsUtilsTest { Map metadata = new HashMap<>(); + List defaultCorsMethods = List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"); @BeforeEach void setup() { @@ -35,62 +33,67 @@ void setup() { @Nested class GivenCorsEnabled { - CorsUtils corsUtils = new CorsUtils(true, Collections.emptyList()); - - @Test - void registerDefaultConfig() { - corsUtils.registerDefaultCorsConfiguration((path, configuration) -> { - assertTrue(path.contains("gateway")); - assertNotNull(configuration.getAllowedHeaders()); - assertEquals(1, configuration.getAllowedHeaders().size()); - assertEquals(6, configuration.getAllowedMethods().size()); - } - ); - } - - @Test - void registerConfigForService() { - - corsUtils.setCorsConfiguration("dclient", metadata, (path, serviceId, configuration) -> { - assertEquals(metadata.get("apiml.routes.v1.gateway"), path); - assertNotNull(configuration.getAllowedHeaders()); - assertEquals(1, configuration.getAllowedHeaders().size()); - assertEquals(6, configuration.getAllowedMethods().size()); - } - ); - } - - @Test - void registerDefaultConfigForService() { - metadata.remove("apiml.corsEnabled"); - corsUtils.setCorsConfiguration("dclient", metadata, (path, serviceId, configuration) -> { - assertEquals(metadata.get("apiml.routes.v1.gateway"), path); - assertNull(configuration.getAllowedMethods()); - } - ); - } - - @Test - 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) -> { - assertEquals(metadata.get("apiml.routes.v1.gateway"), path); - assertNotNull(configuration.getAllowedHeaders()); - assertTrue(configuration.getAllowedOrigins().contains("https://localhost:3000")); - assertEquals(3, configuration.getAllowedOrigins().size()); - assertEquals(1, configuration.getAllowedHeaders().size()); - assertEquals(6, configuration.getAllowedMethods().size()); - } - ); + @Nested + class givenDefaultCorsAllowedMethods { + + CorsUtils corsUtils = new CorsUtils(true, defaultCorsMethods); + + @Test + void registerDefaultConfig() { + corsUtils.registerDefaultCorsConfiguration((path, configuration) -> { + assertTrue(path.contains("gateway")); + assertNotNull(configuration.getAllowedHeaders()); + assertEquals(1, configuration.getAllowedHeaders().size()); + assertEquals(defaultCorsMethods.size(), configuration.getAllowedMethods().size()); + } + ); + } + + @Test + void registerConfigForService() { + + corsUtils.setCorsConfiguration("dclient", metadata, (path, serviceId, configuration) -> { + assertEquals(metadata.get("apiml.routes.v1.gateway"), path); + assertNotNull(configuration.getAllowedHeaders()); + assertEquals(1, configuration.getAllowedHeaders().size()); + assertEquals(defaultCorsMethods.size(), configuration.getAllowedMethods().size()); + } + ); + + } + + @Test + void registerDefaultConfigForService() { + metadata.remove("apiml.corsEnabled"); + corsUtils.setCorsConfiguration("dclient", metadata, (path, serviceId, configuration) -> { + assertEquals(metadata.get("apiml.routes.v1.gateway"), path); + assertNull(configuration.getAllowedMethods()); + } + ); + } + + @Test + 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) -> { + assertEquals(metadata.get("apiml.routes.v1.gateway"), path); + assertNotNull(configuration.getAllowedHeaders()); + assertTrue(configuration.getAllowedOrigins().contains("https://localhost:3000")); + assertEquals(3, configuration.getAllowedOrigins().size()); + assertEquals(1, configuration.getAllowedHeaders().size()); + assertEquals(defaultCorsMethods.size(), configuration.getAllowedMethods().size()); + } + ); + } } } @Nested class GivenCorsDisabled { - CorsUtils corsUtils = new CorsUtils(false, Collections.emptyList()); + CorsUtils corsUtils = new CorsUtils(false, null); @Test void registerEmptyDefaultConfig() { @@ -111,23 +114,4 @@ void registerEmptyConfigForService() { } } - @Nested - class Attls { - - @Test - void setAllowedOrigins() { - List allowedOrigins = Arrays.asList("a"); - CorsUtils corsUtils = new CorsUtils(true, allowedOrigins); - BiConsumer pathMapper = mock(BiConsumer.class); - corsUtils.registerDefaultCorsConfiguration(pathMapper); - - ArgumentCaptor corsConfigurationCaptor = ArgumentCaptor.forClass(CorsConfiguration.class); - - verify(pathMapper, times(3)).accept(any(), corsConfigurationCaptor.capture()); - assertEquals(1, corsConfigurationCaptor.getValue().getAllowedOrigins().size()); - assertEquals("a", corsConfigurationCaptor.getValue().getAllowedOrigins().get(0)); - } - - } - } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index d233fa92ad..f18f966b5d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -84,6 +84,8 @@ public class ConnectionsConfig { @Value("${apiml.service.corsEnabled:false}") private boolean corsEnabled; + @Value("${apiml.service.corsAllowedMethods:GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}") + private List corsAllowedMethods; private final ApplicationContext context; private final HttpConfig config; private static final ApimlLogger apimlLog = ApimlLogger.of(ConnectionsConfig.class, YamlMessageServiceInstance.getInstance()); @@ -268,7 +270,7 @@ Customizer defaultCustomizer() { @Bean CorsUtils corsUtils() { - return new CorsUtils(corsEnabled, null); + return new CorsUtils(corsEnabled, corsAllowedMethods); } @Bean diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java index 360ce10865..2f2e4ad3a5 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java @@ -31,24 +31,29 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.SslInfo; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.WebFilter; import org.zowe.apiml.gateway.GatewayServiceApplication; import org.zowe.apiml.product.web.HttpConfig; import org.zowe.apiml.security.common.util.ConnectionUtil; +import org.zowe.apiml.util.CorsUtils; import reactor.netty.http.client.HttpClient; import reactor.netty.tcp.SslProvider; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.X509KeyManager; import java.io.IOException; +import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.Socket; import java.security.*; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -157,7 +162,7 @@ class Negative { void whenAliasIsInvalid_thenNoCertificateProvided() throws UnrecoverableKeyException, CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException { when(httpConfig.getKeyAlias()).thenReturn("invalid"); - var sslContext = ConnectionUtil.getSslContext(httpConfig,true); + var sslContext = ConnectionUtil.getSslContext(httpConfig, true); var sslProvider = SslProvider.builder().sslContext(sslContext).build(); var httpClient = HttpClient.create().secure(sslProvider); httpClient.get() @@ -395,5 +400,54 @@ WebFilter sslDetector() { } + @Nested + @SpringBootTest( + properties = {"apiml.service.corsEnabled=true"} + ) + @ComponentScan(basePackages = "org.zowe.apiml.gateway") + class GivenCorsEnabled { + + @Nested + public class WhenCorsAllowedMethodsIsNotSet { + + @Autowired + private ConnectionsConfig connectionsConfig; + + @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); + assertEquals(7, corsAllowedMethods.size()); + } + } + + @Nested + @TestPropertySource(properties = { + "apiml.service.corsAllowedMethods=GET,POST, PATCH" + }) + @DirtiesContext + public class WhenCorsAllowedMethodsIsSet { + + @Autowired + private ConnectionsConfig connectionsConfig; + + @Test + void validateCorsAllowedMethods() throws NoSuchFieldException, IllegalAccessException { + CorsUtils corsUtils = connectionsConfig.corsUtils(); + + 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)); + } + } + } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java index 25bf721001..b4fca1b3db 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java @@ -30,8 +30,8 @@ import reactor.core.publisher.Flux; import reactor.test.StepVerifier; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -48,9 +48,10 @@ class ServiceCorsUpdaterTest { private static final String SERVICE_ID = "myserviceid"; private static final String APIML_ID = "apimlid"; - private CorsUtils corsUtils = spy(new CorsUtils(true, Collections.emptyList())); + private CorsUtils corsUtils = spy(new CorsUtils(true, List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"))); - @Mock private ReactiveDiscoveryClient discoveryClient; + @Mock + private ReactiveDiscoveryClient discoveryClient; private ServiceCorsUpdater serviceCorsUpdater; diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java index c62e8ac83d..ef0685120e 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java @@ -19,6 +19,7 @@ import org.zowe.apiml.util.CorsUtils; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; @@ -29,14 +30,14 @@ import static org.mockito.Mockito.verify; class CorsMetadataProcessorTest { - private CorsUtils corsUtils = new CorsUtils(true, null); + private CorsUtils corsUtils; private UrlBasedCorsConfigurationSource configurationSource; private ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(CorsConfiguration.class); @BeforeEach void setUp() { configurationSource = mock(UrlBasedCorsConfigurationSource.class); - corsUtils = new CorsUtils(true, null); + corsUtils = new CorsUtils(true, List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS")); } @Nested @@ -80,7 +81,7 @@ void corsIsEnabledPerService_allowedOriginsArentProvided() { private void assertDefaultConfiguration(CorsConfiguration provided) { assertThat(provided.getAllowedHeaders(), hasSize(1)); assertThat(provided.getAllowedHeaders().get(0), is("*")); - assertThat(provided.getAllowedMethods(), hasSize(6)); + assertThat(provided.getAllowedMethods(), hasSize(7)); assertThat(provided.getAllowedMethods().get(0), is("GET")); } } From bc78b33280ecd2d97df3cb37921b3c336bb9a4bf Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:58:56 +0200 Subject: [PATCH 052/152] fix: configure methods from zowe, catch exception (#4261) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- apiml-package/src/main/resources/bin/start.sh | 1 + .../apiml/security/common/audit/RauditxService.java | 12 ++++++++---- gateway-package/src/main/resources/bin/start.sh | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 442ad191eb..7ed0b7676f 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -394,6 +394,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.service.allowEncodedSlashes=${ZWE_components_gateway_apiml_service_allowEncodedSlashes:-${ZWE_configs_apiml_service_allowEncodedSlashes:-true}} \ -Dapiml.service.apimlId=${ZWE_components_gateway_apimlId:-${ZWE_configs_apimlId:-}} \ -Dapiml.service.corsEnabled=${ZWE_components_gateway_apiml_service_corsEnabled:-${ZWE_configs_apiml_service_corsEnabled:-false}} \ + -Dapiml.service.corsAllowedMethods=${ZWE_components_gateway_apiml_service_corsAllowedMethods:-${ZWE_configs_apiml_service_corsAllowedMethods:-}} \ -Dapiml.service.externalUrl="${externalProtocol}://${ZWE_zowe_externalDomains_0}:${ZWE_zowe_externalPort}" \ -Dapiml.service.forwardClientCertEnabled=${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_configs_apiml_security_x509_enabled:-false}} \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/audit/RauditxService.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/audit/RauditxService.java index 0b10af0507..deee538587 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/audit/RauditxService.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/audit/RauditxService.java @@ -123,10 +123,14 @@ public void verifyPrivileges() { if (!StringUtils.isBlank(userId)) { SafResourceAccessVerifying safResourceAccessVerifying = getNativeSafResourceAccessVerifying(); if (safResourceAccessVerifying != null) { - hasAccess = safResourceAccessVerifying.hasSafResourceAccess( - new UsernamePasswordAuthenticationToken(userId, null), - "FACILITY", "IRR.RAUDITX", "READ" - ); + try { + hasAccess = safResourceAccessVerifying.hasSafResourceAccess( + new UsernamePasswordAuthenticationToken(userId, null), + "FACILITY", "IRR.RAUDITX", "READ" + ); + } catch (Exception e) { + log.debug("There was a problem while verifying user access. {}", e.getMessage(), e); + } } } if (!hasAccess) { diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 0bc23d898c..4179feba2c 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -325,6 +325,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-true} \ -Dapiml.service.apimlId=${ZWE_configs_apimlId:-} \ -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-false} \ + -Dapiml.service.corsAllowedMethods=${ZWE_configs_apiml_service_corsAllowedMethods:-} \ -Dapiml.service.externalUrl="${externalProtocol}://${ZWE_zowe_externalDomains_0}:${ZWE_zowe_externalPort}" \ -Dapiml.service.forwardClientCertEnabled=${ZWE_configs_apiml_security_x509_enabled:-false} \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ From e5f57f8edc34b81004212f16630c909998e528f4 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 7 Aug 2025 12:33:52 +0000 Subject: [PATCH 053/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.2.26'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e29c418a0d..fffdfb699a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.26-SNAPSHOT +version=3.2.26 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From da5f0154770745626c41a2bd0a3f613608ed6266 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 7 Aug 2025 12:33:54 +0000 Subject: [PATCH 054/152] [Gradle Release plugin] Create new version: 'v3.3.0-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fffdfb699a..aee5cbe8f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.2.26 +version=3.3.0-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 32853edfea561e50c37da8dd5f3c520325a2ca5a Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 7 Aug 2025 12:33:55 +0000 Subject: [PATCH 055/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 076efc6c0a..e3685f49f3 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.2.26-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.0-SNAPSHOT From d4d1710b95aaf959080f942febc5492458809236 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 7 Aug 2025 13:32:06 +0000 Subject: [PATCH 056/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.0'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index aee5cbe8f3..a6f676196d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.0-SNAPSHOT +version=3.3.0 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 6aae66965d20988bfb89b63f0f6603d09752bab9 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 7 Aug 2025 13:32:08 +0000 Subject: [PATCH 057/152] [Gradle Release plugin] Create new version: 'v3.3.1-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a6f676196d..d322294716 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.0 +version=3.3.1-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 462c4b42106ce2535c80f50b5516bb0ccc275d4e Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 7 Aug 2025 13:32:09 +0000 Subject: [PATCH 058/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index e3685f49f3..3fef834c6c 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.0-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.1-SNAPSHOT From 94a0ebd22d5379c1d6fe922fc7e2b7a5d4fd6308 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 8 Aug 2025 00:46:52 +0000 Subject: [PATCH 059/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.1'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d322294716..31468f7c6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.1-SNAPSHOT +version=3.3.1 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From b26f5adb7d5e21b7e50d683e6aebb545470dd4b2 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 8 Aug 2025 00:46:54 +0000 Subject: [PATCH 060/152] [Gradle Release plugin] Create new version: 'v3.3.2-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 31468f7c6c..8294b079f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.1 +version=3.3.2-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 0cee8e48741dc525a4b9358df856324ea31543af Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 8 Aug 2025 00:46:56 +0000 Subject: [PATCH 061/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 3fef834c6c..db93b6db15 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.1-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.2-SNAPSHOT From 4f0655c6d30695f91da1e1b1f93e1ddcd77829c0 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:18:43 +0200 Subject: [PATCH 062/152] chore: Update all non-major dependencies (v3.x.x) (#4259) Signed-off-by: Renovate Bot Co-authored-by: Renovate Bot Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 32 +++++++++++------------ api-catalog-ui/frontend/package.json | 8 +++--- gradle/versions.gradle | 4 +-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index d73262238f..fa796f8d6e 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -43,7 +43,7 @@ "react-error-boundary": "5.0.0", "react-hook-form": "7.62.0", "react-redux": "9.2.0", - "react-router": "7.6.3", + "react-router": "7.8.0", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -53,7 +53,7 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.89.2", + "sass": "1.90.0", "stream": "0.0.3", "swagger-ui-react": "5.27.1", "url": "0.11.4", @@ -92,7 +92,7 @@ "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-prettier": "5.5.3", + "eslint-plugin-prettier": "5.5.4", "eslint-plugin-react": "7.37.5", "express": "4.21.2", "globals": "15.15.0", @@ -121,7 +121,7 @@ "start-server-and-test": "2.0.13", "tmpl": "1.0.5", "undici": "6.19.8", - "yaml": "2.8.0" + "yaml": "2.8.1" }, "engines": { "node": "=20.19.4", @@ -12561,9 +12561,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", - "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", + "version": "5.5.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", "dev": true, "license": "MIT", "dependencies": { @@ -24045,9 +24045,9 @@ } }, "node_modules/react-router": { - "version": "7.6.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.6.3.tgz", - "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", + "version": "7.8.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.8.0.tgz", + "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -27432,9 +27432,9 @@ "license": "CC0-1.0" }, "node_modules/sass": { - "version": "1.89.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.89.2.tgz", - "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", + "version": "1.90.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.90.0.tgz", + "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -32106,9 +32106,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 488a27f6cd..cfb78cf93f 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -39,7 +39,7 @@ "react-error-boundary": "5.0.0", "react-hook-form": "7.62.0", "react-redux": "9.2.0", - "react-router": "7.6.3", + "react-router": "7.8.0", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -49,7 +49,7 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.89.2", + "sass": "1.90.0", "stream": "0.0.3", "swagger-ui-react": "5.27.1", "url": "0.11.4", @@ -113,7 +113,7 @@ "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-prettier": "5.5.3", + "eslint-plugin-prettier": "5.5.4", "eslint-plugin-react": "7.37.5", "express": "4.21.2", "globals": "15.15.0", @@ -142,7 +142,7 @@ "start-server-and-test": "2.0.13", "tmpl": "1.0.5", "undici": "6.19.8", - "yaml": "2.8.0" + "yaml": "2.8.1" }, "overrides": { "nth-check": "2.1.1", diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 593d59d04b..aa944d2e35 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -59,7 +59,7 @@ dependencyResolutionManagement { } version('jbossLogging', '3.6.1.Final') version('jerseySun', '1.19.4') - version('jettyWebSocketClient', '12.0.23') + version('jettyWebSocketClient', '12.0.24') version('jettison', '1.5.4') //0.12.x version contains breaking changes version('jjwt', '0.12.6') @@ -76,7 +76,7 @@ dependencyResolutionManagement { version('netty', '4.2.3.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 version('nettyReactor', '1.2.8') - version('nimbusJoseJwt', '10.4') + version('nimbusJoseJwt', '10.4.1') version('openApiDiff', '2.1.2') version('picocli', '4.7.7') From 4f935e0cf2c2bdfa3627e39a0eb0e1a578fc46b6 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:38:34 +0200 Subject: [PATCH 063/152] chore: Update all non-major dependencies (v3.x.x) (#4264) Signed-off-by: Renovate Bot Co-authored-by: Renovate Bot Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 78 +++---- api-catalog-ui/frontend/package.json | 12 +- gradle/versions.gradle | 2 +- .../package-lock.json | 190 +++++++++--------- zowe-cli-id-federation-plugin/package.json | 18 +- 5 files changed, 150 insertions(+), 150 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index fa796f8d6e..9494f17930 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -12,7 +12,7 @@ "@emotion/is-prop-valid": "1.3.1", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", - "@eslint/migrate-config": "1.5.2", + "@eslint/migrate-config": "1.5.3", "@jest/globals": "29.7.0", "@material-ui/core": "4.12.4", "@material-ui/icons": "4.11.3", @@ -67,8 +67,8 @@ "@babel/preset-env": "7.28.0", "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", - "@eslint/compat": "1.3.1", - "@eslint/js": "9.32.0", + "@eslint/compat": "1.3.2", + "@eslint/js": "9.33.0", "@reduxjs/toolkit": "2.8.2", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.6.4", @@ -77,14 +77,14 @@ "ajv": "8.17.1", "ansi-regex": "6.1.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001731", + "caniuse-lite": "1.0.30001734", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.32.0", + "eslint": "9.33.0", "eslint-config-airbnb": "19.0.4", "eslint-config-prettier": "9.1.2", "eslint-plugin-cypress": "4.3.0", @@ -106,7 +106,7 @@ "jest-mock": "29.7.0", "jest-watch-typeahead": "2.2.2", "json-schema": "0.4.0", - "mini-css-extract-plugin": "2.9.3", + "mini-css-extract-plugin": "2.9.4", "nodemon": "3.1.10", "nth-check": "2.1.1", "prettier": "3.6.2", @@ -2945,9 +2945,9 @@ } }, "node_modules/@eslint/compat": { - "version": "1.3.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/compat/-/compat-1.3.1.tgz", - "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", + "version": "1.3.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/compat/-/compat-1.3.2.tgz", + "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2977,9 +2977,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2987,9 +2987,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3057,9 +3057,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -3070,12 +3070,12 @@ } }, "node_modules/@eslint/migrate-config": { - "version": "1.5.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/migrate-config/-/migrate-config-1.5.2.tgz", - "integrity": "sha512-OOh3O5ncnW/ZNZEVGNQNKI0ElDW29HmEcAAsgl+skEMw1b+VN0YVsUJjbPNzJDBnarq3jFeVQq6nPI+9jHFnZw==", + "version": "1.5.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/migrate-config/-/migrate-config-1.5.3.tgz", + "integrity": "sha512-2bNQdMc2FOy20GhZvZjwz8Ypnj+3t4qo0JLgpASCd6j8AQjzWBgJ0elhc3gXjwntS2XUSxRyw72gchGNxoFzQw==", "license": "Apache-2.0", "dependencies": { - "@eslint/compat": "^1.3.1", + "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.1.0", "camelcase": "^8.0.0", "espree": "^10.3.0", @@ -3099,13 +3099,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -9407,9 +9407,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001734", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", "funding": [ { "type": "opencollective", @@ -12215,20 +12215,20 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.33.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -20320,9 +20320,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.3.tgz", - "integrity": "sha512-tRA0+PsS4kLVijnN1w9jUu5lkxBwUk9E8SbgEB5dBJqchE6pVYdawROG6uQtpmAri7tdCK9i7b1bULeVWqS6Ag==", + "version": "2.9.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index cfb78cf93f..9dbc40bb21 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -8,7 +8,7 @@ "@emotion/is-prop-valid": "1.3.1", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", - "@eslint/migrate-config": "1.5.2", + "@eslint/migrate-config": "1.5.3", "@jest/globals": "29.7.0", "@material-ui/core": "4.12.4", "@material-ui/icons": "4.11.3", @@ -88,8 +88,8 @@ "@babel/preset-env": "7.28.0", "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", - "@eslint/compat": "1.3.1", - "@eslint/js": "9.32.0", + "@eslint/compat": "1.3.2", + "@eslint/js": "9.33.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.6.4", "@testing-library/react": "16.3.0", @@ -98,14 +98,14 @@ "ajv": "8.17.1", "ansi-regex": "6.1.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001731", + "caniuse-lite": "1.0.30001734", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.32.0", + "eslint": "9.33.0", "eslint-config-airbnb": "19.0.4", "eslint-config-prettier": "9.1.2", "eslint-plugin-cypress": "4.3.0", @@ -127,7 +127,7 @@ "jest-mock": "29.7.0", "jest-watch-typeahead": "2.2.2", "json-schema": "0.4.0", - "mini-css-extract-plugin": "2.9.3", + "mini-css-extract-plugin": "2.9.4", "nodemon": "3.1.10", "nth-check": "2.1.1", "prettier": "3.6.2", diff --git a/gradle/versions.gradle b/gradle/versions.gradle index aa944d2e35..bc9c9bf2f1 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -69,7 +69,7 @@ dependencyResolutionManagement { version('junitJupiter', '5.13.4') version('junitPlatform', '1.13.4') version('jxpath', '1.4.0') - version('lettuce', '6.7.1.RELEASE') + version('lettuce', '6.8.0.RELEASE') // force version in build.gradle file - compatibility with Slf4j version('log4j', '2.25.1') version('lombok', '1.18.38') diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index da9c3949e5..e1b802aed0 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -12,17 +12,17 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.32.0", + "@eslint/js": "9.33.0", "@types/jest": "29.5.14", - "@types/node": "20.19.9", - "@typescript-eslint/eslint-plugin": "8.39.0", - "@typescript-eslint/parser": "8.39.0", - "@zowe/cli": "8.26.0", + "@types/node": "20.19.10", + "@typescript-eslint/eslint-plugin": "8.39.1", + "@typescript-eslint/parser": "8.39.1", + "@zowe/cli": "8.26.1", "@zowe/cli-test-utils": "8.26.0", - "@zowe/imperative": "8.26.0", + "@zowe/imperative": "8.26.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.32.0", + "eslint": "9.33.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.1.4", @@ -38,7 +38,7 @@ "madge": "8.0.0", "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.9", + "typedoc": "0.28.10", "typescript": "5.9.2" }, "engines": { @@ -46,7 +46,7 @@ "npm": "=10.9.3" }, "peerDependencies": { - "@zowe/imperative": "8.26.0" + "@zowe/imperative": "8.26.2" } }, "node_modules/@ampproject/remapping": { @@ -681,9 +681,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -691,9 +691,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -740,9 +740,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -763,13 +763,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -2060,9 +2060,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.19.9", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.9.tgz", - "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "version": "20.19.10", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.10.tgz", + "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2105,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", - "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/type-utils": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2129,7 +2129,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2158,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.39.0.tgz", - "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2183,14 +2183,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", - "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.0", - "@typescript-eslint/types": "^8.39.0", + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2205,14 +2205,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", - "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0" + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,9 +2223,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", - "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", "dev": true, "license": "MIT", "engines": { @@ -2240,15 +2240,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", - "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2278,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.39.0.tgz", - "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", "dev": true, "license": "MIT", "engines": { @@ -2292,16 +2292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", - "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.0", - "@typescript-eslint/tsconfig-utils": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2360,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0" + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2384,13 +2384,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", - "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "version": "8.39.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2471,9 +2471,9 @@ "dev": true }, "node_modules/@zowe/cli": { - "version": "8.26.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.26.0.tgz", - "integrity": "sha512-4swTYaPN1IUsRnoccB/MPDaMnnCVxqin9lNY3Eys42SySAppLyKDvHRiNeSxikx2ekLsf8BoyVnRe/E2tmYtJw==", + "version": "8.26.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.26.1.tgz", + "integrity": "sha512-HkTkq4nm2DQjPxlQVltyCTBPBWdpAVquqGNmu/eBLyIkdNhAc5ClPYiP/JF2NDbHUNerpkXbaqsI0cgZbdSVZA==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, @@ -6036,9 +6036,9 @@ } }, "node_modules/@zowe/imperative": { - "version": "8.26.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.26.0.tgz", - "integrity": "sha512-hW+eXxmUgZ0qBfYXzx1Ywk+4uk7uxCaeO8tgWCByX8OrbsgJkv71wKJ7zrQaobENOh6Cqk66EZqJZgx/a5M7rA==", + "version": "8.26.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/imperative/-/imperative-8.26.2.tgz", + "integrity": "sha512-/28ns2ze24Zx+avjN9i0xXWynlcUWZ4SPlfBz293elAw0cUaX7a1l3kkFuDp6iQHiKYDvWOBfTeEfnNhjnKbBQ==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -7629,20 +7629,20 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.33.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -12545,9 +12545,9 @@ } }, "node_modules/typedoc": { - "version": "0.28.9", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.9.tgz", - "integrity": "sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==", + "version": "0.28.10", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.10.tgz", + "integrity": "sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 07ca66bc62..6194dacd1d 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -49,17 +49,17 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.32.0", + "@eslint/js": "9.33.0", "@types/jest": "29.5.14", - "@types/node": "20.19.9", - "@typescript-eslint/eslint-plugin": "8.39.0", - "@typescript-eslint/parser": "8.39.0", - "@zowe/cli": "8.26.0", + "@types/node": "20.19.10", + "@typescript-eslint/eslint-plugin": "8.39.1", + "@typescript-eslint/parser": "8.39.1", + "@zowe/cli": "8.26.1", "@zowe/cli-test-utils": "8.26.0", - "@zowe/imperative": "8.26.0", + "@zowe/imperative": "8.26.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.32.0", + "eslint": "9.33.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.1.4", @@ -75,14 +75,14 @@ "madge": "8.0.0", "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.9", + "typedoc": "0.28.10", "typescript": "5.9.2" }, "overrides": { "@babel/traverse": "7.28.0" }, "peerDependencies": { - "@zowe/imperative": "8.26.0" + "@zowe/imperative": "8.26.2" }, "engines": { "npm": "=10.9.3", From c1569f7aa4cc7b91b9db292baed0370513bc51cc Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 15 Aug 2025 00:44:58 +0000 Subject: [PATCH 064/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.2'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8294b079f9..935a7bfed3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.2-SNAPSHOT +version=3.3.2 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 48e7721c3b372c75d4aea814fc41bc91053d62aa Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 15 Aug 2025 00:45:00 +0000 Subject: [PATCH 065/152] [Gradle Release plugin] Create new version: 'v3.3.3-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 935a7bfed3..9a2f793631 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.2 +version=3.3.3-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From f460006119c1b82d38ae896e168bea9db3117727 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 15 Aug 2025 00:45:01 +0000 Subject: [PATCH 066/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index db93b6db15..580c1afa8d 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.2-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.3-SNAPSHOT From a844ad3a9236b3b5cee5953d2603d89168333aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Fri, 15 Aug 2025 11:03:26 +0200 Subject: [PATCH 067/152] Disable retry for configured services (#4265) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 11 ++++ .../src/main/resources/bin/start.sh | 1 + .../apiml/gateway/config/RoutingConfig.java | 23 ++++++--- .../apiml/gateway/service/RouteLocator.java | 17 +++++-- .../acceptance/RetryPerServiceTest.java | 50 ++++++++++++++++--- .../gateway/service/RouteLocatorTest.java | 18 +++++-- .../proxy/MultipartPutIntegrationTest.java | 43 ++++++++++++++++ schemas/gateway-schema.json | 8 +++ 8 files changed, 149 insertions(+), 22 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a5488534dd..052525dab6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -51,11 +51,17 @@ jobs: SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient + APIML_GATEWAY_SERVICESTODISABLERETRY: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken + #The memory is constrained to test large file upload memory issues (#4265) + #If the container runs oom, it is hard killed without printing any reasonable message in the log + options: --memory 640m --memory-swap 640m discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka + SPRING_SERVLET_MULTIPART_MAXFILESIZE: 1024MB + SPRING_SERVLET_MULTIPART_MAXREQUESTSIZE: 1024MB mock-services: image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} env: @@ -328,6 +334,9 @@ jobs: image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} discoverable-client: image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }} + env: + SPRING_SERVLET_MULTIPART_MAXFILESIZE: 1024MB + SPRING_SERVLET_MULTIPART_MAXREQUESTSIZE: 1024MB discovery-service: image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} # needs to run in isolation from another DS for multi-tenancy setup @@ -347,9 +356,11 @@ jobs: SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient + APIML_GATEWAY_SERVICESTODISABLERETRY: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken EUREKA_CLIENT_INSTANCEINFOREPLICATIONINTERVALSECONDS: 1 EUREKA_CLIENT_REGISTRYFETCHINTERVALSECONDS: 1 + options: --memory 640m --memory-swap 640m zaas-service: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 4179feba2c..7e0e569375 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -305,6 +305,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.gateway.registry.enabled=${ZWE_configs_apiml_gateway_registry_enabled:-false} \ -Dapiml.gateway.registry.metadata-key-allow-list=${ZWE_configs_gateway_registry_metadataKeyAllowList:-} \ -Dapiml.gateway.servicesToLimitRequestRate=${ZWE_configs_apiml_gateway_servicesToLimitRequestRate:-} \ + -Dapiml.gateway.servicesToDisableRetry=${ZWE_configs_apiml_gateway_servicesToDisableRetry:-} \ -Dapiml.health.protected=${ZWE_configs_apiml_health_protected:-true} \ -Dapiml.httpclient.ssl.enabled-protocols=${client_enabled_protocols} \ -Dapiml.logs.location=${ZWE_zowe_logDirectory} \ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java index ca19ce5598..8b4a81022a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java @@ -32,7 +32,7 @@ public class RoutingConfig { private boolean allowEncodedSlashes; @Bean - public List filters() { + public List commonNoRetryFilters() { List filters = new ArrayList<>(); if (acceptForwardedCert) { @@ -55,13 +55,6 @@ public List filters() { circuitBreakerFilter.setName("CircuitBreaker"); filters.add(circuitBreakerFilter); - FilterDefinition retryFilter = new FilterDefinition(); - retryFilter.setName("Retry"); - retryFilter.addArg("retries", "5"); - retryFilter.addArg("statuses", "SERVICE_UNAVAILABLE"); - retryFilter.addArg("series", ""); - filters.add(retryFilter); - for (String headerName : ignoredHeadersWhenCorsEnabled.split(",")) { FilterDefinition removeHeaders = new FilterDefinition(); removeHeaders.setName("RemoveRequestHeader"); @@ -72,4 +65,18 @@ public List filters() { } return filters; } + + @Bean + public List commonFilters(List commonNoRetryFilters) { + List filters = new ArrayList<>(commonNoRetryFilters); + + FilterDefinition retryFilter = new FilterDefinition(); + retryFilter.setName("Retry"); + retryFilter.addArg("retries", "5"); + retryFilter.addArg("statuses", "SERVICE_UNAVAILABLE"); + retryFilter.addArg("series", ""); + filters.add(retryFilter); + + return filters; + } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java index 482a1e546a..3796dbfb31 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java @@ -50,11 +50,17 @@ public class RouteLocator implements RouteDefinitionLocator { private boolean forwardingClientCertEnabled; @Value("${apiml.gateway.servicesToLimitRequestRate:-}") + List servicesToLimitRequestRateProperty; List servicesToLimitRequestRate; + @Value("${apiml.gateway.servicesToDisableRetry:-}") + List servicesToDisableRetryProperty; + List servicesToDisableRetry; + private final ReactiveDiscoveryClient discoveryClient; private final List commonFilters; + private final List commonNoRetryFilters; private final List routeDefinitionProducers; private final List schemeHandlersList; private final Map schemeHandlers = new EnumMap<>(AuthenticationScheme.class); @@ -64,6 +70,9 @@ void afterPropertiesSet() { for (SchemeHandler schemeHandler : schemeHandlersList) { schemeHandlers.put(schemeHandler.getAuthenticationScheme(), schemeHandler); } + + servicesToLimitRequestRate = servicesToLimitRequestRateProperty.stream().map(String::toLowerCase).toList(); + servicesToDisableRetry = servicesToDisableRetryProperty.stream().map(String::toLowerCase).toList(); } Flux> getServiceInstances() { @@ -82,8 +91,6 @@ void setAuth(ServiceInstance serviceInstance, RouteDefinition routeDefinition, A } } - - Stream getRoutedService(ServiceInstance serviceInstance) { return metadataParser.parseToListRoute(serviceInstance.getMetadata()).stream() // sorting avoid a conflict with the more general pattern @@ -141,7 +148,11 @@ List getPostRoutingFilters(ServiceInstance serviceInstance, Ro pageRedirectionFilter.addArg("serviceUrl", routedService.getServiceUrl()); serviceRelated.add(pageRedirectionFilter); - return join(commonFilters, serviceRelated); + if (servicesToDisableRetry.contains(serviceInstance.getServiceId().toLowerCase())) { + return join(commonNoRetryFilters, serviceRelated); + } else { + return join(commonFilters, serviceRelated); + } } private List getAuthFilterPerRoute( diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java index d2b5f365c6..e6bfc64cd3 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java @@ -14,11 +14,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.gateway.MockService; -import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; - -import java.io.IOException; +import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; @@ -28,26 +27,40 @@ @MicroservicesAcceptanceTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestPropertySource(properties = { + "apiml.gateway.servicesToDisableRetry=no-retry-service,no-RETRY-Service-2" +}) class RetryPerServiceTest extends AcceptanceTestWithMockServices { private static final String HEADER_X_FORWARD_TO = "X-Forward-To"; private MockService mockService; + private MockService mockNoRetryService; + private MockService mockNoRetryService2; @BeforeAll - void startMockService() throws IOException { + void startMockService() { mockService = mockService("serviceid1").scope(MockService.Scope.CLASS) .addEndpoint("/503").responseCode(503) .and() .addEndpoint("/401").responseCode(401) .and().start(); + + mockNoRetryService = mockService("no-retry-service").scope(MockService.Scope.CLASS) + .addEndpoint("/503").responseCode(503) + .and().start(); + + mockNoRetryService2 = mockService("No-Retry-Service-2").scope(MockService.Scope.CLASS) + .addEndpoint("/503").responseCode(503) + .and().start(); } @Nested class GivenRetryOnAllOperationsIsDisabled { + //Only default GET method remains active @Test - void whenGetReturnsUnavailable_thenRetry() throws Exception { + void whenGetReturnsUnavailable_thenRetry() { given() .header(HEADER_X_FORWARD_TO, "serviceid1") .when() @@ -58,7 +71,7 @@ void whenGetReturnsUnavailable_thenRetry() throws Exception { } @Test - void whenRequestReturnsUnauthorized_thenDontRetry() throws Exception { + void whenRequestReturnsUnauthorized_thenDontRetry() { for (int i = 1; i < 6; i++) { given() .header(HEADER_X_FORWARD_TO, "serviceid1") @@ -71,7 +84,7 @@ void whenRequestReturnsUnauthorized_thenDontRetry() throws Exception { } @Test - void whenPostReturnsUnavailable_thenDontRetry() throws Exception { + void whenPostReturnsUnavailable_thenDontRetry() { given() .header(HEADER_X_FORWARD_TO, "serviceid1") .when() @@ -81,6 +94,29 @@ void whenPostReturnsUnavailable_thenDontRetry() throws Exception { assertEquals(1, mockService.getCounter()); } + @Test + void whenRetryForServiceIsDisabled_andGetReturnsUnavailable_thenDontRetry() { + given() + .header(HEADER_X_FORWARD_TO, "no-retry-service") + .when() + .get(basePath + "/503") + .then() + .statusCode(is(SC_SERVICE_UNAVAILABLE)); + assertEquals(1, mockNoRetryService.getCounter()); + } + + @Test + void whenRetryForServiceIsDisabled_andGetReturnsUnavailable_onMixedCaseServiceId_thenDontRetry() { + given() + .header(HEADER_X_FORWARD_TO, "no-retry-service-2") + .when() + .get(basePath + "/503") + .then() + .statusCode(is(SC_SERVICE_UNAVAILABLE)); + + assertEquals(1, mockNoRetryService2.getCounter()); + } + } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java index c606f5ab2b..4916b28f8c 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java @@ -10,6 +10,7 @@ package org.zowe.apiml.gateway.service; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -56,9 +57,12 @@ void init() { routeLocator = spy(new RouteLocator( discoveryClient, Arrays.asList(COMMON_FILTERS), + Arrays.asList(COMMON_FILTERS), Arrays.asList(PRODUCERS), Arrays.asList(SCHEME_HANDLER_FILTERS) )); + routeLocator.servicesToLimitRequestRateProperty = Collections.emptyList(); + routeLocator.servicesToDisableRetryProperty = Collections.emptyList(); routeLocator.afterPropertiesSet(); } @@ -218,10 +222,17 @@ void givenRouteLocator_whenGetRouteDefinitions_thenGenerateAll() { @Nested class PostRoutingFilterDefinition { - private final List COMMON_FILTERS = Collections.singletonList(mock(FilterDefinition.class)); - private final RouteLocator routeLocator = new RouteLocator(null, COMMON_FILTERS, Collections.emptyList(), null); + private static final List COMMON_FILTERS = Collections.singletonList(mock(FilterDefinition.class)); + private static final RouteLocator routeLocator = new RouteLocator(null, COMMON_FILTERS, COMMON_FILTERS, Collections.emptyList(), Collections.emptyList()); private final RoutedService routedService = new RoutedService("test", "api/v1", "/service1"); + @BeforeAll + static void init() { + routeLocator.servicesToLimitRequestRateProperty = Collections.emptyList(); + routeLocator.servicesToDisableRetryProperty = Collections.emptyList(); + routeLocator.afterPropertiesSet(); + } + private ServiceInstance createServiceInstance(Boolean forwardingEnabled, Boolean encodedCharactersEnabled, Boolean rateLimiterEnabled) { Map metadata = new HashMap<>(); if (forwardingEnabled != null) { @@ -235,11 +246,10 @@ private ServiceInstance createServiceInstance(Boolean forwardingEnabled, Boolean } ServiceInstance serviceInstance = mock(ServiceInstance.class); doReturn(metadata).when(serviceInstance).getMetadata(); + doReturn("dummy").when(serviceInstance).getServiceId(); return serviceInstance; } - - @Nested class EnabledForwarding { diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java index c647d0fa3c..e4efcfe3dd 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java @@ -20,7 +20,10 @@ import org.zowe.apiml.util.http.HttpRequestUtils; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; +import java.util.Random; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; @@ -35,6 +38,7 @@ class MultipartPutIntegrationTest implements TestWithStartedInstances { @BeforeAll static void beforeClass() { RestAssured.useRelaxedHTTPSValidation(); + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); } @Nested @@ -71,5 +75,44 @@ void givenPostRequest() { post(url); } } + + @Test + void givenLargeFileUpload() { + int payloadSize = 750 * 1024 * 1024; //750MB + + given() + .multiPart( + "file", + "largefile.dat", + new RandomDataInputStream(payloadSize), + "application/octet-stream" + ) + .when() + .post(url) + .then() + .statusCode(200) + .body("fileName", equalTo("largefile.dat")) + .body("fileType", equalTo("application/octet-stream")) + .body("size", equalTo(payloadSize)); + } + + static class RandomDataInputStream extends InputStream { + private final long targetSize; + private long count = 0; + private final Random random = new Random(); + + RandomDataInputStream(long targetSize) { + this.targetSize = targetSize; + } + + @Override + public int read() throws IOException { + if (count >= targetSize) { + return -1; + } + count++; + return random.nextInt(256); + } + } } } diff --git a/schemas/gateway-schema.json b/schemas/gateway-schema.json index a2a243d523..c7524c1d36 100644 --- a/schemas/gateway-schema.json +++ b/schemas/gateway-schema.json @@ -199,6 +199,14 @@ "type": "string", "description": "The name of the service." } + }, + "servicesToDisableRetry":{ + "type": "array", + "description": "Array of services which the retry filter will be disabled for.", + "items": { + "type": "string", + "description": "The name of the service." + } } } }, From abb7dd405653852555ae15ddd2b2699368569b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= <58428711+pj892031@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:36:12 +0200 Subject: [PATCH 068/152] fix: Fix URLs for onboarding when AT-TLS is enabled (#4169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Elena Kubantseva Signed-off-by: nxhafa Signed-off-by: ac892247 Signed-off-by: Pablo Carle Co-authored-by: Elena Kubantseva Co-authored-by: Nafi Xhafa <164854562+nxhafa@users.noreply.github.com> Co-authored-by: nxhafa Co-authored-by: ac892247 Co-authored-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../src/main/resources/bin/start.sh | 24 +++++++++----- .../StaticRegistrationServiceRest.java | 6 ++-- .../apicatalog/swagger/ContainerService.java | 2 +- .../src/main/resources/application.yml | 33 +++++++++++++++---- .../functional/AttlsConfigTest.java | 2 +- apiml-package/src/main/resources/bin/start.sh | 24 +++++++++----- .../zowe/apiml/filter/AttlsHttpHandler.java | 2 +- .../product/web/ApimlTomcatCustomizer.java | 2 +- apiml/src/main/resources/application.yml | 19 +++++++++-- .../src/main/resources/bin/start.sh | 22 ++++++++----- .../src/main/resources/application.yml | 14 ++++++-- .../apiml/caching/config/AttlsConfigTest.java | 2 +- .../configuration/SecurityConfiguration.java | 6 ++-- .../src/main/resources/application.yml | 19 ++++++++--- .../src/main/resources/bin/start.sh | 11 ++++--- .../config/HttpWebSecurityConfig.java | 2 +- .../config/HttpWebSecurityLoginConfig.java | 2 +- .../config/HttpsWebSecurityConfig.java | 16 +++++---- .../src/main/resources/application.yml | 18 ++++++++-- .../discovery/config/AttlsConfigTest.java | 4 +-- .../src/main/resources/bin/start.sh | 27 +++++++++------ .../apiml/gateway/config/RegistryConfig.java | 8 ++--- .../apiml/gateway/config/SwaggerConfig.java | 6 ++-- .../filters/PageRedirectionFilterFactory.java | 6 ++-- .../src/main/resources/application.yml | 25 ++++++++++++-- .../PageRedirectionFilterFactoryTest.java | 2 +- .../src/main/resources/application.yml | 8 ++++- zaas-package/src/main/resources/bin/start.sh | 24 +++++++++----- .../config/NewSecurityConfiguration.java | 8 ++--- .../src/main/resources/application.yml | 25 +++++++++++--- .../zaas/security/config/AttlsConfigTest.java | 2 +- 31 files changed, 258 insertions(+), 113 deletions(-) diff --git a/api-catalog-package/src/main/resources/bin/start.sh b/api-catalog-package/src/main/resources/bin/start.sh index bb5d5ca3d3..df8e5fd723 100755 --- a/api-catalog-package/src/main/resources/bin/start.sh +++ b/api-catalog-package/src/main/resources/bin/start.sh @@ -150,25 +150,30 @@ ADD_OPENS="--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED" -ATTLS_ENABLED="false" +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + +ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" if [ "${ZWE_zowe_network_server_tls_attls}" = "true" ]; then - ATTLS_ENABLED="true" + ATTLS_SERVER_ENABLED="true" fi if [ "${ZWE_zowe_network_client_tls_attls}" = "true" ]; then ATTLS_CLIENT_ENABLED="true" fi -if [ "${ATTLS_ENABLED}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" ]; then + add_profile "attlsServer" ZWE_configs_server_ssl_enabled="false" - if [ -n "${ZWE_configs_spring_profiles_active}" ]; then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}attls" fi -if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" -o "$ATTLS_ENABLED" = "true" ]; then +if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" -o "$ATTLS_SERVER_ENABLED" = "true" ]; then externalProtocol="https" else externalProtocol="http" @@ -177,6 +182,7 @@ fi internalProtocol="https" ZWE_DISCOVERY_SERVICES_LIST=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} if [ "$ATTLS_CLIENT_ENABLED" = "true" ]; then + add_profile "attlsClient" ZWE_DISCOVERY_SERVICES_LIST=$(echo "${ZWE_DISCOVERY_SERVICES_LIST=}" | sed -e 's|https://|http://|g') internalProtocol="http" fi @@ -250,7 +256,7 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" fi -if [ "${ATTLS_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then keystore_type= keystore_pass= key_pass= diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java index 1760184920..157c6cd1c7 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/staticapi/StaticRegistrationServiceRest.java @@ -47,8 +47,8 @@ public class StaticRegistrationServiceRest implements StaticRegistrationService @Qualifier("webClientClientCert") private final WebClient webClientClientCert; - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; + @Value("${server.attlsServer.enabled:false}") + private boolean isServerAttlsEnabled; private final DiscoveryConfigProperties discoveryConfigProperties; @@ -60,7 +60,7 @@ public Mono refresh() { .header(ACCEPT, APPLICATION_JSON_VALUE) .headers(headers -> { boolean isHttp = uri.startsWith("http://"); - if (isHttp && !isAttlsEnabled) { + if (isHttp && !isServerAttlsEnabled) { String basicToken = "Basic " + Base64.getEncoder().encodeToString((eurekaUserid + ":" + eurekaPassword).getBytes()); headers.add(HttpHeaders.AUTHORIZATION, basicToken); } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java index 8724c19a23..efa006d8da 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java @@ -62,7 +62,7 @@ public class ContainerService { @Value("${apiml.catalog.hide.serviceInfo:false}") private boolean hideServiceInfo; - @Value("${server.attls.enabled:false}") + @Value("${server.attlsClient.enabled:false}") private boolean isAttlsEnabled; @InjectApimlLogger diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index 1915f9f65a..1d21f8c0b6 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + spring: application: name: ${apiml.service.id} @@ -229,13 +232,34 @@ management: exposure: include: "*" --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: - attls: + attlsServer: enabled: true ssl: enabled: false + +eureka: + instance: + secureHealthCheckUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health + metadata-map: + apiml: + corsEnabled: true + corsAllowedOrigins: https://${apiml.service.hostname}:${apiml.service.port},${apiml.service.externalUrl} + apiInfo: + - apiId: zowe.apiml.apicatalog + version: 1.0.0 + gatewayUrl: api/v1 + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + eureka: instance: securePort: 0 @@ -251,8 +275,3 @@ eureka: version: 1.0.0 gatewayUrl: api/v1 swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs -apiml: - service: - scheme: http - nonSecurePortEnabled: true - securePortEnabled: false 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 0f6355a24d..95559747df 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 @@ -39,7 +39,7 @@ @TestPropertySource( properties = { - "server.attls.enabled=true", + "server.attlsServer.enabled=true", "server.ssl.enabled=false" } ) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 7ed0b7676f..b84dc8dd6c 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -181,33 +181,39 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" fi -ATTLS_ENABLED="false" +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + +ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" if [ "${ZWE_zowe_network_server_tls_attls}" = "true" ]; then - ATTLS_ENABLED="true" + ATTLS_SERVER_ENABLED="true" fi if [ "${ZWE_zowe_network_client_tls_attls}" = "true" ]; then ATTLS_CLIENT_ENABLED="true" fi -if [ "${ATTLS_ENABLED}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" ]; then + add_profile "attlsServer" ZWE_configs_server_ssl_enabled="false" - if [ -n "${ZWE_configs_spring_profiles_active}" ]; then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}attls" fi internalProtocol="https" ZWE_DISCOVERY_SERVICES_LIST=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} if [ "${ATTLS_CLIENT_ENABLED}" = "true" ]; then + add_profile "attlsClient" ZWE_DISCOVERY_SERVICES_LIST=$(echo "${ZWE_DISCOVERY_SERVICES_LIST=}" | sed -e 's|https://|http://|g') internalProtocol=http ZWE_configs_apiml_service_corsEnabled=true fi -if [ "${ZWE_configs_server_ssl_enabled:-${ZWE_components_gateway_server_ssl_enabled:-${ZWE_components_discovery_server_ssl_enabled:-true}}}" = "true" -o "$ATTLS_ENABLED" = "true" ]; then +if [ "${ZWE_configs_server_ssl_enabled:-${ZWE_components_gateway_server_ssl_enabled:-${ZWE_components_discovery_server_ssl_enabled:-true}}}" = "true" -o "$ATTLS_SERVER_ENABLED" = "true" ]; then externalProtocol="https" else externalProtocol="http" @@ -304,7 +310,7 @@ elif [ "${keystore_type}" = "JCEHYBRIDRACFKS" ]; then truststore_location=$(echo "${truststore_location}" | sed s_safkeyring://_safkeyringjcehybrid://_) fi -if [ "${ATTLS_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then keystore_type= keystore_pass= key_pass= diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java index 39f5c53969..3ee92021b8 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java @@ -48,7 +48,7 @@ @Component @RequiredArgsConstructor -@ConditionalOnProperty(name = "server.attls.enabled", havingValue = "true") +@ConditionalOnProperty(name = "server.attlsServer.enabled", havingValue = "true") @Slf4j public class AttlsHttpHandler implements BeanPostProcessor { diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java index 4ad6410c30..c1894d1327 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java @@ -33,7 +33,7 @@ @Slf4j @Component -@ConditionalOnProperty(name = "server.attls.enabled", havingValue = "true") +@ConditionalOnProperty(name = "server.attlsServer.enabled", havingValue = "true") public class ApimlTomcatCustomizer implements TomcatConnectorCustomizer { private static final String INCOMPATIBLE_VERSION_MESSAGE = "AT-TLS-Incompatible configuration. Verify AT-TLS requirements: Java version, Tomcat version. Exception message: "; diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index ed713c64f1..21c177415b 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + eureka: dashboard: path: /eureka @@ -357,19 +360,29 @@ logging: org.infinispan: DEBUG --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: - attls: + attlsServer: enabled: true ssl: enabled: false +apiml: + service: + corsEnabled: true + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true service: scheme: http + apiml: service: - corsEnabled: true scheme: http nonSecurePortEnabled: true securePortEnabled: false diff --git a/caching-service-package/src/main/resources/bin/start.sh b/caching-service-package/src/main/resources/bin/start.sh index ee57cccdc7..0ade013093 100755 --- a/caching-service-package/src/main/resources/bin/start.sh +++ b/caching-service-package/src/main/resources/bin/start.sh @@ -120,27 +120,33 @@ ADD_OPENS="--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED" -ATTLS_ENABLED="false" +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + +ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" if [ "${ZWE_zowe_network_server_tls_attls}" = "true" ]; then - ATTLS_ENABLED="true" + ATTLS_SERVER_ENABLED="true" fi if [ "${ZWE_zowe_network_client_tls_attls}" = "true" ]; then ATTLS_CLIENT_ENABLED="true" fi -if [ "${ATTLS_ENABLED}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" ]; then + add_profile "attlsServer" ZWE_configs_server_ssl_enabled="false" - if [ -n "${ZWE_configs_spring_profiles_active}" ]; then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}attls" fi # Verify discovery service URL in case AT-TLS is enabled, assumes outgoing rules are in place ZWE_DISCOVERY_SERVICES_LIST=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} if [ "${ATTLS_CLIENT_ENABLED}" = "true" ]; then + add_profile "attlsClient" ZWE_DISCOVERY_SERVICES_LIST=$(echo "${ZWE_DISCOVERY_SERVICES_LIST=}" | sed -e 's|https://|http://|g') fi @@ -216,7 +222,7 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" fi -if [ "${ATTLS_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then keystore_type= keystore_pass= key_pass= diff --git a/caching-service/src/main/resources/application.yml b/caching-service/src/main/resources/application.yml index 9ebaa1f286..d0560e3ffd 100644 --- a/caching-service/src/main/resources/application.yml +++ b/caching-service/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + caching: storage: mode: inMemory @@ -183,18 +186,25 @@ spring.config.activate.on-profile: dev logbackServiceName: ZWEACS1 --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: - attls: + attlsServer: enabled: true ssl: enabled: false +--- +spring.config.activate.on-profile: attlsClient + apiml: service: scheme: http +server: + attlsClient: + enabled: true + eureka: instance: nonSecurePortEnabled: true 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 cb80411970..2ffed8f5ca 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 @@ -51,7 +51,7 @@ @ActiveProfiles("AttlsConfigTestCachingService") @TestPropertySource( properties = { - "server.attls.enabled=true", + "server.attlsServer.enabled=true", "server.ssl.enabled=false", "caching.storage.mode=inMemory" } diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java index d9ce5ef2de..435bd91525 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/SecurityConfiguration.java @@ -34,8 +34,8 @@ @EnableMethodSecurity public class SecurityConfiguration { - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; + @Value("${server.attlsServer.enabled:false}") + private boolean isServerAttlsEnabled; @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -46,7 +46,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/**").permitAll()) .httpBasic(withDefaults()); - if (isAttlsEnabled) { + if (isServerAttlsEnabled) { newConf.addFilterBefore(new AttlsFilter(), UsernamePasswordAuthenticationFilter.class); } return newConf.build(); diff --git a/discoverable-client/src/main/resources/application.yml b/discoverable-client/src/main/resources/application.yml index 623f9c6fe3..57e86c063e 100644 --- a/discoverable-client/src/main/resources/application.yml +++ b/discoverable-client/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + logging: level: ROOT: INFO @@ -220,13 +223,21 @@ logging: logbackServiceName: ZWEADC1 --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer + +server: + attlsServer: + enabled: true + ssl: + enabled: false + +--- +spring.config.activate.on-profile: attlsClient apiml: service: scheme: http server: - ssl: - enabled: false - + attlsClient: + enabled: true diff --git a/discovery-package/src/main/resources/bin/start.sh b/discovery-package/src/main/resources/bin/start.sh index fcd56f7ad6..c1316c9bb6 100755 --- a/discovery-package/src/main/resources/bin/start.sh +++ b/discovery-package/src/main/resources/bin/start.sh @@ -120,19 +120,19 @@ if [ "$(uname)" = "OS/390" ]; then fi fi -ATTLS_ENABLED="false" +ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" if [ "${ZWE_zowe_network_server_tls_attls}" = "true" ]; then - ATTLS_ENABLED="true" + ATTLS_SERVER_ENABLED="true" fi if [ "${ZWE_zowe_network_client_tls_attls}" = "true" ]; then ATTLS_CLIENT_ENABLED="true" fi -if [ "${ATTLS_ENABLED}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" ]; then + add_profile "attlsServer" ZWE_configs_server_ssl_enabled="false" - add_profile "attls" fi if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" ]; then @@ -141,6 +141,7 @@ fi ZWE_DISCOVERY_SERVICES_LIST=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} if [ "${ATTLS_CLIENT_ENABLED}" = "true" ]; then + add_profile "attlsClient" ZWE_DISCOVERY_SERVICES_LIST=$(echo "${ZWE_DISCOVERY_SERVICES_LIST=}" | sed -e 's|https://|http://|g') fi @@ -244,7 +245,7 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" fi -if [ "${ATTLS_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then keystore_type= keystore_pass= key_pass= diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityConfig.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityConfig.java index ad58e83def..48be722b95 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityConfig.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityConfig.java @@ -46,7 +46,7 @@ */ @Configuration @RequiredArgsConstructor -@Profile("!https & !attls") +@Profile("!https & !attlsServer") @ConditionalOnMissingBean(name = "modulithConfig") public class HttpWebSecurityConfig extends AbstractWebSecurityConfigurer { private static final String DISCOVERY_REALM = "API Mediation Discovery Service realm"; diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityLoginConfig.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityLoginConfig.java index ed2bcbf697..379c5c7376 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityLoginConfig.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpWebSecurityLoginConfig.java @@ -19,6 +19,6 @@ "org.zowe.apiml.security.common", "org.zowe.apiml.gateway.security.login" }) -@Profile("!https & !attls") +@Profile("!https & !attlsServer") public class HttpWebSecurityLoginConfig { } diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java index f62b7a9c57..1c6aa24282 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java @@ -47,17 +47,19 @@ @Configuration @RequiredArgsConstructor @EnableApimlAuth -@Profile({"https", "attls"}) +@Profile({"https", "attlsServer"}) @ConditionalOnMissingBean(name = "modulithConfig") public class HttpsWebSecurityConfig extends AbstractWebSecurityConfigurer { + private static final String DISCOVERY_REALM = "API Mediation Discovery Service realm"; + private final HandlerInitializer handlerInitializer; private final AuthConfigurationProperties securityConfigurationProperties; private final GatewayLoginProvider gatewayLoginProvider; private final GatewayTokenProvider gatewayTokenProvider; - private static final String DISCOVERY_REALM = "API Mediation Discovery Service realm"; - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; + + @Value("${server.attlsServer.enabled:false}") + private boolean isServerAttlsEnabled; @Value("${apiml.health.protected:true}") private boolean isHealthEndpointProtected; @@ -111,7 +113,7 @@ public SecurityFilterChain basicAuthOrTokenFilterChain(HttpSecurity http) throws .authorizeHttpRequests(requests -> requests .requestMatchers("/**").authenticated()) .httpBasic(basic -> basic.realmName(DISCOVERY_REALM)); - if (isAttlsEnabled) { + if (isServerAttlsEnabled) { http.addFilterBefore(new SecureConnectionFilter(), UsernamePasswordAuthenticationFilter.class); } @@ -131,7 +133,7 @@ public SecurityFilterChain clientCertificateFilterChain(HttpSecurity http) throw .authorizeHttpRequests(requests -> requests .anyRequest().authenticated() ); - if (isAttlsEnabled) { + if (isServerAttlsEnabled) { http.addFilterBefore(new AttlsFilter(), X509AuthenticationFilter.class); http.addFilterBefore(new SecureConnectionFilter(), AttlsFilter.class); } @@ -154,7 +156,7 @@ public SecurityFilterChain basicAuthOrTokenOrCertFilterChain(HttpSecurity http) if (verifySslCertificatesOfServices || !nonStrictVerifySslCertificatesOfServices) { http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated()) .x509(x509 -> x509.userDetailsService(x509UserDetailsService())); - if (isAttlsEnabled) { + if (isServerAttlsEnabled) { http.addFilterBefore(new AttlsFilter(), X509AuthenticationFilter.class); http.addFilterBefore(new SecureConnectionFilter(), AttlsFilter.class); } diff --git a/discovery-service/src/main/resources/application.yml b/discovery-service/src/main/resources/application.yml index 083994f712..31ddfe640d 100644 --- a/discovery-service/src/main/resources/application.yml +++ b/discovery-service/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + logging: level: ROOT: INFO @@ -186,15 +189,26 @@ management: logbackServiceName: ZWEADS1 --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: - attls: + attlsServer: enabled: true ssl: enabled: false +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + eureka: instance: nonSecurePortEnabled: true securePortEnabled: false + +apiml: + discovery: + allPeersUrls: http://${apiml.service.hostname}:${apiml.service.port}/eureka/ diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java index c7e4486fd2..a1a3b176eb 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java @@ -32,12 +32,12 @@ @TestPropertySource( properties = { - "server.attls.enabled=true", + "server.attlsServer.enabled=true", "server.ssl.enabled=false" } ) @TestInstance(Lifecycle.PER_CLASS) -@ActiveProfiles("attls") +@ActiveProfiles("attlsServer") @Import(TestConfig.class) class AttlsConfigTest extends DiscoveryFunctionalTest { diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 7e0e569375..a8e8b7d6a8 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -148,33 +148,40 @@ fi echo "Setting loader path: "${GATEWAY_LOADER_PATH} -ATTLS_ENABLED="false" +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + +ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" if [ "${ZWE_zowe_network_server_tls_attls}" = "true" ]; then - ATTLS_ENABLED="true" + ATTLS_SERVER_ENABLED="true" fi if [ "${ZWE_zowe_network_client_tls_attls}" = "true" ]; then ATTLS_CLIENT_ENABLED="true" fi -if [ "${ATTLS_ENABLED}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" ]; then + add_profile "attlsServer" ZWE_configs_server_ssl_enabled="false" - if [ -n "${ZWE_configs_spring_profiles_active}" ]; then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}attls" + ZWE_configs_apiml_service_corsEnabled=true fi internalProtocol="https" ZWE_DISCOVERY_SERVICES_LIST=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} if [ "${ATTLS_CLIENT_ENABLED}" = "true" ]; then + add_profile "attlsClient" ZWE_DISCOVERY_SERVICES_LIST=$(echo "${ZWE_DISCOVERY_SERVICES_LIST=}" | sed -e 's|https://|http://|g') internalProtocol=http ZWE_configs_apiml_service_corsEnabled=true fi -if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" -o "$ATTLS_ENABLED" = "true" ]; then +if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" -o "$ATTLS_SERVER_ENABLED" = "true" ]; then externalProtocol="https" else externalProtocol="http" @@ -272,7 +279,7 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" fi -if [ "${ATTLS_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then keystore_type= keystore_pass= key_pass= @@ -326,7 +333,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-true} \ -Dapiml.service.apimlId=${ZWE_configs_apimlId:-} \ -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-false} \ - -Dapiml.service.corsAllowedMethods=${ZWE_configs_apiml_service_corsAllowedMethods:-} \ + -Dapiml.service.corsAllowedMethods=${ZWE_configs_apiml_service_corsAllowedMethods:-GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS} \ -Dapiml.service.externalUrl="${externalProtocol}://${ZWE_zowe_externalDomains_0}:${ZWE_zowe_externalPort}" \ -Dapiml.service.forwardClientCertEnabled=${ZWE_configs_apiml_security_x509_enabled:-false} \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java index f18131f95b..b420d3e088 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java @@ -27,14 +27,14 @@ public class RegistryConfig { @Bean @ConditionalOnMissingBean - public BasicInfoService basicInfoService(EurekaClient eurekaClient, EurekaMetadataParser eurekaMetadataParser) { + BasicInfoService basicInfoService(EurekaClient eurekaClient, EurekaMetadataParser eurekaMetadataParser) { return new BasicInfoService(eurekaClient, eurekaMetadataParser); } @Bean - public ServiceAddress gatewayServiceAddress( + ServiceAddress gatewayServiceAddress( @Value("${apiml.service.externalUrl:#{null}}") String externalUrl, - @Value("${server.attls.enabled:false}") boolean attlsEnabled, + @Value("${server.attlsServer.enabled:false}") boolean serverAttlsEnabled, @Value("${server.ssl.enabled:true}") boolean sslEnabled, @Value("${apiml.service.hostname:localhost}") String hostname, @Value("${server.port}") int port @@ -48,7 +48,7 @@ public ServiceAddress gatewayServiceAddress( } return ServiceAddress.builder() - .scheme(attlsEnabled || sslEnabled ? "https" : "http") + .scheme(serverAttlsEnabled || sslEnabled ? "https" : "http") .hostname(hostname + ":" + port) .build(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java index 731d38d2b0..f6160cff96 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java @@ -75,8 +75,8 @@ ) public class SwaggerConfig { - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; + @Value("${server.attlsClient.enabled:false}") + private boolean isClientAttlsenabled; private final EurekaClient eurekaClient; private final WebClient webClient; @@ -93,7 +93,7 @@ void initEurekaListener() { .ifPresent(app -> { try { zaasUri = new URIBuilder() - .setScheme(isAttlsEnabled || app.isPortEnabled(InstanceInfo.PortType.SECURE) ? "https" : "http") + .setScheme(app.isPortEnabled(InstanceInfo.PortType.SECURE) && !isClientAttlsenabled ? "https" : "http") .setHost(app.getHostName()) .setPort(app.isPortEnabled(InstanceInfo.PortType.SECURE) ? app.getSecurePort() : app.getPort()) .setPath("/v3/api-docs/auth") diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 594a14b4e7..8f28d6b61a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -47,8 +47,8 @@ public class PageRedirectionFilterFactory extends AbstractGatewayFilterFactory

processNewLocationUrl(ServerWebExchange exchange, Config conf if (newUrl.get() != null) { // if the new URL was defined, decorate (scheme by AT-TLS) and set - if (isAttlsEnabled) { + if (isServerAttlsEnabled) { newUrl.set(UriComponentsBuilder.fromUriString(newUrl.get()).scheme("https").build().toUriString()); } diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 11f0387f6b..0ba7d2809e 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + eureka: instance: instanceId: ${apiml.service.hostname}:${apiml.service.id}:${apiml.service.port} @@ -191,10 +194,10 @@ logging: javax.net.ssl: ERROR --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: - attls: + attlsServer: enabled: true ssl: enabled: false @@ -207,3 +210,21 @@ apiml: scheme: http nonSecurePortEnabled: true securePortEnabled: false + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + +apiml: + service: + discoveryServiceUrls: http://localhost:10011/eureka/ + +eureka: + instance: + metadata-map: + apiml: + apiInfo[0]: + swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/gateway/api-docs diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index a032e2c6b4..f162bdc576 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -58,7 +58,7 @@ void setUp() { } private void commonSetup(PageRedirectionFilterFactory factory, ServerWebExchange exchange, ServerHttpResponse res, GatewayFilterChain chain, boolean isAttlsEnabled) { - ReflectionTestUtils.setField(factory, "isAttlsEnabled", isAttlsEnabled); + ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", isAttlsEnabled); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_MOVED_PERMANENTLY)); when(exchange.getResponse()).thenReturn(res); when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty()); diff --git a/mock-services/src/main/resources/application.yml b/mock-services/src/main/resources/application.yml index 4637c125fe..3118de48a2 100644 --- a/mock-services/src/main/resources/application.yml +++ b/mock-services/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + logging: level: ROOT: INFO @@ -46,13 +49,16 @@ zss: "[zoweson@zowe.com]": USER --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: attls: enabled: true ssl: enabled: false + +--- +spring.config.activate.on-profile: attlsClient apiml: service: scheme: http diff --git a/zaas-package/src/main/resources/bin/start.sh b/zaas-package/src/main/resources/bin/start.sh index 06fe863bbc..43ce21aa79 100755 --- a/zaas-package/src/main/resources/bin/start.sh +++ b/zaas-package/src/main/resources/bin/start.sh @@ -166,32 +166,38 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" fi -ATTLS_ENABLED="false" +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + +ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" if [ "${ZWE_zowe_network_server_tls_attls}" = "true" ]; then - ATTLS_ENABLED="true" + ATTLS_SERVER_ENABLED="true" fi if [ "${ZWE_zowe_network_client_tls_attls}" = "true" ]; then ATTLS_CLIENT_ENABLED="true" fi -if [ "${ATTLS_ENABLED}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" ]; then ZWE_configs_server_ssl_enabled="false" - if [ -n "${ZWE_configs_spring_profiles_active}" ]; then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}attls" + add_profile "attlsServer" fi internalProtocol="https" ZWE_DISCOVERY_SERVICES_LIST=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} if [ "${ATTLS_CLIENT_ENABLED}" = "true" ]; then + add_profile "attlsClient" ZWE_DISCOVERY_SERVICES_LIST=$(echo "${ZWE_DISCOVERY_SERVICES_LIST=}" | sed -e 's|https://|http://|g') internalProtocol=http fi -if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" -o "$ATTLS_ENABLED" = "true" ]; then +if [ "${ZWE_configs_server_ssl_enabled:-true}" = "true" -o "$ATTLS_SERVER_ENABLED" = "true" ]; then externalProtocol="https" else externalProtocol="http" @@ -285,7 +291,7 @@ elif [ "${keystore_type}" = "JCEHYBRIDRACFKS" ]; then truststore_location=$(echo "${truststore_location}" | sed s_safkeyring://_safkeyringjcehybrid://_) fi -if [ "${ATTLS_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then +if [ "${ATTLS_SERVER_ENABLED}" = "true" -a "${APIML_ATTLS_LOAD_KEYRING:-false}" = "true" ]; then keystore_type= keystore_pass= key_pass= diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java index ead04f2f1f..354e901b8b 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java @@ -111,8 +111,8 @@ public class NewSecurityConfiguration { private final AuthSourceService authSourceService; private final AuthExceptionHandler authExceptionHandler; - @Value("${server.attls.enabled:false}") - private boolean isAttlsEnabled; + @Value("${server.attlsServer.enabled:false}") + private boolean isServerAttlsEnabled; @Value("${apiml.health.protected:true}") private boolean isHealthEndpointProtected; @@ -518,7 +518,7 @@ public SecurityFilterChain certificateOrAuthEndpointsFilterChain(HttpSecurity ht ) .logout(AbstractHttpConfigurer::disable); // logout filter in this chain not needed - if (isAttlsEnabled) { + if (isServerAttlsEnabled) { http // filter out API ML certificate .addFilterBefore(reversedCategorizeCertFilter(), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class); @@ -636,7 +636,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { * Common configuration for all filterchains */ protected HttpSecurity baseConfigure(HttpSecurity http) throws Exception { - if (isAttlsEnabled) { + if (isServerAttlsEnabled) { http.addFilterBefore(new AttlsFilter(), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class); http.addFilterBefore(new SecureConnectionFilter(), AttlsFilter.class); } diff --git a/zaas-service/src/main/resources/application.yml b/zaas-service/src/main/resources/application.yml index dbd922531b..7174c4db8e 100644 --- a/zaas-service/src/main/resources/application.yml +++ b/zaas-service/src/main/resources/application.yml @@ -1,3 +1,6 @@ +# for back-compatibility +spring.profiles.group.attls: attlsServer,attlsClient + logging: level: ROOT: INFO @@ -245,25 +248,39 @@ spring.config.activate.on-profile: dev logbackServiceName: ZWEAZS1 --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: internal: ssl: enabled: false - attls: + attlsServer: enabled: true ssl: enabled: false - service: scheme: http + apiml: service: corsEnabled: true - scheme: http +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true +apiml: + security: + saf: + urls: + authenticate: http://localhost:10013/zss/saf/authenticate + verify: http://localhost:10013/zss/saf/verify + service: + corsEnabled: true + scheme: http eureka: instance: securePortEnabled: false diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java index d1f1e19a06..ff55a58635 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java @@ -40,7 +40,7 @@ @TestPropertySource( properties = { "server.internal.ssl.enabled=false", - "server.attls.enabled=true", + "server.attlsServer.enabled=true", "server.ssl.enabled=false", "server.service.scheme=http" } From b17896fd3c4f026d770be0627d163cf91c3a303f Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 19 Aug 2025 14:38:16 +0200 Subject: [PATCH 069/152] fix: AT-TLS mode without reading keystore (#4271) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../config/SecurityConfiguration.java | 16 +- .../apicatalog/swagger/ContainerService.java | 4 +- .../functional/AttlsConfigTest.java | 156 ++++++++++------ .../src/test/resources/application.yml | 35 +++- .../web/DiscoveryRestTemplateConfig.java | 11 +- .../zowe/apiml/product/web/HttpConfig.java | 40 ++-- apiml-security-common/build.gradle | 1 + .../common/config/WebClientConfig.java | 11 +- .../security/common/util/ConnectionUtil.java | 24 ++- .../common/util/ConnectionUtilTest.java | 117 ++++++++++++ .../product/web/ApimlTomcatCustomizer.java | 6 +- .../java/org/zowe/apiml/ApimlApplication.java | 1 + .../java/org/zowe/apiml/ModulithConfig.java | 39 +++- apiml/src/main/resources/application.yml | 27 ++- .../apiml/acceptance/AttlsConfigTest.java | 173 +++++++++++++++++ apiml/src/test/resources/application.yml | 29 +++ .../infinispan/config/InfinispanConfig.java | 67 +++++-- .../src/main/resources/infinispan-attls.xml | 80 ++++++++ .../apiml/caching/config/AttlsConfigTest.java | 118 ++++++++---- .../src/test/resources/application.yml | 30 +++ .../org/zowe/apiml/security/HttpsFactory.java | 1 - .../zosmf-static-definition.yaml.template | 4 +- .../config/HttpsWebSecurityConfig.java | 10 +- .../discovery/config/AttlsConfigTest.java | 81 ++++---- .../src/test/resources/application.yml | 31 ++++ .../gateway/GatewayServiceApplication.java | 1 + .../caching/CachingServiceClientRest.java | 33 +++- .../gateway/caching/LoadBalancerCache.java | 4 +- .../gateway/config/ConnectionsConfig.java | 11 +- .../apiml/gateway/config/RegistryConfig.java | 20 +- .../gateway/config/ServiceCorsUpdater.java | 8 +- .../apiml/gateway/config/WebSecurity.java | 7 +- .../src/main/resources/application.yml | 44 +++-- .../gateway/acceptance/AttlsConfigTest.java | 175 ++++++++++++++++++ .../gateway/config/RegistryConfigTest.java | 19 +- .../config/ServiceCorsUpdaterTest.java | 4 +- .../src/test/resources/application.yml | 42 ++++- .../proxy/MultipartPutIntegrationTest.java | 6 +- .../config/NewSecurityConfiguration.java | 22 ++- .../zaas/security/config/AttlsConfigTest.java | 143 ++++++++------ .../src/test/resources/application.yml | 45 +++++ 41 files changed, 1368 insertions(+), 328 deletions(-) create mode 100644 apiml-security-common/src/test/java/org/zowe/apiml/security/common/util/ConnectionUtilTest.java create mode 100644 apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java create mode 100644 caching-service/src/main/resources/infinispan-attls.xml create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java index 77b2d8a02a..09c8090e25 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java @@ -118,7 +118,7 @@ private String[] getFullUrls(String...baseUrl) { @Bean @Order(1) @ConditionalOnMissingBean(name = "modulithConfig") - public SecurityWebFilterChain loginSecurityWebFilterChain(ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint) { + SecurityWebFilterChain loginSecurityWebFilterChain(ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint) { return baseConfiguration(http, serverAuthenticationEntryPoint) .securityMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, getFullUrls(authConfigurationProperties.getServiceLoginEndpoint()))) .authorizeExchange(exchange -> exchange @@ -130,7 +130,7 @@ public SecurityWebFilterChain loginSecurityWebFilterChain(ServerHttpSecurity htt @Bean @Order(2) @ConditionalOnMissingBean(name = "modulithConfig") - public SecurityWebFilterChain logoutSecurityWebFilterChain(ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint) { + SecurityWebFilterChain logoutSecurityWebFilterChain(ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint) { return baseConfiguration(http, serverAuthenticationEntryPoint) .securityMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, getFullUrls(authConfigurationProperties.getServiceLogoutEndpoint()))) .logout(logout -> logout @@ -145,7 +145,7 @@ public SecurityWebFilterChain logoutSecurityWebFilterChain(ServerHttpSecurity ht */ @Bean @Order(3) - public SecurityWebFilterChain basicAuthOrTokenOrCertApiDocFilterChain( + SecurityWebFilterChain basicAuthOrTokenOrCertApiDocFilterChain( ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint ) { @@ -168,7 +168,7 @@ public SecurityWebFilterChain basicAuthOrTokenOrCertApiDocFilterChain( @Bean @Order(4) - public SecurityWebFilterChain healthEndpointSecurityWebFilterChain( + SecurityWebFilterChain healthEndpointSecurityWebFilterChain( ServerHttpSecurity http, @Value("${apiml.health.protected:true}") boolean isHealthEndpointProtected, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint @@ -192,7 +192,7 @@ public SecurityWebFilterChain healthEndpointSecurityWebFilterChain( @Bean @Order(5) - public SecurityWebFilterChain basicAuthOrTokenAllEndpointsFilterChain( + SecurityWebFilterChain basicAuthOrTokenAllEndpointsFilterChain( ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint ) { @@ -210,7 +210,7 @@ public SecurityWebFilterChain basicAuthOrTokenAllEndpointsFilterChain( */ @Bean @Order(6) - public SecurityWebFilterChain webSecurityCustomizer( + SecurityWebFilterChain webSecurityCustomizer( ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint ) { @@ -360,12 +360,12 @@ WebFilter oidcAuthenticationFilter( } @Bean - public ReactiveAuthenticationManager reactiveAuthenticationManager() { + ReactiveAuthenticationManager reactiveAuthenticationManager() { return Mono::just; } @Bean - public ServerAuthenticationEntryPoint serverAuthenticationEntryPoint( + ServerAuthenticationEntryPoint serverAuthenticationEntryPoint( MessageService messageService, ObjectMapper mapper ) { diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java index efa006d8da..d5448ce8dc 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java @@ -63,7 +63,7 @@ public class ContainerService { private boolean hideServiceInfo; @Value("${server.attlsClient.enabled:false}") - private boolean isAttlsEnabled; + private boolean isClientAttlsEnabled; @InjectApimlLogger private final ApimlLogger apimlLog = ApimlLogger.empty(); @@ -141,7 +141,7 @@ private String getInstanceHomePageUrl(ServiceInstance serviceInstance) { serviceId, instanceHomePage, routes, - isAttlsEnabled); + isClientAttlsEnabled); } catch (URLTransformationException | IllegalArgumentException e) { apimlLog.log("org.zowe.apiml.apicatalog.homePageTransformFailed", serviceId, e.getMessage()); } 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 95559747df..e97b0efc50 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 @@ -16,86 +16,138 @@ import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.core.Appender; import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.apicatalog.ApiCatalogApplication; import org.zowe.apiml.filter.AttlsHttpHandler; +import org.zowe.apiml.product.web.ApimlTomcatCustomizer; import javax.net.ssl.SSLException; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.core.StringContains.containsString; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -@TestPropertySource( - properties = { - "server.attlsServer.enabled=true", - "server.ssl.enabled=false" - } -) -@DirtiesContext -@ActiveProfiles("AttlsConfigTestCatalog") -public class AttlsConfigTest extends ApiCatalogFunctionalTest { +@TestInstance(Lifecycle.PER_CLASS) +class AttlsConfigTest { @Nested - class GivenAttlsModeEnabled { - - @Nested - class WhenContextLoads { - - @Mock - private Appender mockedAppender; - - @Captor - private ArgumentCaptor loggingEventCaptor; - - @Test - void requestFailsWithHttps() { - try { - given() - .log().all() - .when() - .get(getCatalogUriWithPath("apicatalog/containers")) - .then() - .log().all() - .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); - fail("Expected an SSL failure"); - } catch (Exception e) { - assertInstanceOf(SSLException.class, e); - } - } - - @Test - void requestFailsWithAttlsContextReasonWithHttp() { - var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); - logger.addAppender(mockedAppender); - logger.setLevel(Level.ERROR); + @DirtiesContext + @ActiveProfiles({"AttlsConfigTestCatalog", "attlsServer", "attlsClient"}) + class GivenAttlsModeEnabled extends ApiCatalogFunctionalTest { + + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + @Test + void requestFailsWithHttps() { + assertThrows(SSLException.class, () -> { given() .log().all() .when() - .get(getCatalogUriWithPath("http", "apicatalog/containers")) + .get(getCatalogUriWithPath("apicatalog/containers")) .then() - .log().all() - .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) - .body(containsString("org.zowe.apiml.common.internalServerError")); + .log().all(); + }); + } + + @Test + void requestFailsWithAttlsContextReasonWithHttp() { + var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); + + given() + .log().all() + .when() + .get(getCatalogUriWithPath("http", "apicatalog/containers")) + .then() + .log().all() + .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .body(containsString("org.zowe.apiml.common.internalServerError")); + + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); + } + + } - verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); - assertThat(loggingEventCaptor.getAllValues()) - .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) - .isNotEmpty(); - } + @Nested + @TestPropertySource( + properties = { + "server.ssl.keyStoreType=", + "server.ssl.keyStorePassword=", + "server.ssl.keyPassword=", + "server.ssl.keyAlias=", + "server.ssl.keyStore=" + } + ) + @ActiveProfiles({ "attlsServer", "attlsClient", "debug" }) + @DirtiesContext + @SpringBootTest( + classes = ApiCatalogApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenSslDisabled extends ApiCatalogFunctionalTest { + + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + + @MockitoBean + private ApimlTomcatCustomizer apimlTomcatCustomizer; + + @BeforeEach + @Override + void setUp() { + doNothing().when(apimlTomcatCustomizer).customize(any()); + } + @Test + void whenNoKeystore_thenStartupSuccess() { + var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); + + given() + .log().all() + .when() + .get(getCatalogUriWithPath("http", "apicatalog/containers")) + .then() + .log().all() + .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .body(containsString("org.zowe.apiml.common.internalServerError")); + + verify(apimlTomcatCustomizer, times(1)).customize(any()); + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); } } diff --git a/api-catalog-services/src/test/resources/application.yml b/api-catalog-services/src/test/resources/application.yml index 2666f90cf0..dbb58ffd66 100644 --- a/api-catalog-services/src/test/resources/application.yml +++ b/api-catalog-services/src/test/resources/application.yml @@ -189,9 +189,9 @@ management: logging: level: - ROOT: INFO + ROOT: DEBUG org.zowe.apiml: DEBUG - org.springframework: INFO + org.springframework: DEBUG org.apache: INFO org.apache.http: DEBUG com.netflix: INFO @@ -211,13 +211,34 @@ management: spring.config.activate.on-profile: dev --- -spring.config.activate.on-profile: attls +spring.config.activate.on-profile: attlsServer server: - attls: + attlsServer: enabled: true ssl: enabled: false + +eureka: + instance: + secureHealthCheckUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/application/health + metadata-map: + apiml: + corsEnabled: true + corsAllowedOrigins: https://${apiml.service.hostname}:${apiml.service.port},${apiml.service.externalUrl} + apiInfo: + - apiId: zowe.apiml.apicatalog + version: 1.0.0 + gatewayUrl: api/v1 + swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + eureka: instance: securePort: 0 @@ -227,13 +248,9 @@ eureka: metadata-map: apiml: corsEnabled: true + corsAllowedOrigins: https://${apiml.service.hostname}:${apiml.service.port},${apiml.service.externalUrl} apiInfo: - apiId: zowe.apiml.apicatalog version: 1.0.0 gatewayUrl: api/v1 swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/apicatalog/v3/api-docs -apiml: - service: - scheme: http - nonSecurePortEnabled: true - securePortEnabled: false diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/web/DiscoveryRestTemplateConfig.java b/apiml-common/src/main/java/org/zowe/apiml/product/web/DiscoveryRestTemplateConfig.java index 888c7fb61e..04bc6d425d 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/web/DiscoveryRestTemplateConfig.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/web/DiscoveryRestTemplateConfig.java @@ -30,20 +30,25 @@ public class DiscoveryRestTemplateConfig { private static final ApimlLogger apimlLog = ApimlLogger.of(DiscoveryRestTemplateConfig.class, YamlMessageServiceInstance.getInstance()); + @Value("${server.attlsClient.enabled:false}") + private boolean isClientAttlsEnabled; + @Bean - public RestClientTransportClientFactories restTemplateTransportClientFactories(RestClientDiscoveryClientOptionalArgs restClientDiscoveryClientOptionalArgs) { + RestClientTransportClientFactories restTemplateTransportClientFactories(RestClientDiscoveryClientOptionalArgs restClientDiscoveryClientOptionalArgs) { return new RestClientTransportClientFactories(restClientDiscoveryClientOptionalArgs); } @Bean - public RestClientDiscoveryClientOptionalArgs defaultArgs(@Value("${eureka.client.serviceUrl.defaultZone}") String eurekaServerUrl, + RestClientDiscoveryClientOptionalArgs defaultArgs(@Value("${eureka.client.serviceUrl.defaultZone}") String eurekaServerUrl, @Qualifier("secureSslContext") SSLContext secureSslContext, HostnameVerifier secureHostnameVerifier ) { RestClientDiscoveryClientOptionalArgs clientArgs = new RestClientDiscoveryClientOptionalArgs(getDefaultEurekaClientHttpRequestFactorySupplier(), RestClient::builder); if (eurekaServerUrl.startsWith("http://")) { - apimlLog.log("org.zowe.apiml.common.insecureHttpWarning"); + if (!isClientAttlsEnabled) { + apimlLog.log("org.zowe.apiml.common.insecureHttpWarning"); + } } else { clientArgs.setSSLContext(secureSslContext); clientArgs.setHostnameVerifier(secureHostnameVerifier); diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java index d000a8a8e7..be82b6a3bb 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java @@ -10,10 +10,10 @@ package org.zowe.apiml.product.web; -import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.socket.ConnectionSocketFactory; @@ -21,6 +21,7 @@ import org.apache.hc.core5.http.config.Registry; import org.apache.hc.core5.http.config.RegistryBuilder; import org.apache.hc.core5.util.Timeout; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.ApplicationContext; @@ -37,7 +38,7 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; -import java.security.KeyStore; + import java.security.cert.X509Certificate; import java.util.Set; import java.util.Timer; @@ -48,14 +49,16 @@ @Configuration @RequiredArgsConstructor @Getter -public class HttpConfig { +public class HttpConfig implements InitializingBean { private static final char[] KEYRING_PASSWORD = "password".toCharArray(); @Value("${server.ssl.protocol:TLSv1.2}") private String protocol; + @Value("${apiml.httpclient.ssl.enabled-protocols:TLSv1.2,TLSv1.3}") private String[] supportedProtocols; + @Value("${server.ssl.ciphers:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384}") private String[] ciphers; @@ -100,12 +103,15 @@ public class HttpConfig { @Value("${apiml.connection.idleConnectionTimeoutSeconds:#{5}}") private int idleConnTimeoutSeconds; + @Value("${apiml.connection.timeout:#{60000}}") private int requestConnectionTimeout; + @Value("${apiml.connection.timeToLive:#{60000}}") private int timeToLive; - private final Timer connectionManagerTimer = new Timer( - "ApimlHttpClientConfiguration.connectionManagerTimer", true); + + private final Timer connectionManagerTimer = new Timer("ApimlHttpClientConfiguration.connectionManagerTimer", true); + private CloseableHttpClient secureHttpClient; private CloseableHttpClient secureHttpClientWithoutKeystore; private HttpsConfig httpsConfig; @@ -130,14 +136,18 @@ void updateStorePaths() { } } - @PostConstruct + @Override + public void afterPropertiesSet() throws Exception { + init(); + } + public void init() { updateStorePaths(); try { X509Certificate certificate = null; - if (keyStorePath != null) { - KeyStore ks = SecurityUtils.loadKeyStore(keyStoreType, keyStorePath, keyStorePassword); + if (StringUtils.isNotBlank(keyStorePath)) { + var ks = SecurityUtils.loadKeyStore(keyStoreType, keyStorePath, keyStorePassword); certificate = (X509Certificate) ks.getCertificate(keyAlias); } Supplier httpsConfigSupplier = () -> @@ -207,7 +217,7 @@ public void run() { } @Bean - public Set publicKeyCertificatesBase64() { + Set publicKeyCertificatesBase64() { return publicKeyCertificatesBase64; } @@ -229,7 +239,7 @@ public HttpsFactory httpsFactory() { */ @Bean @Primary - public RestTemplate restTemplateWithKeystore() { + RestTemplate restTemplateWithKeystore() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(secureHttpClient); factory.setConnectionRequestTimeout(requestConnectionTimeout); factory.setConnectTimeout(requestConnectionTimeout); @@ -244,7 +254,7 @@ public RestTemplate restTemplateWithKeystore() { * @return default RestTemplate, which doesn't use certificate from keystore */ @Bean - public RestTemplate restTemplateWithoutKeystore() { + RestTemplate restTemplateWithoutKeystore() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(secureHttpClientWithoutKeystore); factory.setConnectionRequestTimeout(requestConnectionTimeout); factory.setConnectTimeout(requestConnectionTimeout); @@ -256,7 +266,7 @@ public RestTemplate restTemplateWithoutKeystore() { */ @Bean("secureHttpClientWithKeystore") @Primary - public CloseableHttpClient secureHttpClient() { + CloseableHttpClient secureHttpClient() { return secureHttpClient; } @@ -264,7 +274,7 @@ public CloseableHttpClient secureHttpClient() { * @return HttpClient, which doesn't use a certificate to authenticate */ @Bean - public CloseableHttpClient secureHttpClientWithoutKeystore() { + CloseableHttpClient secureHttpClientWithoutKeystore() { return secureHttpClientWithoutKeystore; } @@ -274,12 +284,12 @@ public SSLContext secureSslContext() { } @Bean - public SSLContext secureSslContextWithoutKeystore() { + SSLContext secureSslContextWithoutKeystore() { return secureSslContextWithoutKeystore; } @Bean - public HostnameVerifier secureHostnameVerifier() { + HostnameVerifier secureHostnameVerifier() { return secureHostnameVerifier; } diff --git a/apiml-security-common/build.gradle b/apiml-security-common/build.gradle index 4710ab1546..0efdd1923d 100644 --- a/apiml-security-common/build.gradle +++ b/apiml-security-common/build.gradle @@ -19,5 +19,6 @@ dependencies { annotationProcessor libs.lombok testCompileOnly libs.lombok + testImplementation libs.netty.reactor.http testAnnotationProcessor libs.lombok } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java index 68ddf3da9b..3b15995e64 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java @@ -14,6 +14,8 @@ import io.netty.resolver.DefaultAddressResolverGroup; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.cloud.gateway.config.HttpClientCustomizer; @@ -40,9 +42,12 @@ @ConditionalOnProperty(name = "apiml.webClientConfig.enabled", havingValue = "true") public class WebClientConfig { + private static final ApimlLogger apimlLog = ApimlLogger.of(WebClientConfig.class, YamlMessageServiceInstance.getInstance()); + private final HttpConfig config; - private static final ApimlLogger apimlLog = ApimlLogger.of(WebClientConfig.class, YamlMessageServiceInstance.getInstance()); + @Value("${server.attlsClient.enabled:false}") + private boolean isClientAttlsEnabled; @Bean HttpClientFactory gatewayHttpClientFactory( @@ -83,13 +88,15 @@ HttpClient getHttpClient(HttpClient httpClient, boolean useClientCert) { WebClient webClient(HttpClient httpClient) { return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, false))) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) .build(); } @Bean WebClient webClientClientCert(HttpClient httpClient) { + boolean isKeyLoadPrevented = StringUtils.isBlank(config.getKeyStorePath()) && isClientAttlsEnabled; return WebClient.builder() - .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, true))) + .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, !isKeyLoadPrevented))) .build(); } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java index e096e0f9ba..612493416d 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/ConnectionUtil.java @@ -10,6 +10,7 @@ package org.zowe.apiml.security.common.util; +import com.google.common.annotations.VisibleForTesting; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import lombok.experimental.UtilityClass; @@ -23,12 +24,17 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509KeyManager; + import java.io.IOException; import java.net.Socket; -import java.security.*; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import com.google.common.annotations.VisibleForTesting; @UtilityClass @Slf4j @@ -38,23 +44,23 @@ public class ConnectionUtil { * @return io.netty.handler.ssl.SslContext for http client. */ public SslContext getSslContext(HttpConfig config, boolean setKeystore) throws CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException { - SslContextBuilder builder = SslContextBuilder.forClient(); + var builder = SslContextBuilder.forClient(); - KeyStore trustStore = SecurityUtils.loadKeyStore( - config.getTrustStoreType(), config.getTrustStorePath(), config.getTrustStorePassword()); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + var trustStore = SecurityUtils.loadKeyStore(config.getTrustStoreType(), config.getTrustStorePath(), config.getTrustStorePassword()); + var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(trustStore); builder.trustManager(trustManagerFactory); - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (setKeystore) { log.info("Loading keystore: {}: {}", config.getKeyStoreType(), config.getKeyStorePath()); - KeyStore keyStore = SecurityUtils.loadKeyStore( + var keyStore = SecurityUtils.loadKeyStore( config.getKeyStoreType(), config.getKeyStorePath(), config.getKeyStorePassword()); keyManagerFactory.init(keyStore, config.getKeyStorePassword()); builder.keyManager(x509KeyManagerSelectedAlias(config, keyManagerFactory)); } else { - KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); + var emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); emptyKeystore.load(null, null); keyManagerFactory.init(emptyKeystore, null); builder.keyManager(keyManagerFactory); diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/util/ConnectionUtilTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/util/ConnectionUtilTest.java new file mode 100644 index 0000000000..e0d81d1165 --- /dev/null +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/util/ConnectionUtilTest.java @@ -0,0 +1,117 @@ +/* + * 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.security.common.util; + +import io.netty.handler.ssl.SslContextBuilder; +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.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.product.web.HttpConfig; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509KeyManager; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ConnectionUtilTest { + + @Mock private HttpConfig httpConfig; + @Mock private SslContextBuilder builder; + + @BeforeEach + void setUp() { + when(httpConfig.getTrustStoreType()).thenReturn("PKCS12"); + when(httpConfig.getTrustStorePath()).thenReturn("../keystore/localhost/localhost.truststore.p12"); + when(httpConfig.getTrustStorePassword()).thenReturn("password".toCharArray()); //NOSONAR + } + + @Test + void onGetSslContextWithKeystore_thenUse() throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + when(httpConfig.getKeyStoreType()).thenReturn("PKCS12"); + when(httpConfig.getKeyStorePath()).thenReturn("../keystore/localhost/localhost.keystore.p12"); + when(httpConfig.getKeyStorePassword()).thenReturn("password".toCharArray()); //NOSONAR + + try (MockedStatic sslContextBuilder = Mockito.mockStatic(SslContextBuilder.class)) { + sslContextBuilder.when(SslContextBuilder::forClient).thenReturn(builder); + + ConnectionUtil.getSslContext(httpConfig, true); + + verify(builder, times(1)).trustManager(any(TrustManagerFactory.class)); + verify(builder, never()).endpointIdentificationAlgorithm(any()); + verify(builder, times(1)).keyManager(any(X509KeyManager.class)); + verify(builder, times(1)).keyManager((X509KeyManager) argThat(x509KeyManager -> { + var m = (X509KeyManager) x509KeyManager; + assertNotNull(m.getPrivateKey("localhost")); + assertTrue(m.getCertificateChain("localhost").length > 0); + return true; + })); + + } + + } + + @Test + void onGetSslContextWithoutKeystore_thenEmpty() throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + try (MockedStatic sslContextBuilder = Mockito.mockStatic(SslContextBuilder.class)) { + sslContextBuilder.when(SslContextBuilder::forClient).thenReturn(builder); + + ConnectionUtil.getSslContext(httpConfig, false); + + verify(builder, times(1)).trustManager(any(TrustManagerFactory.class)); + verify(builder, never()).endpointIdentificationAlgorithm(any()); + verify(builder, times(1)).keyManager(any(KeyManagerFactory.class)); + verify(builder, times(1)).keyManager((KeyManagerFactory) argThat(keyManagerFactory -> { + var f = (KeyManagerFactory) keyManagerFactory; + assertTrue(f.getKeyManagers().length > 0); + return true; + })); + + } + + } + + @Test + void whenNonStrict_thenDisableEndpointIdentificationAlgorithm() throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + when(httpConfig.isVerifySslCertificatesOfServices()).thenReturn(true); + when(httpConfig.isNonStrictVerifySslCertificatesOfServices()).thenReturn(true); + + try (MockedStatic sslContextBuilder = Mockito.mockStatic(SslContextBuilder.class)) { + sslContextBuilder.when(SslContextBuilder::forClient).thenReturn(builder); + + ConnectionUtil.getSslContext(httpConfig, false); + + verify(builder, times(1)).trustManager(any(TrustManagerFactory.class)); + verify(builder, times(1)).endpointIdentificationAlgorithm(isNull()); + } + + } + +} diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java index c1894d1327..124efc9cb0 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java @@ -10,13 +10,13 @@ package org.zowe.apiml.product.web; -import jakarta.annotation.PostConstruct; import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.Connector; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.http11.Http11NioProtocol; import org.apache.tomcat.util.net.*; +import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.stereotype.Component; @@ -34,11 +34,11 @@ @Slf4j @Component @ConditionalOnProperty(name = "server.attlsServer.enabled", havingValue = "true") -public class ApimlTomcatCustomizer implements TomcatConnectorCustomizer { +public class ApimlTomcatCustomizer implements TomcatConnectorCustomizer, InitializingBean { private static final String INCOMPATIBLE_VERSION_MESSAGE = "AT-TLS-Incompatible configuration. Verify AT-TLS requirements: Java version, Tomcat version. Exception message: "; - @PostConstruct + @Override public void afterPropertiesSet() { log.debug("AT-TLS mode is enabled"); InboundAttls.setAlwaysLoadCertificate(true); diff --git a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java index b326705a62..e3eaa5ec8e 100644 --- a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java +++ b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java @@ -24,6 +24,7 @@ @SpringBootApplication( exclude = { ReactiveOAuth2ClientAutoConfiguration.class }, scanBasePackages = { + "org.zowe.apiml.filter", "org.zowe.apiml.gateway", "org.zowe.apiml.product.web", "org.zowe.apiml.product.gateway", diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index b0efc14082..25e31d755d 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -16,16 +16,23 @@ import com.netflix.discovery.CacheRefreshedEvent; import com.netflix.discovery.EurekaClientConfig; import com.netflix.discovery.shared.Application; +import com.netflix.discovery.shared.Applications; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; -import jakarta.annotation.PostConstruct; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.Context; import org.apache.catalina.Host; import org.apache.catalina.connector.Connector; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -65,7 +72,15 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Timer; +import java.util.TimerTask; import static org.zowe.apiml.services.ServiceInfoUtils.getInstances; import static org.zowe.apiml.services.ServiceInfoUtils.getStatus; @@ -76,7 +91,7 @@ @EnableConfigurationProperties @DependsOn(value = {"gatewayHealthIndicator"}) @Slf4j -public class ModulithConfig { +public class ModulithConfig implements InitializingBean { private final ApplicationContext applicationContext; private final Map instances = new HashMap<>(); @@ -155,7 +170,11 @@ static ApimlInstanceRegistry getRegistry() { .orElse(null); } - @PostConstruct + @Override + public void afterPropertiesSet() throws Exception { + createLocalInstances(); + } + void createLocalInstances() { instances.put(CoreService.GATEWAY.getServiceId(), getInstanceInfo(CoreService.GATEWAY.getServiceId())); instances.put(CoreService.DISCOVERY.getServiceId(), getInstanceInfo(CoreService.DISCOVERY.getServiceId())); @@ -252,11 +271,11 @@ public List getServices() { if (registry == null) { return Collections.emptyList(); } - return registry.getApplications().getRegisteredApplications() - .stream() - .map(Application::getName) - .distinct() - .toList(); + + return Optional.ofNullable(registry.getApplications()) + .map(Applications::getRegisteredApplications) + .map(applications -> applications.stream().map(Application::getName).distinct().toList()) + .orElse(List.of()); } }; } diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 21c177415b..0e0555b656 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -331,9 +331,12 @@ spring.config.activate.on-profile: debug logging: level: + org.springframework.cloud.gateway: DEBUG + org.springframework.web.reactive: DEBUG + org.springframework.web.reactive.socket: DEBUG com.netflix: INFO # Update to DEBUG - com.netflix.eureka: DEBUG com.netflix.discovery.shared.transport.decorator: DEBUG + com.netflix.eureka: DEBUG com.netflix.eureka.cluster: DEBUG com.sun.jersey.server.impl.application.WebApplicationImpl: INFO javax.net.ssl: ERROR @@ -342,22 +345,30 @@ logging: org.apache.tomcat.util.net: DEBUG org.apache.tomcat.util.net.jsse.JSSESupport: INFO org.ehcache: INFO + org.infinispan: DEBUG + org.jgroups: DEBUG org.springframework: DEBUG - # org.springframework.cloud.gateway: TRACE - # org.springframework.cloud.gateway.filter: TRACE - # org.springframework.cloud.gateway.route: TRACE org.springframework.context.support.[PostProcessorRegistrationDelegate$BeanPostProcessorChecker]: DEBUG org.springframework.security: TRACE org.springframework.security.web: TRACE - # org.springframework.web.reactive: DEBUG - # org.springframework.web.reactive.socket: DEBUG org.zowe.apiml: DEBUG reactor.netty: DEBUG reactor.netty.http.client: DEBUG reactor.netty.http.client.HttpClient: DEBUG reactor.netty.http.client.HttpClientConnect: DEBUG - org.jgroups: DEBUG - org.infinispan: DEBUG + +--- +spring.config.activate.on-profile: wiretap + +spring: + cloud: + gateway: + server: + webflux: + httpserver: + wiretap: true + httpclient: + wiretap: true --- spring.config.activate.on-profile: attlsServer diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java new file mode 100644 index 0000000000..8e49f922a8 --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java @@ -0,0 +1,173 @@ +/* + * 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.acceptance; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import com.netflix.discovery.shared.Applications; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +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.web.server.LocalServerPort; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.ApimlApplication; +import org.zowe.apiml.discovery.ApimlInstanceRegistry; +import org.zowe.apiml.filter.AttlsHttpHandler; +import org.zowe.apiml.product.web.ApimlTomcatCustomizer; + +import javax.net.ssl.SSLException; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@TestInstance(Lifecycle.PER_CLASS) +class AttlsConfigTest { + + private String getGatewayUrlWithPath(String hostname, int port, String scheme, String path) { + return String.format("%s://%s:%d/%s", scheme, hostname, port, path); + } + + @Nested + @ActiveProfiles({ "attlsClient", "attlsServer" }) + @DirtiesContext + @SpringBootTest( + classes = ApimlApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenAttlsProfile { + + @LocalServerPort + private int port; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + + @MockitoBean + private ApimlTomcatCustomizer apimlTomcatCustomizer; + + @Test + void whenContextloads_requestFailsWithHttps() { + assertThrows(SSLException.class, () -> { + given() + .log().all() + .when() + .get(getGatewayUrlWithPath(hostname, port, "https", "application/version")) + .then() + .log().all(); + }); + } + + @Test + void requestFailsWithAttlsReasonWithHttp() { + var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); + + // Prevent use of native code but verify it calls the customizer + doNothing().when(apimlTomcatCustomizer).customize(any()); + + given() + .log().all() + .when() + .get(getGatewayUrlWithPath(hostname, port, "http", "application/version")) + .then() + .log().all() + .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .body(containsString("org.zowe.apiml.common.internalServerError")); + + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); + } + + } + + /** + * This test intends to verify ICSF workaround (no keyring load) + */ + @Nested + @TestPropertySource( + properties = { + "server.ssl.keyStoreType=", + "server.ssl.keyStorePassword=", + "server.ssl.keyPassword=", + "server.ssl.keyAlias=", + "server.ssl.keyStore=" + } + ) + @ActiveProfiles({ "attlsServer", "attlsClient", "ApimlModulithAcceptanceTest" }) + @AcceptanceTest + class GivenSslDisabled { + + @MockitoBean + private AttlsHttpHandler attlsHttpHandler; + + @MockitoBean + private ApimlInstanceRegistry apimlInstanceRegistry; + + @LocalServerPort + private int port; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + + @BeforeEach + void setUp() { + when(apimlInstanceRegistry.getApplications()).thenReturn(new Applications()); + } + + @Test + void whenNoKeystore_thenStartupSuccess() { + given() + .log().all() + .when() + .get(getGatewayUrlWithPath(hostname, port, "http", "application/version")) + .then() + .statusCode(SC_OK); + + verify(attlsHttpHandler, times(1)).postProcessAfterInitialization(any(HttpHandler.class), any()); + } + + } + +} diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml index fd32179e63..fd8ae11935 100644 --- a/apiml/src/test/resources/application.yml +++ b/apiml/src/test/resources/application.yml @@ -1,6 +1,7 @@ logging: level: ROOT: DEBUG + org.springframework: DEBUG org.zowe.apiml: DEBUG eureka: @@ -134,3 +135,31 @@ spring: banner-mode: ${apiml.banner:"console"} web-application-type: reactive allow-bean-definition-overriding: true + +--- +spring.config.activate.on-profile: attlsServer + +server: + attlsServer: + enabled: true + ssl: + enabled: false + +apiml: + service: + corsEnabled: true + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + service: + scheme: http + +apiml: + service: + scheme: http + nonSecurePortEnabled: true + securePortEnabled: false diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java index bbdb18cf15..a39b532bcb 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java @@ -10,7 +10,6 @@ package org.zowe.apiml.caching.service.infinispan.config; -import jakarta.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; import org.infinispan.commons.api.CacheContainerAdmin; import org.infinispan.commons.dataconversion.MediaType; @@ -22,6 +21,7 @@ import org.infinispan.lock.api.ClusteredLock; import org.infinispan.lock.api.ClusteredLockManager; import org.infinispan.manager.DefaultCacheManager; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -36,6 +36,7 @@ import java.io.InputStream; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Optional; import static org.zowe.apiml.security.SecurityUtils.formatKeyringUrl; import static org.zowe.apiml.security.SecurityUtils.isKeyring; @@ -43,28 +44,41 @@ @Configuration @ConfigurationProperties(value = "caching.storage.infinispan") @ConditionalOnProperty(name = "caching.storage.mode", havingValue = "infinispan") -public class InfinispanConfig { +public class InfinispanConfig implements InitializingBean { + + private static final String SERVER_SSL_KEY_STORE_PASSWORD = "server.ssl.keyStorePassword"; + private static final String SERVER_SSL_KEY_STORE = "server.ssl.keyStore"; + private static final String SERVER_SSL_KEY_STORE_TYPE = "server.ssl.keyStoreType"; private static final String KEYRING_PASSWORD = "password"; @Value("${caching.storage.infinispan.initialHosts}") private String initialHosts; + @Value("${server.ssl.keyStoreType}") private String keyStoreType; + @Value("${server.ssl.keyStore}") private String keyStore; + @Value("${server.ssl.keyStorePassword}") private String keyStorePass; + @Value("${jgroups.bind.port}") private String port; + @Value("${jgroups.bind.address}") private String address; + @Value("${jgroups.keyExchange.port:7601}") private String keyExchangePort; + @Value("${jgroups.tcp.diag.enabled:false}") private String tcpDiagEnabled; - @PostConstruct + @Value("${server.attlsServer.enabled:false}") + private boolean isServerAttlsEnabled; + void updateKeyring() { if (isKeyring(keyStore)) { keyStore = formatKeyringUrl(keyStore); @@ -72,6 +86,11 @@ void updateKeyring() { } } + @Override + public void afterPropertiesSet() throws Exception { + updateKeyring(); + } + static String getRootFolder() { // using getenv().get is because of system compatibility (see non-case sensitive on Windows) String instanceId = System.getenv().get("ZWE_haInstance_id"); @@ -88,34 +107,48 @@ static String getRootFolder() { } @Bean(destroyMethod = "stop") - DefaultCacheManager cacheManager(ResourceLoader resourceLoader) { + synchronized DefaultCacheManager cacheManager(ResourceLoader resourceLoader) { System.setProperty("jgroups.tcpping.initial_hosts", initialHosts); System.setProperty("jgroups.bind.port", port); System.setProperty("jgroups.bind.address", address); System.setProperty("jgroups.keyExchange.port", keyExchangePort); - System.setProperty("server.ssl.keyStoreType", keyStoreType); - System.setProperty("server.ssl.keyStore", keyStore); - System.setProperty("server.ssl.keyStorePassword", keyStorePass); System.setProperty("jgroups.tcp.diag.enabled", String.valueOf(Boolean.parseBoolean(tcpDiagEnabled))); + + var oldKeyStoreType = Optional.ofNullable(System.getProperty(SERVER_SSL_KEY_STORE_TYPE)); + var oldKeyStore = Optional.ofNullable(System.getProperty(SERVER_SSL_KEY_STORE)); + var oldKeyStorePassword = Optional.ofNullable(System.getProperty(SERVER_SSL_KEY_STORE_PASSWORD)); + + if (!isServerAttlsEnabled) { + System.setProperty(SERVER_SSL_KEY_STORE_TYPE, keyStoreType); + System.setProperty(SERVER_SSL_KEY_STORE, keyStore); + System.setProperty(SERVER_SSL_KEY_STORE_PASSWORD, keyStorePass); + } + ConfigurationBuilderHolder holder; - try (InputStream configurationStream = resourceLoader.getResource( - "classpath:infinispan.xml").getInputStream()) { + var infinispanConfigFile = isServerAttlsEnabled ? "infinispan-attls.xml" : "infinispan.xml"; + try (InputStream configurationStream = resourceLoader.getResource("classpath:" + infinispanConfigFile).getInputStream()) { holder = new ParserRegistry().parse(configurationStream, MediaType.APPLICATION_XML); } catch (IOException e) { throw new InfinispanConfigException("Can't read configuration file", e); } holder.getGlobalConfigurationBuilder().globalState().persistentLocation(getRootFolder()).enable(); - holder.newConfigurationBuilder("default").persistence().passivation(true).addSoftIndexFileStore() + holder.newConfigurationBuilder("default") + .persistence() + .passivation(true) + .addSoftIndexFileStore() .shared(false); DefaultCacheManager cacheManager = new DefaultCacheManager(holder, true); ConfigurationBuilder builder = new ConfigurationBuilder(); - builder.clustering().cacheMode(CacheMode.REPL_SYNC) - .encoding().mediaType("application/x-jboss-marshalling"); + builder.clustering() + .cacheMode(CacheMode.REPL_SYNC) + .encoding() + .mediaType("application/x-jboss-marshalling"); - builder.persistence().passivation(true) + builder.persistence() + .passivation(true) .addSoftIndexFileStore() .shared(false); @@ -124,11 +157,15 @@ DefaultCacheManager cacheManager(ResourceLoader resourceLoader) { .withFlags(CacheContainerAdmin.AdminFlag.VOLATILE) .getOrCreateCache(cacheName, builder.build())); + oldKeyStoreType.ifPresent(kst -> System.setProperty(SERVER_SSL_KEY_STORE_TYPE, kst)); + oldKeyStore.ifPresent(ks -> System.setProperty(SERVER_SSL_KEY_STORE, ks)); + oldKeyStorePassword.ifPresent(p -> System.setProperty(SERVER_SSL_KEY_STORE_PASSWORD, p)); + return cacheManager; } @Bean - public ClusteredLock lock(DefaultCacheManager cacheManager) { + ClusteredLock lock(DefaultCacheManager cacheManager) { ClusteredLockManager clm = EmbeddedClusteredLockManagerFactory.from(cacheManager); clm.defineLock("zoweInvalidatedTokenLock"); return clm.get("zoweInvalidatedTokenLock"); @@ -136,7 +173,7 @@ public ClusteredLock lock(DefaultCacheManager cacheManager) { @Bean - public Storage storage(DefaultCacheManager cacheManager, ClusteredLock clusteredLock) { + Storage storage(DefaultCacheManager cacheManager, ClusteredLock clusteredLock) { return new InfinispanStorage(cacheManager.getCache("zoweCache"), cacheManager.getCache("zoweInvalidatedTokenCache"), clusteredLock); } diff --git a/caching-service/src/main/resources/infinispan-attls.xml b/caching-service/src/main/resources/infinispan-attls.xml new file mode 100644 index 0000000000..9353be28d2 --- /dev/null +++ b/caching-service/src/main/resources/infinispan-attls.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.zowe.apiml.caching.model.KeyValue + org.zowe.apiml.security.common.token.TokenAuthentication + org.zowe.apiml.security.common.token.TokenAuthentication$Type + java.util.HashMap + java.util.Arrays$ArrayList + java.security.cert.Certificate$CertificateRep + + + + + 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 2ffed8f5ca..2f91ef6209 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 @@ -39,61 +39,56 @@ import static io.restassured.RestAssured.given; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; -@SpringBootTest( - classes = CachingServiceApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT -) -@ActiveProfiles("AttlsConfigTestCachingService") -@TestPropertySource( - properties = { - "server.attlsServer.enabled=true", - "server.ssl.enabled=false", - "caching.storage.mode=inMemory" - } -) -@DirtiesContext @TestInstance(Lifecycle.PER_CLASS) class AttlsConfigTest { - @Value("${apiml.service.hostname:localhost}") - String hostname; - @LocalServerPort - int port; + private String getUri(String hostname, int port, String scheme) { + return String.format("%s://%s:%d/%s", scheme, hostname, port, "api/v1/cache"); + } + @SpringBootTest( + classes = CachingServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + @ActiveProfiles({ "AttlsConfigTestCachingService", "attlsClient", "attlsServer" }) + @TestPropertySource( + properties = { + "caching.storage.mode=inMemory" + } + ) + @DirtiesContext @Nested class GivenAttlsModeEnabled { + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + @Mock private Appender mockedAppender; @Captor private ArgumentCaptor loggingEventCaptor; - private String getUri(String scheme) { - return String.format("%s://%s:%d/%s", scheme, hostname, port, "api/v1/cache"); - } - @Nested class WhenContextLoads { @Test void requestFailsWithHttps() { - try { + assertThrows(SSLException.class, () -> { given() .config(SslContext.clientCertUnknownUser) .header("Content-type", "application/json") - .get(getUri("https")) - .then() - .statusCode(HttpStatus.FORBIDDEN.value()); - fail(""); - } catch (Exception e) { - assertInstanceOf(SSLException.class, e); - } + .when() + .get(getUri(hostname, port, "https")) + .then() + .log().all(); + }); } @Test @@ -104,8 +99,9 @@ void requestFailsWithAttlsReasonWithHttp() { given() .config(SslContext.clientCertUnknownUser) .header("Content-type", "application/json") - .get(getUri("http")) - .then() + .when() + .get(getUri(hostname, port, "http")) + .then() .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) .body(containsString("org.zowe.apiml.common.internalServerError")); @@ -119,4 +115,60 @@ void requestFailsWithAttlsReasonWithHttp() { } + /** + * This test intends to verify ICSF workaround (no keyring load) + */ + @Nested + @TestPropertySource( + properties = { + "server.ssl.keyStoreType=", + "server.ssl.keyStorePassword=", + "server.ssl.keyPassword=", + "server.ssl.keyAlias=", + "server.ssl.keyStore=", + "apiml.service.discoveryServiceUrls=http://localhost:10011/eureka/" // Caching-service loads onboarding-enabler, which validates SSL configuration for Eureka client if it starts in https + } + ) + @ActiveProfiles({ "attlsClient", "attlsServer" }) + @DirtiesContext + @SpringBootTest( + classes = CachingServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenSslDisabled { + + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + + @Test + void whenNoKeystore_thenStartupSuccess() { + var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); + + given() + .log().all() + .when() + .get(getUri(hostname, port, "http")) + .then() + .log().all() + .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .body(containsString("org.zowe.apiml.common.internalServerError")); + + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); + } + + } + } diff --git a/caching-service/src/test/resources/application.yml b/caching-service/src/test/resources/application.yml index ed96280b1c..a73bda02e8 100644 --- a/caching-service/src/test/resources/application.yml +++ b/caching-service/src/test/resources/application.yml @@ -92,3 +92,33 @@ server: keyStorePassword: password trustStore: ../keystore/localhost/localhost.truststore.p12 trustStorePassword: password + +logging: + level: + ROOT: DEBUG + org.springframework: DEBUG + +--- +spring.config.activate.on-profile: attlsServer + +server: + attlsServer: + enabled: true + ssl: + enabled: false + +--- +spring.config.activate.on-profile: attlsClient + +apiml: + service: + scheme: http + +server: + attlsClient: + enabled: true + +eureka: + instance: + nonSecurePortEnabled: true + securePortEnabled: false diff --git a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java index 771d30ff1a..536e33151d 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java +++ b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsFactory.java @@ -69,7 +69,6 @@ public CloseableHttpClient buildHttpClient(HttpClientConnectionManager connectio .disableAuthCaching() .setRedirectStrategy(new LaxRedirectStrategy()) .build(); - } public ConnectionSocketFactory createSslSocketFactory() { diff --git a/discovery-package/src/main/resources/zosmf-static-definition.yaml.template b/discovery-package/src/main/resources/zosmf-static-definition.yaml.template index d2bd325871..d16f64446a 100644 --- a/discovery-package/src/main/resources/zosmf-static-definition.yaml.template +++ b/discovery-package/src/main/resources/zosmf-static-definition.yaml.template @@ -1,12 +1,12 @@ # Static definition for z/OSMF # # Once configured you can access z/OSMF via the API gateway: -# curl -k -X GET -H "X-CSRF-ZOSMF-HEADER: *" https://${ZOWE_EXPLORER_HOST}:${GATEWAY_PORT}/zosmf/api/v1/info +# curl -k -X GET -H "X-CSRF-ZOSMF-HEADER: *" https://${ZWE_zowe_externalDomains_0}:${GATEWAY_PORT}/zosmf/api/v1/info # services: - serviceId: ibmzosmf title: IBM z/OSMF - description: 'IBM z/OS Management Facility REST API service. Once configured you can access z/OSMF via the API gateway: https://${ZOWE_EXPLORER_HOST}:${GATEWAY_PORT}/ibmzosmf/api/v1/info' + description: 'IBM z/OS Management Facility REST API service. Once configured you can access z/OSMF via the API gateway: https://${ZWE_zowe_externalDomains_0}:${GATEWAY_PORT}/ibmzosmf/api/v1/info' catalogUiTileId: zosmf instanceBaseUrls: - ${ZOSMF_SCHEME}://${ZOSMF_HOST}:${ZOSMF_PORT}/ diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java index 1c6aa24282..8a439ded7e 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java @@ -71,7 +71,7 @@ public class HttpsWebSecurityConfig extends AbstractWebSecurityConfigurer { private boolean nonStrictVerifySslCertificatesOfServices; @Bean - public WebSecurityCustomizer httpsWebSecurityCustomizer() { + WebSecurityCustomizer httpsWebSecurityCustomizer() { String[] noSecurityAntMatchers = { "/eureka/css/**", "/eureka/js/**", @@ -94,7 +94,7 @@ public WebSecurityCustomizer httpsWebSecurityCustomizer() { */ @Bean @Order(1) - public SecurityFilterChain errorHandler(HttpSecurity http) throws Exception { + SecurityFilterChain errorHandler(HttpSecurity http) throws Exception { return baseConfigure(http.securityMatcher("/error")).build(); } @@ -103,7 +103,7 @@ public SecurityFilterChain errorHandler(HttpSecurity http) throws Exception { */ @Bean @Order(3) - public SecurityFilterChain basicAuthOrTokenFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain basicAuthOrTokenFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( "/application/**", "/*" @@ -126,7 +126,7 @@ public SecurityFilterChain basicAuthOrTokenFilterChain(HttpSecurity http) throws */ @Bean @Order(2) - public SecurityFilterChain clientCertificateFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain clientCertificateFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatcher("/eureka/**")); if (verifySslCertificatesOfServices || !nonStrictVerifySslCertificatesOfServices) { http.x509(x509 -> x509.userDetailsService(x509UserDetailsService())) @@ -148,7 +148,7 @@ public SecurityFilterChain clientCertificateFilterChain(HttpSecurity http) throw */ @Bean @Order(4) - public SecurityFilterChain basicAuthOrTokenOrCertFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain basicAuthOrTokenOrCertFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatcher("/discovery/**")) .authenticationProvider(gatewayLoginProvider) .authenticationProvider(gatewayTokenProvider) diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java index a1a3b176eb..6fa9b8f417 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/config/AttlsConfigTest.java @@ -11,15 +11,12 @@ package org.zowe.apiml.discovery.config; import org.apache.http.HttpStatus; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; -import org.zowe.apiml.util.config.TestConfig; import org.zowe.apiml.discovery.functional.DiscoveryFunctionalTest; import java.io.IOException; @@ -27,61 +24,73 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.StringContains.containsString; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; -@TestPropertySource( - properties = { - "server.attlsServer.enabled=true", - "server.ssl.enabled=false" - } -) @TestInstance(Lifecycle.PER_CLASS) -@ActiveProfiles("attlsServer") -@Import(TestConfig.class) -class AttlsConfigTest extends DiscoveryFunctionalTest { +class AttlsConfigTest { private String protocol = "http"; - @Override - protected String getProtocol() { - return protocol; - } - + @ActiveProfiles({ "attlsServer", "attlsClient" }) @Nested - class GivenAttlsModeEnabledAndHttps { + class GivenAttlsModeEnabled extends DiscoveryFunctionalTest { - @BeforeEach - void setUp() { - protocol = "https"; + @Override + protected String getProtocol() { + return protocol; } @Test - void whenContextLoads_RequestFailsWithHttps() { - try { + void whenContextLoads_requestFailsWithHttps() { + protocol = "https"; + assertThrows(IOException.class, () -> { given() .log().all() .when() .get(getDiscoveryUriWithPath("/application/info")) .then() .log().all(); - fail("Expected SSL failure"); - } catch (Exception e) { - assertInstanceOf(IOException.class, e); - } + }); + } + + /** + * This test verifies the call attempted to use AT-TLS filters + */ + @Test + void whenContextLoads_RequestFailsWithAttlsContextReason() { + protocol = "http"; + given() + .log().all() + .when() + .get(getDiscoveryUriWithPath("/eureka/apps")) + .then() + .log().all() + .statusCode(is(HttpStatus.SC_INTERNAL_SERVER_ERROR)) + .body(containsString("Connection is not secure.")) + .body(containsString("AttlsContext.getStatConn")); } + } + /** + * This test intends to verify ICSF workaround (no keyring load) + */ @Nested - class GivenAttlsModeEnabledAndHttp { - - @BeforeEach - void setUp() { - protocol = "http"; + @TestPropertySource( + properties = { + "server.ssl.keyStoreType=", + "server.ssl.keyStorePassword=", + "server.ssl.keyPassword=", + "server.ssl.keyAlias=", + "server.ssl.keyStore=" } + ) + @ActiveProfiles({ "attlsServer", "attlsClient" }) + class GivenSslDisabled extends DiscoveryFunctionalTest { @Test - void whenContextLoads_RequestFailsWithAttlsContextReason() { + void whenNoKeystore_thenStartupSuccess() { + protocol = "http"; given() .log().all() .when() @@ -92,5 +101,7 @@ void whenContextLoads_RequestFailsWithAttlsContextReason() { .body(containsString("Connection is not secure.")) .body(containsString("AttlsContext.getStatConn")); } + } + } diff --git a/discovery-service/src/test/resources/application.yml b/discovery-service/src/test/resources/application.yml index 34740689fc..9e35123686 100644 --- a/discovery-service/src/test/resources/application.yml +++ b/discovery-service/src/test/resources/application.yml @@ -63,6 +63,12 @@ management: DOWN: 503 PARTIAL: 200 show-details: always + +logging: + level: + ROOT: DEBUG + org.springframework: DEBUG + --- spring.config.activate.on-profile: https @@ -82,3 +88,28 @@ server: keyStorePassword: password trustStore: ../keystore/localhost/localhost.truststore.p12 trustStorePassword: password + +--- +spring.config.activate.on-profile: attlsServer + +server: + attlsServer: + enabled: true + ssl: + enabled: false + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + +eureka: + instance: + nonSecurePortEnabled: true + securePortEnabled: false + +apiml: + discovery: + allPeersUrls: http://${apiml.service.hostname}:${apiml.service.port}/eureka/ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java index 74549f2888..14f23e87c1 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java @@ -16,6 +16,7 @@ @SpringBootApplication( scanBasePackages = { + "org.zowe.apiml.filter", "org.zowe.apiml.gateway", "org.zowe.apiml.product.web", "org.zowe.apiml.product.config", diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java index 3e750d0262..34d425939a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/CachingServiceClientRest.java @@ -10,7 +10,6 @@ package org.zowe.apiml.gateway.caching; -import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -35,7 +34,7 @@ public class CachingServiceClientRest implements CachingServiceClient { @Value("${apiml.cachingServiceClient.apiPath:/cachingservice/api/v1/cache}") private String CACHING_API_PATH; - private String cachingBalancerUrl; + private volatile String cachingBalancerUrl; private final GatewayClient gatewayClient; private static final MultiValueMap defaultHeaders = new LinkedMultiValueMap<>(); @@ -54,14 +53,14 @@ public CachingServiceClientRest( this.webClient = webClientClientCert; } - @PostConstruct void updateUrl() { + // Lazy initialization of GatewayClient's ServerAddress may bring invalid URL during initialization this.cachingBalancerUrl = String.format("%s://%s/%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), CACHING_API_PATH); } - public Mono create(ApiKeyValue keyValue) { - return webClient.post() + updateUrl(); + Mono post = webClient.post() .uri(cachingBalancerUrl) .bodyValue(keyValue) .headers(c -> c.addAll(defaultHeaders)) @@ -69,13 +68,18 @@ public Mono create(ApiKeyValue keyValue) { if (handler.statusCode().is2xxSuccessful()) { return empty(); } else { + log.debug("Unable to create cache record with result: {}", handler.statusCode()); return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to create caching key " + keyValue.getKey() + CACHING_SERVICE_RETURNED + handler.statusCode())); } }); + post.doOnError(e -> log.debug("Unable to create cache record", e)); + + return post; } public Mono update(ApiKeyValue keyValue) { - return webClient.put() + updateUrl(); + Mono put = webClient.put() .uri(cachingBalancerUrl) .bodyValue(keyValue) .headers(c -> c.addAll(defaultHeaders)) @@ -86,10 +90,14 @@ public Mono update(ApiKeyValue keyValue) { return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to update caching key " + keyValue.getKey() + CACHING_SERVICE_RETURNED + handler.statusCode())); } }); + put.doOnError(e -> log.debug("Unable to update cache key", e)); + + return put; } public Mono read(String key) { - return webClient.get() + updateUrl(); + Mono read = webClient.get() .uri(cachingBalancerUrl + "/" + key) .headers(c -> c.addAll(defaultHeaders)) .exchangeToMono(handler -> { @@ -97,13 +105,16 @@ public Mono read(String key) { return handler.bodyToMono(ApiKeyValue.class); } else if (handler.statusCode().is4xxClientError()) { if (log.isTraceEnabled()) { - log.trace("Key with ID {}not found. Status code from caching service: {}", key, handler.statusCode()); + log.trace("Key with ID {} not found. Status code from caching service: {}", key, handler.statusCode()); } return empty(); } else { return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to read caching key " + key + CACHING_SERVICE_RETURNED + handler.statusCode())); } }); + read.doOnError(e -> log.debug("Unable to read cache key", e)); + + return read; } /** @@ -113,7 +124,8 @@ public Mono read(String key) { * @return mono with status success / error */ public Mono delete(String key) { - return webClient.delete() + updateUrl(); + Mono delete = webClient.delete() .uri(cachingBalancerUrl + "/" + key) .headers(c -> c.addAll(defaultHeaders)) .exchangeToMono(handler -> { @@ -123,6 +135,9 @@ public Mono delete(String key) { return error(new CachingServiceClientException(handler.statusCode().value(), "Unable to delete caching key " + key + CACHING_SERVICE_RETURNED + handler.statusCode())); } }); + delete.doOnError(e -> log.debug("Unable to delete cache key", e)); + + return delete; } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java index dd56cc99f3..242ede8dc2 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/caching/LoadBalancerCache.java @@ -172,9 +172,11 @@ private String getKey(String user, String service) { */ @Data public static class LoadBalancerCacheRecord { + + public static final LoadBalancerCacheRecord NONE = new LoadBalancerCacheRecord(null, null); + private final String instanceId; private final LocalDateTime creationTime; - public static final LoadBalancerCacheRecord NONE = new LoadBalancerCacheRecord(null, null); public LoadBalancerCacheRecord(String instanceId) { this(instanceId, LocalDateTime.now()); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index f18f966b5d..4cb9e28b9b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -79,16 +79,22 @@ @RequiredArgsConstructor public class ConnectionsConfig { + private static final ApimlLogger apimlLog = ApimlLogger.of(ConnectionsConfig.class, YamlMessageServiceInstance.getInstance()); + @Value("${eureka.client.serviceUrl.defaultZone}") private String eurekaServerUrl; @Value("${apiml.service.corsEnabled:false}") private boolean corsEnabled; + @Value("${apiml.service.corsAllowedMethods:GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}") private List corsAllowedMethods; + + @Value("${server.attlsClient.enabled:false}") + private boolean isClientAttlsEnabled; + private final ApplicationContext context; private final HttpConfig config; - private static final ApimlLogger apimlLog = ApimlLogger.of(ConnectionsConfig.class, YamlMessageServiceInstance.getInstance()); @Value("${apiml.service.externalUrl:}") private String externalUrl; @@ -101,10 +107,11 @@ public class ConnectionsConfig { */ @Bean NettyRoutingFilterApiml createNettyRoutingFilterApiml(HttpClient httpClient, ObjectProvider> headersFiltersProvider, HttpClientProperties properties) { + boolean isKeyLoadPrevented = StringUtils.isBlank(config.getKeyStorePath()) && isClientAttlsEnabled; try { return new NettyRoutingFilterApiml( ConnectionUtil.getHttpClient(config, httpClient, false), - ConnectionUtil.getHttpClient(config, httpClient, true), + ConnectionUtil.getHttpClient(config, httpClient, !isKeyLoadPrevented), headersFiltersProvider, properties ); } catch (Exception e) { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java index b420d3e088..834b163994 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RegistryConfig.java @@ -35,6 +35,7 @@ BasicInfoService basicInfoService(EurekaClient eurekaClient, EurekaMetadataParse ServiceAddress gatewayServiceAddress( @Value("${apiml.service.externalUrl:#{null}}") String externalUrl, @Value("${server.attlsServer.enabled:false}") boolean serverAttlsEnabled, + @Value("${server.attlsClient.enabled:false}") boolean clientAttlsEnabled, @Value("${server.ssl.enabled:true}") boolean sslEnabled, @Value("${apiml.service.hostname:localhost}") String hostname, @Value("${server.port}") int port @@ -42,15 +43,30 @@ ServiceAddress gatewayServiceAddress( if (externalUrl != null) { URI uri = new URI(externalUrl); return ServiceAddress.builder() - .scheme(uri.getScheme()) + .scheme(clientAttlsEnabled ? "http" : uri.getScheme()) .hostname(uri.getHost() + ":" + uri.getPort()) .build(); } return ServiceAddress.builder() - .scheme(serverAttlsEnabled || sslEnabled ? "https" : "http") + .scheme(determineScheme(serverAttlsEnabled, clientAttlsEnabled, sslEnabled)) .hostname(hostname + ":" + port) .build(); } + private String determineScheme( + boolean serverAttlsEnabled, + boolean clientAttlsEnabled, + boolean sslEnabled + ) { + String scheme; + if (clientAttlsEnabled) { + scheme = "http"; + } else { + scheme = serverAttlsEnabled || sslEnabled ? "https" : "http"; + } + + return scheme; + } + } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java index 4632d4270a..4fd6e0f7e5 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ServiceCorsUpdater.java @@ -10,9 +10,9 @@ package org.zowe.apiml.gateway.config; -import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.InitializingBean; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.gateway.config.GlobalCorsProperties; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; @@ -28,7 +28,7 @@ @Component @RequiredArgsConstructor -public class ServiceCorsUpdater { +public class ServiceCorsUpdater implements InitializingBean { private final CorsUtils corsUtils; private final ReactiveDiscoveryClient discoveryClient; @@ -38,8 +38,8 @@ public class ServiceCorsUpdater { @Getter private UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource; - @PostConstruct - void initCorsConfigurationSource() { + @Override + public void afterPropertiesSet() throws Exception { urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(new PathPatternParser()); urlBasedCorsConfigurationSource.setCorsConfigurations(globalCorsProperties.getCorsConfigurations()); corsUtils.registerDefaultCorsConfiguration(urlBasedCorsConfigurationSource::registerCorsConfiguration); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java index e865e21088..4e6beb3dfb 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java @@ -77,11 +77,11 @@ import org.zowe.apiml.gateway.filters.security.TokenAuthFilter; import org.zowe.apiml.gateway.service.BasicAuthProvider; import org.zowe.apiml.gateway.service.TokenProvider; -import org.zowe.apiml.security.HttpsConfig; -import org.zowe.apiml.security.common.util.X509Util; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.security.HttpsConfig; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.config.SafSecurityConfigurationProperties; +import org.zowe.apiml.security.common.util.X509Util; import reactor.core.publisher.Mono; import java.io.IOException; @@ -343,6 +343,7 @@ private List getClientRegistrations() { public ServerHttpSecurity defaultSecurityConfig(ServerHttpSecurity http) { var gatewayExceptionHandler = applicationContext.getBean("gatewayExceptionHandler", GatewayExceptionHandler.class); + return http .headers(headers -> headers .hsts(hsts -> hsts.disable()) @@ -543,7 +544,7 @@ StrictServerWebExchangeFirewall httpFirewall() { @Bean @Primary @ConditionalOnProperty(name = "spring.cloud.gateway.x-forwarded.enabled", matchIfMissing = true) - public XForwardedHeadersFilter xForwardedHeadersFilter( + XForwardedHeadersFilter xForwardedHeadersFilter( @Value("${apiml.security.forwardHeader.trustedProxies:#{null}}") String trustedProxies, HttpsConfig httpsConfig, AdditionalRegistrationGatewayRegistry additionalRegistrationGatewayRegistry diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 0ba7d2809e..594925346e 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -142,21 +142,21 @@ logging: level: ROOT: INFO com.netflix: WARN - com.netflix.discovery: ERROR com.netflix.config: ERROR - com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient: OFF + com.netflix.discovery: ERROR com.netflix.discovery.DiscoveryClient: OFF + com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient: OFF + io.netty.resolver.dns: WARN + io.netty.util.internal.MacAddressUtil: ERROR # Can print WARN not finding usable MAC Address + javax.net.ssl: ERROR + org.apache.tomcat.util.net.SSLUtilBase: ERROR + org.springframework.beans: WARN org.springframework.cloud.gateway.filter: WARN org.springframework.cloud.gateway.route: WARN org.springframework.context.support: WARN - org.springframework.beans: WARN org.springframework.context.support.[PostProcessorRegistrationDelegate$BeanPostProcessorChecker]: ERROR reactor.netty.http.client: INFO reactor.netty.http.client.HttpClientConnect: OFF - io.netty.util.internal.MacAddressUtil: ERROR # Can print WARN not finding usable MAC Address - io.netty.resolver.dns: WARN - javax.net.ssl: ERROR - org.apache.tomcat.util.net.SSLUtilBase: ERROR management: endpoint: @@ -178,20 +178,32 @@ spring.config.activate.on-profile: debug logging: level: - org.zowe.apiml: DEBUG + com.netflix: DEBUG + javax.net.ssl: ERROR + org.apache.tomcat.util.net: DEBUG + org.apache.tomcat.util.net.jsse.JSSESupport: INFO + org.springframework.boot.autoconfigure.web: DEBUG org.springframework.cloud.gateway: DEBUG org.springframework.context.support.[PostProcessorRegistrationDelegate$BeanPostProcessorChecker]: DEBUG - org.springframework.security: DEBUG org.springframework.http.server.reactive: DEBUG + org.springframework.security: DEBUG org.springframework.web.reactive: DEBUG org.springframework.web.reactive.socket: DEBUG - reactor.netty.http.client: DEBUG - reactor.netty.http.client.HttpClient: DEBUG - reactor.netty.http.client.HttpClientConnect: DEBUG - com.netflix: DEBUG - org.apache.tomcat.util.net: DEBUG - org.apache.tomcat.util.net.jsse.JSSESupport: INFO - javax.net.ssl: ERROR + org.zowe.apiml: DEBUG + reactor.netty: TRACE + +--- +spring.config.activate.on-profile: wiretap + +spring: + cloud: + gateway: + server: + webflux: + httpserver: + wiretap: true + httpclient: + wiretap: true --- spring.config.activate.on-profile: attlsServer diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java new file mode 100644 index 0000000000..17b12d33f3 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java @@ -0,0 +1,175 @@ +/* + * 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.acceptance; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +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.web.server.LocalServerPort; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.filter.AttlsHttpHandler; +import org.zowe.apiml.gateway.GatewayServiceApplication; +import org.zowe.apiml.product.web.ApimlTomcatCustomizer; + +import javax.net.ssl.SSLException; + +import static io.restassured.RestAssured.given; +import static org.apache.hc.core5.http.HttpStatus.SC_OK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@TestInstance(Lifecycle.PER_CLASS) +class AttlsConfigTest { + + private String getGatewayUrlWithPath(String hostname, int port, String scheme, String path) { + return String.format("%s://%s:%d/%s", scheme, hostname, port, path); + } + + @Nested + @ActiveProfiles({ "attlsServer", "attlsClient" }) + @DirtiesContext + @SpringBootTest( + classes = GatewayServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenAttlsProfile { + + @LocalServerPort + private int port; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + + @MockitoBean + private ApimlTomcatCustomizer apimlTomcatCustomizer; + + @Test + void whenContextloads_requestFailsWithHttps() { + assertThrows(SSLException.class, () -> { + given() + .log().all() + .when() + .get(getGatewayUrlWithPath(hostname, port, "https", "application/version")) + .then() + .log().all(); + }); + } + + @Test + void requestFailsWithAttlsReasonWithHttp() { + var logger = (Logger) LoggerFactory.getLogger(AttlsHttpHandler.class); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); + + // Prevent use of native code but verify it calls the customizer + doNothing().when(apimlTomcatCustomizer).customize(any()); + + given() + .log().all() + .when() + .get(getGatewayUrlWithPath(hostname, port, "http", "application/version")) + .then() + .log().all() + .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .body(containsString("org.zowe.apiml.common.internalServerError")); + + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); + } + + } + + /** + * This test intends to verify ICSF workaround (no keyring load) + */ + @Nested + @TestPropertySource( + properties = { + "server.ssl.keyStoreType=", + "server.ssl.keyStorePassword=", + "server.ssl.keyPassword=", + "server.ssl.keyAlias=", + "server.ssl.keyStore=" + } + ) + @ActiveProfiles({ "attlsServer", "attlsClient" }) + @DirtiesContext + @SpringBootTest( + classes = GatewayServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenSslDisabled { + + @MockitoBean + private AttlsHttpHandler attlsHttpHandler; + + @LocalServerPort + private int port; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + + @MockitoBean + private ApimlTomcatCustomizer apimlTomcatCustomizer; + + @BeforeEach + void setUp() { + doNothing().when(apimlTomcatCustomizer).customize(any()); + } + + @Test + void whenNoKeystore_thenStartupSuccess() { + given() + .log().all() + .when() + .get(getGatewayUrlWithPath(hostname, port, "http", "application/version")) + .then() + .statusCode(SC_OK); + + verify(apimlTomcatCustomizer, times(1)).customize(any()); + verify(attlsHttpHandler, times(1)).postProcessAfterInitialization(any(HttpHandler.class), any()); + } + + } + +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/RegistryConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/RegistryConfigTest.java index 4ac3a6b02a..cb015b16b9 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/RegistryConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/RegistryConfigTest.java @@ -28,7 +28,7 @@ class WithExternalUrl { @Test void whenExternalUrlIsDefined_thenTransformIt() throws URISyntaxException { - ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress("https://host:123/path", false, false, null, 0); + ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress("https://host:123/path", false, false, false, null, 0); assertEquals("https", serviceAddress.getScheme()); assertEquals("host:123", serviceAddress.getHostname()); } @@ -39,22 +39,29 @@ void whenExternalUrlIsDefined_thenTransformIt() throws URISyntaxException { class WithoutExternalUrl { @Test - void whenAttls_thenTransformIt() throws URISyntaxException { - ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress(null, true, false, "hostname", 10010); + void whenClientAttls_thenTransformIt() throws URISyntaxException { + ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress(null, true, true, false, "hostname", 10010); + assertEquals("http", serviceAddress.getScheme()); + assertEquals("hostname:10010", serviceAddress.getHostname()); + } + + @Test + void whenOnlyServerAttls_thenTransformIt() throws URISyntaxException { + ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress(null, true, false, false, "hostname", 10010); assertEquals("https", serviceAddress.getScheme()); assertEquals("hostname:10010", serviceAddress.getHostname()); } @Test void whenSsl_thenTransformIt() throws URISyntaxException { - ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress(null, false, true, "localhost", 10010); + ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress(null, false, false, true, "localhost", 10010); assertEquals("https", serviceAddress.getScheme()); assertEquals("localhost:10010", serviceAddress.getHostname()); } @Test void whenNoTtls_thenTransformIt() throws URISyntaxException { - ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress(null, false, false, "localhost", 80); + ServiceAddress serviceAddress = new RegistryConfig().gatewayServiceAddress(null, false, false, false, "localhost", 80); assertEquals("http", serviceAddress.getScheme()); assertEquals("localhost:80", serviceAddress.getHostname()); } @@ -63,4 +70,4 @@ void whenNoTtls_thenTransformIt() throws URISyntaxException { } -} \ No newline at end of file +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java index b4fca1b3db..d7f76cbf43 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java @@ -58,9 +58,9 @@ class ServiceCorsUpdaterTest { private UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource; @BeforeEach - void setUp() { + void setUp() throws Exception { serviceCorsUpdater = new ServiceCorsUpdater(corsUtils, discoveryClient, mock(RoutePredicateHandlerMapping.class), mock(GlobalCorsProperties.class)); - serviceCorsUpdater.initCorsConfigurationSource(); + serviceCorsUpdater.afterPropertiesSet(); urlBasedCorsConfigurationSource = spy((UrlBasedCorsConfigurationSource) ReflectionTestUtils.getField(serviceCorsUpdater, "urlBasedCorsConfigurationSource")); ReflectionTestUtils.setField(serviceCorsUpdater, "urlBasedCorsConfigurationSource", urlBasedCorsConfigurationSource); } diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml index b5ea677787..0a4147ac55 100644 --- a/gateway-service/src/test/resources/application.yml +++ b/gateway-service/src/test/resources/application.yml @@ -59,11 +59,13 @@ spring: logging: level: + ROOT: DEBUG + org.springframework: DEBUG + org.springframework.web: TRACE org.springframework.cloud.gateway: DEBUG - reactor.netty: DEBUG org.springframework.security: DEBUG org.springframework.beans: WARN - + reactor.netty: DEBUG management: endpoint: @@ -74,3 +76,39 @@ management: base-path: /application exposure: include: health,gateway + +--- +spring.config.activate.on-profile: attlsServer + +server: + attlsServer: + enabled: true + ssl: + enabled: false + + service: + scheme: http +apiml: + service: + corsEnabled: true + scheme: http + nonSecurePortEnabled: true + securePortEnabled: false + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + +apiml: + service: + discoveryServiceUrls: http://localhost:10011/eureka/ + +eureka: + instance: + metadata-map: + apiml: + apiInfo[0]: + swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/gateway/api-docs diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java index e4efcfe3dd..77686f631c 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/MultipartPutIntegrationTest.java @@ -27,7 +27,7 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; -import static org.zowe.apiml.util.requests.Endpoints.*; +import static org.zowe.apiml.util.requests.Endpoints.DISCOVERABLE_MULTIPART; @DiscoverableClientDependentTest class MultipartPutIntegrationTest implements TestWithStartedInstances { @@ -87,9 +87,9 @@ void givenLargeFileUpload() { new RandomDataInputStream(payloadSize), "application/octet-stream" ) - .when() + .when() .post(url) - .then() + .then() .statusCode(200) .body("fileName", equalTo("largefile.dat")) .body("fileType", equalTo("application/octet-stream")) diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java index 354e901b8b..782a7a4145 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/NewSecurityConfiguration.java @@ -134,7 +134,7 @@ class AuthenticationFunctionality { private final CompoundAuthProvider compoundAuthProvider; @Bean - public SecurityFilterChain authenticationFunctionalityFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain authenticationFunctionalityFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( // no http method to catch all attempts to login and handle them here. Otherwise it falls to default filterchain and tries to route the calls, which doesnt make sense authConfigurationProperties.getZaasLoginEndpoint(), authConfigurationProperties.getZaasLogoutEndpoint() @@ -157,6 +157,7 @@ public SecurityFilterChain authenticationFunctionalityFilterChain(HttpSecurity h } private class CustomSecurityFilters extends AbstractHttpConfigurer { + @Override public void configure(HttpSecurity http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); @@ -188,6 +189,7 @@ private LogoutHandler logoutHandler() { FailedAuthenticationHandler failure = handlerInitializer.getAuthenticationFailureHandler(); return new JWTLogoutHandler(authenticationService, failure); } + } /** @@ -212,7 +214,7 @@ class AccessToken { private final AuthenticationProvider tokenAuthenticationProvider; @Bean - public SecurityFilterChain accessTokenFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain accessTokenFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( // no http method to catch all attempts to login and handle them here. Otherwise it falls to default filterchain and tries to route the calls, which doesnt make sense authConfigurationProperties.getZaasAccessTokenEndpoint() ))) @@ -274,7 +276,7 @@ class AuthenticationProtectedEndpoints { private final CompoundAuthProvider compoundAuthProvider; @Bean - public SecurityFilterChain authProtectedEndpointsFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain authProtectedEndpointsFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( // no http method to catch all attempts to login and handle them here. Otherwise it falls to default filterchain and tries to route the calls, which doesnt make sense authConfigurationProperties.getZaasRevokeMultipleAccessTokens() + "/**", authConfigurationProperties.getZaasEvictAccessTokensAndRules() @@ -319,7 +321,7 @@ private X509ForwardingAwareAuthenticationFilter x509ForwardingAwareAuthenticatio class ZaasEndpoints { @Bean - public SecurityFilterChain authZaasEndpointsFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain authZaasEndpointsFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( // no http method to catch all attempts to login and handle them here. Otherwise it falls to default filterchain and tries to route the calls, which doesnt make sense "/zaas/scheme/**" ))) @@ -348,7 +350,7 @@ class Query { private final TokenAuthenticationProvider tokenAuthenticationProvider; @Bean - public SecurityFilterChain queryFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain queryFilterChain(HttpSecurity http) throws Exception { return baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( authConfigurationProperties.getZaasQueryEndpoint() ))) @@ -392,7 +394,7 @@ class Ticket { private final AuthenticationProvider tokenAuthenticationProvider; @Bean - public SecurityFilterChain ticketFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain ticketFilterChain(HttpSecurity http) throws Exception { return baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( authConfigurationProperties.getZaasTicketEndpoint() ))).authorizeHttpRequests(requests -> requests.anyRequest().authenticated()) @@ -438,7 +440,7 @@ class Refresh { private final AuthenticationProvider tokenAuthenticationProvider; @Bean - public SecurityFilterChain refreshFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain refreshFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers( authConfigurationProperties.getZaasRefreshEndpoint() ))).authorizeHttpRequests(requests -> requests @@ -481,7 +483,7 @@ private QueryFilter refreshFilter(String ticketEndpoint, AuthenticationManager a @Order(4) class CertificateProtectedEndpoints { @Bean - public SecurityFilterChain certificateEndpointsFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain certificateEndpointsFilterChain(HttpSecurity http) throws Exception { return baseConfigure(http.securityMatchers(matchers -> matchers .requestMatchers(AuthController.CONTROLLER_PATH + AuthController.INVALIDATE_PATH, AuthController.CONTROLLER_PATH + AuthController.DISTRIBUTE_PATH)) ).authorizeHttpRequests(requests -> requests @@ -606,7 +608,7 @@ class DefaultSecurity { // Web security only needs to be configured once, putting it to multiple filter chains causes multiple evaluations of the same rules @Bean - public WebSecurityCustomizer webSecurityCustomizer() { + WebSecurityCustomizer webSecurityCustomizer() { return web -> { if (!isHealthEndpointProtected) { web.ignoring().requestMatchers("/application/health"); @@ -623,7 +625,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { } @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return baseConfigure(http.securityMatchers(matchers -> matchers.requestMatchers("/**", "/gateway/version"))) .authorizeHttpRequests(requests -> requests .anyRequest() diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java index ff55a58635..9405acb7dd 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/config/AttlsConfigTest.java @@ -15,13 +15,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.zaas.ZaasApplication; import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; import javax.net.ssl.SSLException; @@ -29,74 +30,104 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.StringContains.containsString; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; -/** - * Simple Spring Context test to verify AT-TLS filter chain setup is in place with the right properties being sent - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@TestPropertySource( - properties = { - "server.internal.ssl.enabled=false", - "server.attlsServer.enabled=true", - "server.ssl.enabled=false", - "server.service.scheme=http" - } -) @TestInstance(Lifecycle.PER_CLASS) -public class AttlsConfigTest { - - @MockitoBean(name = "x509Mapper") - private AuthenticationMapper x509Mapper; - - @Autowired - HttpSecurity http; +class AttlsConfigTest { + + /** + * Simple Spring Context test to verify AT-TLS filter chain setup is in place with the right properties being sent + */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @ActiveProfiles({ "attlsServer", "attlsClient" }) + @DirtiesContext + @Nested + class GivenAttlsModeEnabled { - @LocalServerPort - private int port; + @MockitoBean(name = "x509Mapper") + private AuthenticationMapper x509Mapper; - @Value("${apiml.service.hostname:localhost}") - private String hostname; + @LocalServerPort + private int port; - @Nested - class GivenAttlsModeEnabled { + @Value("${apiml.service.hostname:localhost}") + private String hostname; - @Nested - class WhenContextLoads { - - @Test - void requestFailsWithHttps() { - try { - given() - .log().all() - .cookie(COOKIE_AUTH_NAME, "jwttoken") - .when() - .get(String.format("https://%s:%d", hostname, port)) - .then() - .log().all() - .statusCode(is(HttpStatus.SC_INTERNAL_SERVER_ERROR)); - fail("Expected SSL failure"); - } catch (Exception e) { - assertInstanceOf(SSLException.class, e); - } - } - - @Test - void requestFailsWithAttlsContextReasonWithHttp() { + @Test + void requestFailsWithHttps() { + assertThrows(SSLException.class, () -> { given() .log().all() .cookie(COOKIE_AUTH_NAME, "jwttoken") .when() - .get(String.format("http://%s:%d", hostname, port)) + .get(String.format("https://%s:%d", hostname, port)) .then() - .log().all() - .statusCode(is(HttpStatus.SC_INTERNAL_SERVER_ERROR)) - .body(containsString("Connection is not secure.")) - .body(containsString("AttlsContext.getStatConn")); - } + .log().all(); + fail("Expected SSL failure"); + }); + } + + @Test + void requestFailsWithAttlsContextReasonWithHttp() { + given() + .log().all() + .cookie(COOKIE_AUTH_NAME, "jwttoken") + .when() + .get(String.format("http://%s:%d", hostname, port)) + .then() + .log().all() + .statusCode(is(HttpStatus.SC_INTERNAL_SERVER_ERROR)) + .body(containsString("Connection is not secure.")) + .body(containsString("AttlsContext.getStatConn")); + } + } + + /** + * This test intends to verify ICSF workaround (no keyring load) + */ + @Nested + @TestPropertySource( + properties = { + "server.ssl.keyStoreType=", + "server.ssl.keyStorePassword=", + "server.ssl.keyPassword=", + "server.ssl.keyAlias=", + "server.ssl.keyStore=", + "apiml.security.auth.provider=zosmf" + } + ) + @ActiveProfiles({ "attlsServer", "attlsClient" }) + @DirtiesContext + @SpringBootTest( + classes = ZaasApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenSslDisabled { + + @MockitoBean(name = "x509Mapper") + private AuthenticationMapper x509Mapper; + + @LocalServerPort + private int port; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + + @Test + void whenNoKeystore_thenStartupSuccess() { + given() + .log().all() + .cookie(COOKIE_AUTH_NAME, "jwttoken") + .when() + .get(String.format("http://%s:%d", hostname, port)) + .then() + .log().all() + .statusCode(is(HttpStatus.SC_INTERNAL_SERVER_ERROR)) + .body(containsString("Connection is not secure.")) + .body(containsString("AttlsContext.getStatConn")); } } diff --git a/zaas-service/src/test/resources/application.yml b/zaas-service/src/test/resources/application.yml index 76652cdcb2..9c018d26e3 100644 --- a/zaas-service/src/test/resources/application.yml +++ b/zaas-service/src/test/resources/application.yml @@ -183,3 +183,48 @@ management: spring.config.activate.on-profile: dev logbackServiceName: ZWEAZS1 + +--- +spring.config.activate.on-profile: attlsServer + +server: + internal: + ssl: + enabled: false + attlsServer: + enabled: true + ssl: + enabled: false + service: + scheme: http + +apiml: + service: + corsEnabled: true + +--- +spring.config.activate.on-profile: attlsClient + +server: + attlsClient: + enabled: true + +apiml: + security: + saf: + urls: + authenticate: http://localhost:10013/zss/saf/authenticate + verify: http://localhost:10013/zss/saf/verify + service: + corsEnabled: true + scheme: http +eureka: + instance: + securePortEnabled: false + metadata-map: + apiml: + apiInfo: + - apiId: zowe.apiml.zaas + gatewayUrl: api/v1 + swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/api-doc + documentationUrl: https://zowe.github.io/docs-site/ From 4566591eb5a7426e02fe7cd37787f07e99241c06 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:55:24 -0400 Subject: [PATCH 070/152] chore: Update all non-major dependencies (v3.x.x) (#4270) Signed-off-by: Renovate Bot Co-authored-by: Renovate Bot Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 161 +++++----- api-catalog-ui/frontend/package.json | 14 +- gradle/versions.gradle | 26 +- onboarding-enabler-nodejs/package-lock.json | 8 +- onboarding-enabler-nodejs/package.json | 2 +- .../package-lock.json | 275 +++++++++--------- zowe-cli-id-federation-plugin/package.json | 14 +- 7 files changed, 250 insertions(+), 250 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index 9494f17930..399490084d 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -43,7 +43,7 @@ "react-error-boundary": "5.0.0", "react-hook-form": "7.62.0", "react-redux": "9.2.0", - "react-router": "7.8.0", + "react-router": "7.8.1", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -61,23 +61,23 @@ "uuid": "10.0.0" }, "devDependencies": { - "@babel/core": "7.28.0", + "@babel/core": "7.28.3", "@babel/eslint-parser": "7.28.0", "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "@babel/preset-env": "7.28.0", + "@babel/preset-env": "7.28.3", "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", "@eslint/compat": "1.3.2", "@eslint/js": "9.33.0", "@reduxjs/toolkit": "2.8.2", "@testing-library/dom": "10.4.1", - "@testing-library/jest-dom": "6.6.4", + "@testing-library/jest-dom": "6.7.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "ajv": "8.17.1", - "ansi-regex": "6.1.0", + "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001734", + "caniuse-lite": "1.0.30001735", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", @@ -203,21 +203,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -252,13 +252,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -307,18 +307,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "engines": { @@ -400,14 +400,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -531,25 +531,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -626,14 +626,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -1160,13 +1160,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -1177,9 +1177,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", + "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -1188,7 +1188,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -1820,9 +1820,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", - "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", + "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", "dev": true, "license": "MIT", "dependencies": { @@ -2058,9 +2058,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/preset-env/-/preset-env-7.28.0.tgz", - "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", "dev": true, "license": "MIT", "dependencies": { @@ -2072,7 +2072,7 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", @@ -2083,8 +2083,8 @@ "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", @@ -2116,7 +2116,7 @@ "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regenerator": "^7.28.3", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -2264,17 +2264,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -2282,9 +2282,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -6599,9 +6599,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", - "integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==", + "version": "6.7.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", + "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", "dev": true, "license": "MIT", "dependencies": { @@ -6609,7 +6609,6 @@ "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.21", "picocolors": "^1.1.1", "redent": "^3.0.0" }, @@ -7942,9 +7941,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { @@ -9407,9 +9406,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "version": "1.0.30001735", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "funding": [ { "type": "opencollective", @@ -24045,9 +24044,9 @@ } }, "node_modules/react-router": { - "version": "7.8.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.8.0.tgz", - "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", + "version": "7.8.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.8.1.tgz", + "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 9dbc40bb21..f08c7eb748 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -39,7 +39,7 @@ "react-error-boundary": "5.0.0", "react-hook-form": "7.62.0", "react-redux": "9.2.0", - "react-router": "7.8.0", + "react-router": "7.8.1", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -82,23 +82,23 @@ "includeFailureMsg": true }, "devDependencies": { - "@babel/core": "7.28.0", + "@babel/core": "7.28.3", "@babel/eslint-parser": "7.28.0", "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "@babel/preset-env": "7.28.0", + "@babel/preset-env": "7.28.3", "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", "@eslint/compat": "1.3.2", "@eslint/js": "9.33.0", "@testing-library/dom": "10.4.1", - "@testing-library/jest-dom": "6.6.4", + "@testing-library/jest-dom": "6.7.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@reduxjs/toolkit": "2.8.2", "ajv": "8.17.1", - "ansi-regex": "6.1.0", + "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001734", + "caniuse-lite": "1.0.30001735", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", @@ -154,7 +154,7 @@ "resolve-url-loader": "5.0.0", "lodash": "4.17.21", "semver": "7.7.2", - "@babel/traverse": "7.28.0", + "@babel/traverse": "7.28.3", "axios": "1.11.0", "form-data": "3.0.4", "swagger-ui-react": { diff --git a/gradle/versions.gradle b/gradle/versions.gradle index bc9c9bf2f1..e401e4e225 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -11,7 +11,7 @@ dependencyResolutionManagement { version('springCloudCommons', '4.3.0') version('springCloudCB', '3.3.0') version('springCloudGateway', '4.3.0') - version('springFramework', '6.2.9') + version('springFramework', '6.2.10') version('springRetry', '2.0.12') version('modulith', '1.4.2') @@ -28,7 +28,7 @@ dependencyResolutionManagement { version('commonsLogging', '1.3.5') version('commonsText', '1.14.0') version('commonsIo', '2.20.0') - version('ehCache', '3.10.8') + version('ehCache', '3.11.0') version('eureka', '2.0.5') version('netflixServo', '0.13.2') version('googleErrorprone', '2.41.0') @@ -59,10 +59,10 @@ dependencyResolutionManagement { } version('jbossLogging', '3.6.1.Final') version('jerseySun', '1.19.4') - version('jettyWebSocketClient', '12.0.24') + version('jettyWebSocketClient', '12.1.0') version('jettison', '1.5.4') //0.12.x version contains breaking changes - version('jjwt', '0.12.6') + version('jjwt', '0.12.7') version('jodaTime', '2.14.0') version('jsonPath', '2.9.0') version('jsonSmart', '2.6.0') @@ -73,21 +73,21 @@ dependencyResolutionManagement { // force version in build.gradle file - compatibility with Slf4j version('log4j', '2.25.1') version('lombok', '1.18.38') - version('netty', '4.2.3.Final') + version('netty', '4.2.4.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 - version('nettyReactor', '1.2.8') - version('nimbusJoseJwt', '10.4.1') + version('nettyReactor', '1.2.9') + version('nimbusJoseJwt', '10.4.2') version('openApiDiff', '2.1.2') version('picocli', '4.7.7') - version('reactor', '3.7.8') - version('restAssured', '5.5.5') + version('reactor', '3.7.9') + version('restAssured', '5.5.6') version('rhino', '1.8.0') version('springDoc', '2.8.9') - version('swaggerCore', '2.2.35') + version('swaggerCore', '2.2.36') version('swaggerInflector', '2.0.13') version('swagger2Parser', '1.0.75') - version('swagger3Parser', '2.1.31') + version('swagger3Parser', '2.1.32') version('thymeleaf', '3.1.3.RELEASE') version('velocity', '2.4.1') version('woodstoxCore', '7.1.1') @@ -101,7 +101,7 @@ dependencyResolutionManagement { version('gradleTestLogger', '4.0.0') version('testLogger', '4.0.0') version('micronautPlatform', '4.9.0') - version('micronaut', '4.9.9') + version('micronaut', '4.9.10') version('micronautPlugin', '4.5.4') version('shadow', '8.1.1') version('checkstyle', '10.17.0') @@ -272,7 +272,7 @@ dependencyResolutionManagement { // Pure Java dependencies - do not use in Spring unless really necessary version('logback', '1.5.18') - version('mockitoCore', '5.18.0') + version('mockitoCore', '5.19.0') version('mockitoInline', '5.2.0') library('logback_classic', 'ch.qos.logback', 'logback-classic').versionRef('logback') diff --git a/onboarding-enabler-nodejs/package-lock.json b/onboarding-enabler-nodejs/package-lock.json index 3b89a5ab42..b2bcef1358 100644 --- a/onboarding-enabler-nodejs/package-lock.json +++ b/onboarding-enabler-nodejs/package-lock.json @@ -17,7 +17,7 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.2.1", + "chai": "5.3.1", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", @@ -1621,9 +1621,9 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", - "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.1.tgz", + "integrity": "sha512-48af6xm9gQK8rhIcOxWwdGzIervm8BVTin+yRp9HEvU20BtVZ2lBywlIJBzwaDtvo0FvjeL7QdCADoUoqIbV3A==", "dev": true, "license": "MIT", "dependencies": { diff --git a/onboarding-enabler-nodejs/package.json b/onboarding-enabler-nodejs/package.json index 31fc90ccd9..449397a293 100644 --- a/onboarding-enabler-nodejs/package.json +++ b/onboarding-enabler-nodejs/package.json @@ -27,7 +27,7 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.2.1", + "chai": "5.3.1", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index e1b802aed0..48e7bb8849 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -14,18 +14,18 @@ "devDependencies": { "@eslint/js": "9.33.0", "@types/jest": "29.5.14", - "@types/node": "20.19.10", - "@typescript-eslint/eslint-plugin": "8.39.1", - "@typescript-eslint/parser": "8.39.1", - "@zowe/cli": "8.26.1", - "@zowe/cli-test-utils": "8.26.0", + "@types/node": "20.19.11", + "@typescript-eslint/eslint-plugin": "8.40.0", + "@typescript-eslint/parser": "8.40.0", + "@zowe/cli": "8.26.2", + "@zowe/cli-test-utils": "8.26.2", "@zowe/imperative": "8.26.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", "eslint": "9.33.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", - "eslint-plugin-unused-imports": "4.1.4", + "eslint-plugin-unused-imports": "4.2.0", "globals": "15.15.0", "husky": "9.1.7", "jest": "29.7.0", @@ -130,14 +130,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -284,13 +284,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -541,18 +541,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -560,9 +560,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2060,9 +2060,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.19.10", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.10.tgz", - "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", + "version": "20.19.11", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "dev": true, "license": "MIT", "dependencies": { @@ -2105,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", - "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/type-utils": "8.39.1", - "@typescript-eslint/utils": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2129,7 +2129,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.1", + "@typescript-eslint/parser": "^8.40.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2158,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.39.1.tgz", - "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4" }, "engines": { @@ -2183,14 +2183,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", - "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.1", - "@typescript-eslint/types": "^8.39.1", + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", "debug": "^4.3.4" }, "engines": { @@ -2205,14 +2205,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", - "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1" + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,9 +2223,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", - "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", "dev": true, "license": "MIT", "engines": { @@ -2240,15 +2240,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", - "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2278,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.39.1.tgz", - "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", "dev": true, "license": "MIT", "engines": { @@ -2292,16 +2292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", - "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.1", - "@typescript-eslint/tsconfig-utils": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2360,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.39.1.tgz", - "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1" + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2384,13 +2384,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", - "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "version": "8.40.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2471,25 +2471,25 @@ "dev": true }, "node_modules/@zowe/cli": { - "version": "8.26.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.26.1.tgz", - "integrity": "sha512-HkTkq4nm2DQjPxlQVltyCTBPBWdpAVquqGNmu/eBLyIkdNhAc5ClPYiP/JF2NDbHUNerpkXbaqsI0cgZbdSVZA==", + "version": "8.26.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli/-/cli-8.26.2.tgz", + "integrity": "sha512-TCOZ6V+7eQblLNU7nz1/e4D6MYKc466aCoK24hcdcOLs7nhincesY9wZIwKi9XUrCM9gxemLb7pzV6YlRCbyEg==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, "license": "EPL-2.0", "dependencies": { - "@zowe/core-for-zowe-sdk": "8.26.0", - "@zowe/imperative": "8.26.0", - "@zowe/provisioning-for-zowe-sdk": "8.26.0", - "@zowe/zos-console-for-zowe-sdk": "8.26.0", - "@zowe/zos-files-for-zowe-sdk": "8.26.0", - "@zowe/zos-jobs-for-zowe-sdk": "8.26.0", - "@zowe/zos-logs-for-zowe-sdk": "8.26.0", - "@zowe/zos-tso-for-zowe-sdk": "8.26.0", - "@zowe/zos-uss-for-zowe-sdk": "8.26.0", - "@zowe/zos-workflows-for-zowe-sdk": "8.26.0", - "@zowe/zosmf-for-zowe-sdk": "8.26.0", + "@zowe/core-for-zowe-sdk": "8.26.2", + "@zowe/imperative": "8.26.2", + "@zowe/provisioning-for-zowe-sdk": "8.26.2", + "@zowe/zos-console-for-zowe-sdk": "8.26.2", + "@zowe/zos-files-for-zowe-sdk": "8.26.2", + "@zowe/zos-jobs-for-zowe-sdk": "8.26.2", + "@zowe/zos-logs-for-zowe-sdk": "8.26.2", + "@zowe/zos-tso-for-zowe-sdk": "8.26.2", + "@zowe/zos-uss-for-zowe-sdk": "8.26.2", + "@zowe/zos-workflows-for-zowe-sdk": "8.26.2", + "@zowe/zosmf-for-zowe-sdk": "8.26.2", "find-process": "1.4.7", "lodash": "4.17.21", "minimatch": "9.0.5", @@ -2506,9 +2506,9 @@ } }, "node_modules/@zowe/cli-test-utils": { - "version": "8.26.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.26.0.tgz", - "integrity": "sha512-5w3YFep2YnQvhYGEFl3toLYEQtb3bvcEwMlVM28ncXgjUIZG0ys8F6YGZKRxHxf/Ytmygf526WuWwE/bZGVcew==", + "version": "8.26.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@zowe/cli-test-utils/-/cli-test-utils-8.26.2.tgz", + "integrity": "sha512-0i+mkBm+jkHSPPMLOl8wHxGD5ad4zgRkjAtaBNpxMEY+SLPBvvJbA4L0aMODfZF5372LtWLDhe0dXqhEFDQdLg==", "dev": true, "license": "EPL-2.0", "dependencies": { @@ -3088,9 +3088,9 @@ "license": "MIT" }, "node_modules/@zowe/cli/node_modules/@zowe/core-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-PRgbyxzdugGNyeLX+G8uNNE6qqlnkI9YGyvK9ka5uE4ODFGEDGOHGfn695AHt8gUYVXSXJrBVDevvzJV38iKkQ==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/core-for-zowe-sdk/-/core-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-DCa9eh/p+CVhPhCsA8NijCxN4EfkswsEYXFuMa1Cv9HLohZnz4rn8EH6sAIp2N7QcCDOH4zaNs/zsW+h8XfDvQ==", "dev": true, "dependencies": { "comment-json": "~4.2.3", @@ -3104,9 +3104,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/imperative": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.26.0.tgz", - "integrity": "sha512-hW+eXxmUgZ0qBfYXzx1Ywk+4uk7uxCaeO8tgWCByX8OrbsgJkv71wKJ7zrQaobENOh6Cqk66EZqJZgx/a5M7rA==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/imperative/-/imperative-8.26.2.tgz", + "integrity": "sha512-/28ns2ze24Zx+avjN9i0xXWynlcUWZ4SPlfBz293elAw0cUaX7a1l3kkFuDp6iQHiKYDvWOBfTeEfnNhjnKbBQ==", "dev": true, "dependencies": { "@types/yargs": "^17.0.32", @@ -3232,9 +3232,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/provisioning-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-HETHGOKaCUk13GYbB11MfQzzkMDIS/tM4les5hn084j+t3u39ObBiiHUNMJp4WmkRIS01DICp7Mu1iEdoORoYg==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/provisioning-for-zowe-sdk/-/provisioning-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-xwFFU613NmTdauSl4xoEXoAwW1PppzE6AUKLrd00KaWSUlLKSAPgKbfLwUFQ57HlvLk7dUYwGxpzQEljHtgeVQ==", "dev": true, "dependencies": { "js-yaml": "^4.1.0" @@ -3259,9 +3259,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-console-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-tnx/7QxB3/1SS//x7+mKRpbHnoylSAyYPQqg35nOLmpw+HmOeEt7Z9ZWyWrZcNUf6a2TnV8mwEIc4Y2+IAIbZg==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-console-for-zowe-sdk/-/zos-console-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-2gLjlSr6ZFD7A7w1Xcq4PMj0g9idb41Phrljp0Xl0OVEf2jEiMqNiIK6R48XVCinheSMie7gHqzQPvAKH8zm+w==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3272,9 +3272,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-files-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-F71t7N3AKIQaGG/k6YXRJS0y95Mf9Yffke+hM4B9eFozoh5YvuHCRl9YBQyY4DJo418ZN/c5v6WyWSmatE0Oug==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-files-for-zowe-sdk/-/zos-files-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-xZDkpeEx4j4JpFxHvnGI3oELniYxMmBjjZPxYjsD8bYcMpULm4Og+aTFRJNKATZFwtctKHgd2Us1egJd4PUquA==", "dev": true, "dependencies": { "lodash": "^4.17.21", @@ -3289,12 +3289,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-jobs-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-DLscbxUJLX/kOdivTfNJiNWq9b9kBzoR7NVYXIsB4HzuM0bcstqFQgD+muXweacgtC5Qk+9mFqHQgGp3hoDpzQ==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-jobs-for-zowe-sdk/-/zos-jobs-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-6uMftKPwdrzPA0AfXUrhmyUKeD3hIp2ywg+mACyQCZQkGghrcXKnA95vBThDNLiQR9vVCbw9Gsdz2PWTO/nmWw==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.26.0" + "@zowe/zos-files-for-zowe-sdk": "8.26.2" }, "engines": { "node": ">=18.12.0" @@ -3305,9 +3305,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-logs-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-eIFX0IQpBX3VqfgdapiyjjTB5BRpiCUTwZvbDxfKY2cMSsXSq+ZN+l1sIxzHswAdbWa4hHIrYhR4JavjCJiXuQ==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-logs-for-zowe-sdk/-/zos-logs-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-KjTDhSAaZytBSKcTjXriPRtdijacfimow0+IkC/6wS3OA3kWethW/ADiaaAYn4lj4XElGNuIZgtsqCjmxK5U0g==", "dev": true, "engines": { "node": ">=18.12.0" @@ -3318,12 +3318,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-tso-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-d6fHTteYUHz4ckgiYq6D1/7/1TXpCsZwM158s2tplnU4Z7ec+ug+0dUDL1PEeCtOWB7qcJwuCwClStbfkCsc8w==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-tso-for-zowe-sdk/-/zos-tso-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-bpd0+E+A4JP8x+oV5nt66TU2JzJXbRIhgVzZa0SehamH1Ekt6ZmD7F9rncra51iUe6KplxNsqVoY5NauNxsSPg==", "dev": true, "dependencies": { - "@zowe/zosmf-for-zowe-sdk": "8.26.0" + "@zowe/zosmf-for-zowe-sdk": "8.26.2" }, "engines": { "node": ">=18.12.0" @@ -3334,9 +3334,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-uss-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-4JiTfSbRq/ETPZ6ZCAshiKpAxc1Wa0YxAzUxYladueZyTFm9CVQQUUD73VERwMHJqtV6gAN5RTlAinOLHDXs8Q==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-uss-for-zowe-sdk/-/zos-uss-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-sqmTPjHcLrgxJzM+RRc06G+4P98Vf1Em1b7b9CPsRuJ7177OFacG0x07rrCWnPEq2ifd8ktaHyQWtjVOoH1HJQ==", "dev": true, "dependencies": { "ssh2": "^1.15.0" @@ -3349,12 +3349,12 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zos-workflows-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-S59PqhSguIHSbE+geoV2UTqsDgJs0TFTKF36HFc/LQANfE2VwE/webkngq4Zgrmo2Nhdl2+z0mi/lSP7HPAabg==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zos-workflows-for-zowe-sdk/-/zos-workflows-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-4+fzPephFJfyY8PBuMYy7LJdmKSrm3TQZLyZ+LZaShxtPA7O5c8t86OuM08iKLVv0Aa+dR5KAN3cXO7bTmVgdg==", "dev": true, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.26.0" + "@zowe/zos-files-for-zowe-sdk": "8.26.2" }, "engines": { "node": ">=18.12.0" @@ -3365,9 +3365,9 @@ } }, "node_modules/@zowe/cli/node_modules/@zowe/zosmf-for-zowe-sdk": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.26.0.tgz", - "integrity": "sha512-KnK9jD+qFddImtrhB+LlSJiHCvStFhOMwPWbraGWnRoVXMH+X5bGxRdaiKZBLZRjygM4d6uU3ptcw/xSwr54Ng==", + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/@zowe/zosmf-for-zowe-sdk/-/zosmf-for-zowe-sdk-8.26.2.tgz", + "integrity": "sha512-iCQP82QLwrzwZ/x8ppLhUSPbwfWmUncgAzBSpYbELQZY/ttBIMXWOqiuoQoP6PK582SFz6kfLmnYeGYwWvthTw==", "dev": true, "engines": { "node": ">=18.12.0" @@ -7726,10 +7726,11 @@ } }, "node_modules/eslint-plugin-unused-imports": { - "version": "4.1.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", - "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "version": "4.2.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.2.0.tgz", + "integrity": "sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==", "dev": true, + "license": "MIT", "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 6194dacd1d..8b63522b59 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -51,18 +51,18 @@ "devDependencies": { "@eslint/js": "9.33.0", "@types/jest": "29.5.14", - "@types/node": "20.19.10", - "@typescript-eslint/eslint-plugin": "8.39.1", - "@typescript-eslint/parser": "8.39.1", - "@zowe/cli": "8.26.1", - "@zowe/cli-test-utils": "8.26.0", + "@types/node": "20.19.11", + "@typescript-eslint/eslint-plugin": "8.40.0", + "@typescript-eslint/parser": "8.40.0", + "@zowe/cli": "8.26.2", + "@zowe/cli-test-utils": "8.26.2", "@zowe/imperative": "8.26.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", "eslint": "9.33.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", - "eslint-plugin-unused-imports": "4.1.4", + "eslint-plugin-unused-imports": "4.2.0", "globals": "15.15.0", "husky": "9.1.7", "jest": "29.7.0", @@ -79,7 +79,7 @@ "typescript": "5.9.2" }, "overrides": { - "@babel/traverse": "7.28.0" + "@babel/traverse": "7.28.3" }, "peerDependencies": { "@zowe/imperative": "8.26.2" From 8abe0395308a24d6dd25f66e44c095ff7731d3b9 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:41:20 +0200 Subject: [PATCH 071/152] chore: remove twisted (#4280) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- onboarding-enabler-python-sample-app/Pipfile | 1 - .../Pipfile.lock | 1949 +++++++++-------- .../README.md | 13 +- 3 files changed, 998 insertions(+), 965 deletions(-) diff --git a/onboarding-enabler-python-sample-app/Pipfile b/onboarding-enabler-python-sample-app/Pipfile index cd1fa839b0..0e4428f438 100644 --- a/onboarding-enabler-python-sample-app/Pipfile +++ b/onboarding-enabler-python-sample-app/Pipfile @@ -103,7 +103,6 @@ tomli = ">=2.2.1" tornado = ">=6.4.2" traitlets = ">=5.14.3" trio = ">=0.29.0" -twisted = ">=24.11.0" typeguard = ">=4.3.0" typing-extensions = ">=4.12.2" urllib3 = ">=2.3.0" diff --git a/onboarding-enabler-python-sample-app/Pipfile.lock b/onboarding-enabler-python-sample-app/Pipfile.lock index 8f824b1c7e..a06f961d16 100644 --- a/onboarding-enabler-python-sample-app/Pipfile.lock +++ b/onboarding-enabler-python-sample-app/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "57fd59a3e38ffbbd528eba08aaa9123ba6dbd003f85daf8163d5c6e1cc605658" + "sha256": "b3123a4848d5cdfcd209e4e2d64c458a968ffcbbb04fb7a45d1db0b3dceef75c" }, "pipfile-spec": 6, "requires": { @@ -27,100 +27,105 @@ }, "aiohttp": { "hashes": [ - "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717", - "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d", - "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8", - "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", - "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421", - "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", - "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d", - "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8", - "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3", - "sha256:1596ebf17e42e293cbacc7a24c3e0dc0f8f755b40aff0402cb74c1ff6baec1d3", - "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361", - "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137", - "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b", - "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd", - "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8", - "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", - "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d", - "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", - "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", - "sha256:3cec21dd68924179258ae14af9f5418c1ebdbba60b98c667815891293902e5e0", - "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756", - "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", - "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9", - "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", - "sha256:46533e6792e1410f9801d09fd40cbbff3f3518d1b501d6c3c5b218f427f6ff08", - "sha256:469ac32375d9a716da49817cd26f1916ec787fc82b151c1c832f58420e6d3533", - "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811", - "sha256:5199be2a2f01ffdfa8c3a6f5981205242986b9e63eb8ae03fd18f736e4840721", - "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118", - "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55", - "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609", - "sha256:5bc0ae0a5e9939e423e065a3e5b00b24b8379f1db46046d7ab71753dfc7dd0e1", - "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66", - "sha256:5d61df4a05476ff891cff0030329fee4088d40e4dc9b013fac01bc3c745542c2", - "sha256:5e7007b8d1d09bce37b54111f593d173691c530b80f27c6493b928dabed9e6ef", - "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", - "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", - "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804", - "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f", - "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94", - "sha256:7ccec9e72660b10f8e283e91aa0295975c7bd85c204011d9f5eb69310555cf30", - "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", - "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1", - "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", - "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f", - "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4", - "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f", - "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6", - "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4", - "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f", - "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7", - "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421", - "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e", - "sha256:a2fd04ae4971b914e54fe459dd7edbbd3f2ba875d69e057d5e3c8e8cac094935", - "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c", - "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", - "sha256:ad2f41203e2808616292db5d7170cccf0c9f9c982d02544443c7eb0296e8b0c7", - "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868", - "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", - "sha256:b2f317d1678002eee6fe85670039fb34a757972284614638f82b903a03feacdc", - "sha256:b426495fb9140e75719b3ae70a5e8dd3a79def0ae3c6c27e012fc59f16544a4a", - "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643", - "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01", - "sha256:c1b90407ced992331dd6d4f1355819ea1c274cc1ee4d5b7046c6761f9ec11829", - "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93", - "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9", - "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78", - "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", - "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd", - "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", - "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2", - "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6", - "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261", - "sha256:eab7b040a8a873020113ba814b7db7fa935235e4cbaf8f3da17671baa1024863", - "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1", - "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb", - "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415", - "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000", - "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1", - "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7", - "sha256:fe7cdd3f7d1df43200e1c80f1aed86bb36033bf65e3c7cf46a2b97a253ef8798" + "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", + "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", + "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", + "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263", + "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142", + "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6", + "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", + "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09", + "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", + "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", + "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", + "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", + "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", + "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", + "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", + "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", + "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75", + "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", + "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", + "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", + "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", + "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", + "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", + "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", + "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", + "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf", + "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", + "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", + "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", + "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", + "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", + "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02", + "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", + "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05", + "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", + "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", + "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", + "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98", + "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", + "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", + "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", + "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89", + "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", + "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", + "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", + "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", + "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", + "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", + "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", + "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", + "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8", + "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", + "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406", + "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", + "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", + "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", + "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", + "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", + "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", + "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", + "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", + "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", + "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", + "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", + "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", + "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530", + "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", + "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d", + "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", + "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", + "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54", + "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d", + "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", + "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", + "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", + "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", + "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", + "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", + "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", + "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0", + "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", + "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", + "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", + "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", + "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", + "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.11.18" + "version": "==3.12.15" }, "aiosignal": { "hashes": [ - "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", - "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54" + "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.3.2" + "version": "==1.4.0" }, "annotated-types": { "hashes": [ @@ -133,12 +138,12 @@ }, "anyio": { "hashes": [ - "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", - "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c" + "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", + "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.9.0" + "version": "==4.10.0" }, "appnope": { "hashes": [ @@ -234,120 +239,107 @@ }, "certifi": { "hashes": [ - "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", - "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" + "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", + "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2025.4.26" + "markers": "python_version >= '3.7'", + "version": "==2025.8.3" }, "charset-normalizer": { "hashes": [ - "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", - "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", - "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", - "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", - "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", - "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", - "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", - "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", - "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", - "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", - "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", - "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", - "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", - "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", - "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", - "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", - "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", - "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", - "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", - "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", - "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", - "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", - "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", - "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", - "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", - "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", - "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", - "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", - "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", - "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", - "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", - "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", - "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", - "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", - "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", - "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", - "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", - "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", - "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", - "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", - "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", - "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", - "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", - "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", - "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", - "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", - "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", - "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", - "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", - "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", - "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", - "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", - "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", - "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", - "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", - "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", - "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", - "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", - "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", - "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", - "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", - "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", - "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", - "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", - "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", - "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", - "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", - "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", - "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", - "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", - "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", - "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", - "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", - "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", - "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", - "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", - "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", - "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", - "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", - "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", - "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", - "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", - "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", - "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", - "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", - "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", - "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", - "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", - "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", - "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", - "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", - "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" + "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", + "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", + "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", + "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", + "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", + "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", + "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", + "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", + "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", + "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", + "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", + "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", + "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", + "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", + "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", + "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", + "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", + "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", + "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", + "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", + "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", + "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", + "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", + "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", + "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", + "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", + "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", + "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", + "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", + "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", + "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", + "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", + "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", + "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", + "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", + "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", + "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", + "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", + "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", + "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", + "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", + "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", + "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", + "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", + "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", + "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", + "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", + "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", + "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", + "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", + "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", + "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", + "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", + "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", + "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", + "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", + "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", + "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", + "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", + "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", + "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", + "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", + "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", + "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", + "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", + "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", + "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", + "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", + "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", + "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", + "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", + "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", + "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", + "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", + "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", + "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", + "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", + "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", + "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.4.2" + "version": "==3.4.3" }, "click": { "hashes": [ - "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", - "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d" + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==8.2.0" + "version": "==8.2.1" }, "colorama": { "hashes": [ @@ -421,202 +413,201 @@ }, "fastapi": { "hashes": [ - "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", - "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d" + "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", + "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.115.12" + "version": "==0.116.1" }, "fastjsonschema": { "hashes": [ - "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", - "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667" + "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", + "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de" ], "index": "pypi", - "version": "==2.21.1" + "version": "==2.21.2" }, "flask": { "hashes": [ - "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", - "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e" + "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", + "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.1.1" + "version": "==3.1.2" }, "frozenlist": { "hashes": [ - "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117", - "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2", - "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42", - "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812", - "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3", - "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a", - "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572", - "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa", - "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b", - "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626", - "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e", - "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d", - "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c", - "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530", - "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", - "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e", - "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869", - "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd", - "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603", - "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606", - "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85", - "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64", - "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f", - "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0", - "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c", - "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4", - "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", - "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02", - "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", - "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0", - "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e", - "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791", - "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983", - "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f", - "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff", - "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8", - "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860", - "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8", - "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", - "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f", - "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba", - "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24", - "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e", - "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd", - "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911", - "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c", - "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595", - "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c", - "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", - "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2", - "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75", - "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4", - "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0", - "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe", - "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249", - "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", - "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", - "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b", - "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", - "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", - "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584", - "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497", - "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f", - "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f", - "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b", - "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", - "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d", - "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a", - "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", - "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", - "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189", - "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", - "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", - "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", - "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0", - "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3", - "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", - "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0", - "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215", - "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", - "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c", - "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", - "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1", - "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769", - "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506", - "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3", - "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348", - "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad", - "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a", - "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad", - "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6", - "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45", - "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188", - "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e", - "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", - "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70", - "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1", - "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106", - "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd", - "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc", - "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352", - "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91", - "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1", - "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f" + "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", + "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", + "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", + "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", + "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6", + "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", + "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", + "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", + "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677", + "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", + "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", + "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", + "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", + "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", + "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", + "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", + "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", + "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938", + "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", + "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", + "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", + "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", + "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", + "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", + "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", + "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", + "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", + "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", + "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", + "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", + "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", + "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", + "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63", + "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", + "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", + "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", + "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", + "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", + "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", + "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", + "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", + "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", + "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", + "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", + "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", + "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", + "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", + "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", + "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", + "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87", + "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", + "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", + "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71", + "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", + "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", + "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2", + "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", + "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", + "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", + "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", + "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", + "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878", + "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", + "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890", + "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", + "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", + "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb", + "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", + "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", + "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", + "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", + "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", + "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", + "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", + "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", + "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", + "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", + "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e", + "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", + "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", + "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", + "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", + "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", + "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", + "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb", + "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", + "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", + "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", + "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630", + "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", + "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", + "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", + "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44", + "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319", + "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", + "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", + "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35", + "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", + "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", + "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd", + "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", + "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", + "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", + "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.6.0" + "version": "==1.7.0" }, "greenlet": { "hashes": [ - "sha256:00cd814b8959b95a546e47e8d589610534cfb71f19802ea8a2ad99d95d702057", - "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", - "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", - "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", - "sha256:0a16fb934fcabfdfacf21d79e6fed81809d8cd97bc1be9d9c89f0e4567143d7b", - "sha256:1592a615b598643dbfd566bac8467f06c8c8ab6e56f069e573832ed1d5d528cc", - "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", - "sha256:1e4747712c4365ef6765708f948acc9c10350719ca0545e362c24ab973017370", - "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", - "sha256:1f72667cc341c95184f1c68f957cb2d4fc31eef81646e8e59358a10ce6689457", - "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", - "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", - "sha256:3091bc45e6b0c73f225374fefa1536cd91b1e987377b12ef5b19129b07d93ebe", - "sha256:354f67445f5bed6604e493a06a9a49ad65675d3d03477d38a4db4a427e9aad0e", - "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", - "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", - "sha256:3aeca9848d08ce5eb653cf16e15bb25beeab36e53eb71cc32569f5f3afb2a3aa", - "sha256:44671c29da26539a5f142257eaba5110f71887c24d40df3ac87f1117df589e0e", - "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", - "sha256:4bd139e4943547ce3a56ef4b8b1b9479f9e40bb47e72cc906f0f66b9d0d5cab3", - "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", - "sha256:6629311595e3fe7304039c67f00d145cd1d38cf723bb5b99cc987b23c1433d61", - "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", - "sha256:71566302219b17ca354eb274dfd29b8da3c268e41b646f330e324e3967546a74", - "sha256:7409796591d879425997a518138889d8d17e63ada7c99edc0d7a1c22007d4907", - "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", - "sha256:7791dcb496ec53d60c7f1c78eaa156c21f402dda38542a00afc3e20cae0f480f", - "sha256:782743700ab75716650b5238a4759f840bb2dcf7bff56917e9ffdf9f1f23ec59", - "sha256:7c9896249fbef2c615853b890ee854f22c671560226c9221cfd27c995db97e5c", - "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", - "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", - "sha256:8cb8553ee954536500d88a1a2f58fcb867e45125e600e80f586ade399b3f8819", - "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", - "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", - "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", - "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", - "sha256:a8fa80665b1a29faf76800173ff5325095f3e66a78e62999929809907aca5659", - "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", - "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", - "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", - "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", - "sha256:bf3fc9145141250907730886b031681dfcc0de1c158f3cc51c092223c0f381ce", - "sha256:c23ea227847c9dbe0b3910f5c0dd95658b607137614eb821e6cbaecd60d81cc6", - "sha256:c3cc1a3ed00ecfea8932477f729a9f616ad7347a5e55d50929efa50a86cb7be7", - "sha256:c49e9f7c6f625507ed83a7485366b46cbe325717c60837f7244fc99ba16ba9d6", - "sha256:d0cb7d47199001de7658c213419358aa8937df767936506db0db7ce1a71f4a2f", - "sha256:d8009ae46259e31bc73dc183e402f548e980c96f33a6ef58cc2e7865db012e13", - "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", - "sha256:dcb9cebbf3f62cb1e5afacae90761ccce0effb3adaa32339a0670fe7805d8068", - "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", - "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", - "sha256:eeb27bece45c0c2a5842ac4c5a1b5c2ceaefe5711078eed4e8043159fa05c834", - "sha256:efcdfb9df109e8a3b475c016f60438fcd4be68cd13a365d42b35914cdab4bb2b", - "sha256:fd9fb7c941280e2c837b603850efc93c999ae58aae2b40765ed682a6907ebbc5", - "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421" + "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", + "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", + "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", + "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", + "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", + "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", + "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", + "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", + "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", + "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", + "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", + "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", + "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", + "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", + "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", + "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", + "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", + "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", + "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", + "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", + "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", + "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", + "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", + "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", + "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", + "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", + "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", + "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", + "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", + "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", + "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", + "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", + "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", + "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", + "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", + "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", + "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", + "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", + "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", + "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", + "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", + "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", + "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", + "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", + "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", + "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", + "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", + "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", + "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", + "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", + "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", + "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", + "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", + "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.2.2" + "version": "==3.2.4" }, "h11": { "hashes": [ @@ -708,12 +699,12 @@ }, "ipython": { "hashes": [ - "sha256:62a9373dbc12f28f9feaf4700d052195bf89806279fc8ca11f3f54017d04751b", - "sha256:fef5e33c4a1ae0759e0bba5917c9db4eb8c53fee917b6a526bd973e1ca5159f6" + "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", + "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270" ], "index": "pypi", "markers": "python_version >= '3.11'", - "version": "==9.2.0" + "version": "==9.4.0" }, "ipython-pygments-lexers": { "hashes": [ @@ -752,12 +743,12 @@ }, "jsonschema": { "hashes": [ - "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", - "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" + "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", + "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.23.0" + "markers": "python_version >= '3.9'", + "version": "==4.25.1" }, "jsonschema-specifications": { "hashes": [ @@ -779,12 +770,12 @@ }, "jupyter-core": { "hashes": [ - "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", - "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9" + "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", + "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.7.2" + "version": "==5.8.1" }, "jupyterlab-pygments": { "hashes": [ @@ -892,114 +883,120 @@ }, "multidict": { "hashes": [ - "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", - "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", - "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef", - "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", - "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", - "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8", - "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", - "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", - "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", - "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", - "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", - "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", - "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", - "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", - "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9", - "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", - "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e", - "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b", - "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", - "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", - "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", - "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", - "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", - "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", - "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", - "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5", - "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", - "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", - "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", - "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", - "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666", - "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", - "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf", - "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", - "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", - "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", - "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", - "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", - "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", - "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21", - "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7", - "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", - "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343", - "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", - "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", - "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", - "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", - "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459", - "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6", - "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", - "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", - "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", - "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", - "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", - "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", - "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", - "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5", - "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", - "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", - "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1", - "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", - "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7", - "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", - "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", - "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39", - "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752", - "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", - "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188", - "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451", - "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078", - "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7", - "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", - "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f", - "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", - "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f", - "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", - "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291", - "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897", - "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887", - "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", - "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685", - "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", - "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", - "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", - "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", - "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", - "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", - "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", - "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", - "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", - "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df", - "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", - "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", - "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", - "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c", - "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840", - "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", - "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", - "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", - "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", - "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e", - "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2", - "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1", - "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad" + "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", + "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", + "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", + "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", + "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", + "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", + "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f", + "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1", + "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", + "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", + "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", + "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", + "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae", + "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", + "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", + "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", + "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", + "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", + "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", + "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0", + "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", + "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", + "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", + "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", + "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0", + "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", + "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", + "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", + "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", + "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", + "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", + "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", + "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", + "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", + "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", + "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", + "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", + "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", + "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb", + "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", + "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", + "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", + "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", + "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", + "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", + "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", + "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb", + "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", + "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", + "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", + "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", + "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", + "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", + "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", + "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", + "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", + "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", + "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", + "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", + "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", + "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b", + "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", + "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", + "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", + "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", + "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", + "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978", + "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", + "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", + "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", + "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", + "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", + "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", + "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4", + "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665", + "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", + "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", + "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", + "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", + "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", + "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17", + "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", + "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", + "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", + "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", + "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", + "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", + "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", + "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", + "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", + "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", + "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd", + "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", + "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", + "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", + "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", + "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", + "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", + "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", + "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", + "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", + "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb", + "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210", + "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53", + "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", + "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", + "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9", + "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", + "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a", + "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==6.4.3" + "version": "==6.6.4" }, "nbclient": { "hashes": [ @@ -1082,20 +1079,20 @@ }, "pip": { "hashes": [ - "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", - "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077" + "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", + "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717" ], "markers": "python_version >= '3.9'", - "version": "==25.1.1" + "version": "==25.2" }, "pipdeptree": { "hashes": [ - "sha256:3849d62a2ed641256afac3058c4f9b85ac4a47e9d8c991ee17a8f3d230c5cffb", - "sha256:92a8f37ab79235dacb46af107e691a1309ca4a429315ba2a1df97d1cd56e27ac" + "sha256:a4ce80e7fdf93dfc6d99809d9bbe0115a13902b1f0498775abeafe56c77bdd3d", + "sha256:bae533e30249b1aa6d9cb315ef6f1c039e9adaa55d5b25438395cace5716eaa6" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.26.1" + "version": "==2.28.0" }, "platformdirs": { "hashes": [ @@ -1126,108 +1123,108 @@ }, "propcache": { "hashes": [ - "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", - "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", - "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf", - "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", - "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5", - "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c", - "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", - "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", - "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", - "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", - "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", - "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42", - "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035", - "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0", - "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e", - "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46", - "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", - "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", - "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", - "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", - "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", - "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", - "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", - "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833", - "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259", - "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136", - "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", - "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", - "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", - "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", - "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f", - "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", - "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", - "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", - "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566", - "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", - "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908", - "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf", - "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", - "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64", - "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", - "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71", - "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", - "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", - "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", - "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5", - "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894", - "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe", - "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", - "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", - "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", - "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", - "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641", - "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", - "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649", - "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", - "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd", - "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", - "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", - "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229", - "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", - "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", - "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", - "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", - "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", - "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294", - "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", - "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", - "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", - "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", - "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", - "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7", - "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", - "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", - "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", - "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", - "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7", - "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519", - "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", - "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180", - "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", - "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", - "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", - "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", - "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", - "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", - "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", - "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6", - "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c", - "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", - "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", - "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98", - "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", - "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", - "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", - "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", - "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", - "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5" + "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", + "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", + "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", + "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", + "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", + "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", + "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", + "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", + "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", + "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4", + "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", + "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", + "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea", + "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", + "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2", + "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", + "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", + "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", + "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb", + "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", + "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef", + "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe", + "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", + "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", + "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", + "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", + "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", + "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", + "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", + "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1", + "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", + "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", + "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", + "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", + "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", + "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", + "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", + "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", + "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", + "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", + "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", + "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", + "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", + "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", + "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", + "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", + "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", + "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", + "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701", + "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9", + "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", + "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", + "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", + "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", + "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", + "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", + "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", + "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", + "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", + "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", + "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", + "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", + "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", + "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", + "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", + "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", + "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5", + "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", + "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1", + "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", + "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", + "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", + "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", + "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", + "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", + "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", + "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", + "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", + "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", + "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b", + "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", + "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", + "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", + "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec", + "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886", + "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb", + "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", + "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", + "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d", + "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", + "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", + "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", + "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", + "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", + "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", + "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", + "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", + "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.3.1" + "version": "==0.3.2" }, "ptyprocess": { "hashes": [ @@ -1265,12 +1262,12 @@ }, "pydantic": { "hashes": [ - "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", - "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb" + "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", + "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.11.4" + "version": "==2.11.7" }, "pydantic-core": { "hashes": [ @@ -1380,39 +1377,39 @@ }, "pygments": { "hashes": [ - "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", - "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.19.1" + "version": "==2.19.2" }, "pytest": { "hashes": [ - "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", - "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845" + "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", + "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==8.3.5" + "markers": "python_version >= '3.9'", + "version": "==8.4.1" }, "pytest-asyncio": { "hashes": [ - "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", - "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f" + "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", + "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.26.0" + "version": "==1.1.0" }, "pytest-mock": { "hashes": [ - "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", - "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" + "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", + "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.14.0" + "version": "==3.14.1" }, "pytest-tornasync": { "hashes": [ @@ -1511,103 +1508,102 @@ }, "pyzmq": { "hashes": [ - "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918", - "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be", - "sha256:0a294026e28679a8dd64c922e59411cb586dad307661b4d8a5c49e7bbca37621", - "sha256:0a744ce209ecb557406fb928f3c8c55ce79b16c3eeb682da38ef5059a9af0848", - "sha256:1410c3a3705db68d11eb2424d75894d41cff2f64d948ffe245dd97a9debfebf4", - "sha256:14fc678b696bc42c14e2d7f86ac4e97889d5e6b94d366ebcb637a768d2ad01af", - "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101", - "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08", - "sha256:22c8dd677274af8dfb1efd05006d6f68fb2f054b17066e308ae20cb3f61028cf", - "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf", - "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b", - "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", - "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637", - "sha256:2ea81823840ef8c56e5d2f9918e4d571236294fea4d1842b302aebffb9e40997", - "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0", - "sha256:2f9f7ffe9db1187a253fca95191854b3fda24696f086e8789d1d449308a34b88", - "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b", - "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e", - "sha256:3709c9ff7ba61589b7372923fd82b99a81932b592a5c7f1a24147c91da9a68d6", - "sha256:382a4a48c8080e273427fc692037e3f7d2851959ffe40864f2db32646eeb3cef", - "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315", - "sha256:41a2508fe7bed4c76b4cf55aacfb8733926f59d440d9ae2b81ee8220633b4d12", - "sha256:43b03c1ceea27c6520124f4fb2ba9c647409b9abdf9a62388117148a90419494", - "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84", - "sha256:445c97854204119ae2232503585ebb4fa7517142f71092cb129e5ee547957a1f", - "sha256:4478b14cb54a805088299c25a79f27eaf530564a7a4f72bf432a040042b554eb", - "sha256:4550af385b442dc2d55ab7717837812799d3674cb12f9a3aa897611839c18e9e", - "sha256:49b6ca2e625b46f499fb081aaf7819a177f41eeb555acb05758aa97f4f95d147", - "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", - "sha256:51d18be6193c25bd229524cfac21e39887c8d5e0217b1857998dfbef57c070a4", - "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b", - "sha256:552b0d2e39987733e1e9e948a0ced6ff75e0ea39ab1a1db2fc36eb60fd8760db", - "sha256:6145df55dc2309f6ef72d70576dcd5aabb0fd373311613fe85a5e547c722b780", - "sha256:61c5f93d7622d84cb3092d7f6398ffc77654c346545313a3737e266fc11a3beb", - "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771", - "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63", - "sha256:6bab961c8c9b3a4dc94d26e9b2cdf84de9918931d01d6ff38c721a83ab3c0ef5", - "sha256:6d52d62edc96787f5c1dfa6c6ccff9b581cfae5a70d94ec4c8da157656c73b5b", - "sha256:7731abd23a782851426d4e37deb2057bf9410848a4459b5ede4fe89342e687a9", - "sha256:7a5c09413b924d96af2aa8b57e76b9b0058284d60e2fc3730ce0f979031d162a", - "sha256:7d489ac234d38e57f458fdbd12a996bfe990ac028feaf6f3c1e81ff766513d3b", - "sha256:7dacb06a9c83b007cc01e8e5277f94c95c453c5851aac5e83efe93e72226353f", - "sha256:807b8f4ad3e6084412c0f3df0613269f552110fa6fb91743e3e306223dbf11a6", - "sha256:80c9b48aef586ff8b698359ce22f9508937c799cc1d2c9c2f7c95996f2300c94", - "sha256:8112af16c406e4a93df2caef49f884f4c2bb2b558b0b5577ef0b2465d15c1abc", - "sha256:831cc53bf6068d46d942af52fa8b0b9d128fb39bcf1f80d468dc9a3ae1da5bfb", - "sha256:8a28ac29c60e4ba84b5f58605ace8ad495414a724fe7aceb7cf06cd0598d04e1", - "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43", - "sha256:91c3ffaea475ec8bb1a32d77ebc441dcdd13cd3c4c284a6672b92a0f5ade1917", - "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4", - "sha256:9434540f333332224ecb02ee6278b6c6f11ea1266b48526e73c903119b2f420f", - "sha256:963977ac8baed7058c1e126014f3fe58b3773f45c78cce7af5c26c09b6823896", - "sha256:98d948288ce893a2edc5ec3c438fe8de2daa5bbbd6e2e865ec5f966e237084ba", - "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0", - "sha256:a651fe2f447672f4a815e22e74630b6b1ec3a1ab670c95e5e5e28dcd4e69bbb5", - "sha256:a88643de8abd000ce99ca72056a1a2ae15881ee365ecb24dd1d9111e43d57842", - "sha256:a9f34f5c9e0203ece706a1003f1492a56c06c0632d86cb77bcfe77b56aacf27b", - "sha256:acae207d4387780838192326b32d373bb286da0b299e733860e96f80728eb0af", - "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b", - "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c", - "sha256:b4f6919d9c120488246bdc2a2f96662fa80d67b35bd6d66218f457e722b3ff64", - "sha256:b70cab356ff8c860118b89dc86cd910c73ce2127eb986dada4fbac399ef644cf", - "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44", - "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d", - "sha256:bfcf82644c9b45ddd7cd2a041f3ff8dce4a0904429b74d73a439e8cab1bd9e54", - "sha256:c01d109dd675ac47fa15c0a79d256878d898f90bc10589f808b62d021d2e653c", - "sha256:c0c8e8cadc81e44cc5088fcd53b9b3b4ce9344815f6c4a03aec653509296fae3", - "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3", - "sha256:c76c298683f82669cab0b6da59071f55238c039738297c69f187a542c6d40099", - "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8", - "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a", - "sha256:cc2abc385dc37835445abe206524fbc0c9e3fce87631dfaa90918a1ba8f425eb", - "sha256:ccdff8ac4246b6fb60dcf3982dfaeeff5dd04f36051fe0632748fc0aa0679c01", - "sha256:d1ef0a536662bbbdc8525f7e2ef19e74123ec9c4578e0582ecd41aedc414a169", - "sha256:d367b7b775a0e1e54a59a2ba3ed4d5e0a31566af97cc9154e34262777dab95ed", - "sha256:d4000e8255d6cbce38982e5622ebb90823f3409b7ffe8aeae4337ef7d6d2612a", - "sha256:d56aad0517d4c09e3b4f15adebba8f6372c5102c27742a5bdbfc74a7dceb8fca", - "sha256:d9a78a52668bf5c9e7b0da36aa5760a9fc3680144e1445d68e98df78a25082ed", - "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30", - "sha256:dd670a8aa843f2ee637039bbd412e0d7294a5e588e1ecc9ad98b0cdc050259a4", - "sha256:dea1c8db78fb1b4b7dc9f8e213d0af3fc8ecd2c51a1d5a3ca1cde1bda034a980", - "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f", - "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905", - "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6", - "sha256:e6c6f0a23e55cd38d27d4c89add963294ea091ebcb104d7fdab0f093bc5abb1c", - "sha256:e9bcae3979b2654d5289d3490742378b2f3ce804b0b5fd42036074e2bf35b030", - "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5", - "sha256:f3f2a5b74009fd50b53b26f65daff23e9853e79aa86e0aa08a53a7628d92d44a", - "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86", - "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b", - "sha256:f8f3c30fb2d26ae5ce36b59768ba60fb72507ea9efc72f8f69fa088450cff1df", - "sha256:f928eafd15794aa4be75463d537348b35503c1e014c5b663f206504ec1a90fe4", - "sha256:fa59e1f5a224b5e04dc6c101d7186058efa68288c2d714aa12d27603ae93318b" + "sha256:05a94233fdde585eb70924a6e4929202a747eea6ed308a6171c4f1c715bbe39e", + "sha256:092f4011b26d6b0201002f439bd74b38f23f3aefcb358621bdc3b230afc9b2d5", + "sha256:0ec09073ed67ae236785d543df3b322282acc0bdf6d1b748c3e81f3043b21cb5", + "sha256:0f772eea55cccce7f45d6ecdd1d5049c12a77ec22404f6b892fae687faa87bee", + "sha256:0fc24bf45e4a454e55ef99d7f5c8b8712539200ce98533af25a5bfa954b6b390", + "sha256:119ce8590409702394f959c159d048002cbed2f3c0645ec9d6a88087fc70f0f1", + "sha256:1843fd0daebcf843fe6d4da53b8bdd3fc906ad3e97d25f51c3fed44436d82a49", + "sha256:19dce6c93656f9c469540350d29b128cd8ba55b80b332b431b9a1e9ff74cfd01", + "sha256:1c363c6dc66352331d5ad64bb838765c6692766334a6a02fdb05e76bd408ae18", + "sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a", + "sha256:1da8e645c655d86f0305fb4c65a0d848f461cd90ee07d21f254667287b5dbe50", + "sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55", + "sha256:27a78bdd384dbbe7b357af95f72efe8c494306b5ec0a03c31e2d53d6763e5307", + "sha256:2852f67371918705cc18b321695f75c5d653d5d8c4a9b946c1eec4dab2bd6fdf", + "sha256:313a7b374e3dc64848644ca348a51004b41726f768b02e17e689f1322366a4d9", + "sha256:351bf5d8ca0788ca85327fda45843b6927593ff4c807faee368cc5aaf9f809c2", + "sha256:4401649bfa0a38f0f8777f8faba7cd7eb7b5b8ae2abc7542b830dd09ad4aed0d", + "sha256:44909aa3ed2234d69fe81e1dade7be336bcfeab106e16bdaa3318dcde4262b93", + "sha256:45c3e00ce16896ace2cd770ab9057a7cf97d4613ea5f2a13f815141d8b6894b9", + "sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b", + "sha256:497bd8af534ae55dc4ef67eebd1c149ff2a0b0f1e146db73c8b5a53d83c1a5f5", + "sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb", + "sha256:4bca8abc31799a6f3652d13f47e0b0e1cab76f9125f2283d085a3754f669b607", + "sha256:4c3874344fd5fa6d58bb51919708048ac4cab21099f40a227173cddb76b4c20b", + "sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1", + "sha256:5268a5a9177afff53dc6d70dffe63114ba2a6e7b20d9411cc3adeba09eeda403", + "sha256:544b995a6a1976fad5d7ff01409b4588f7608ccc41be72147700af91fd44875d", + "sha256:56a3b1853f3954ec1f0e91085f1350cc57d18f11205e4ab6e83e4b7c414120e0", + "sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed", + "sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb", + "sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e", + "sha256:64ca3c7c614aefcdd5e358ecdd41d1237c35fe1417d01ec0160e7cdb0a380edc", + "sha256:678e50ec112bdc6df5a83ac259a55a4ba97a8b314c325ab26b3b5b071151bc61", + "sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1", + "sha256:6dcbcb34f5c9b0cefdfc71ff745459241b7d3cda5b27c7ad69d45afc0821d1e1", + "sha256:6f02f30a4a6b3efe665ab13a3dd47109d80326c8fd286311d1ba9f397dc5f247", + "sha256:70b719a130b81dd130a57ac0ff636dc2c0127c5b35ca5467d1b67057e3c7a4d2", + "sha256:72d235d6365ca73d8ce92f7425065d70f5c1e19baa458eb3f0d570e425b73a96", + "sha256:7418fb5736d0d39b3ecc6bec4ff549777988feb260f5381636d8bd321b653038", + "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811", + "sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f", + "sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94", + "sha256:87aebf4acd7249bdff8d3df03aed4f09e67078e6762cfe0aecf8d0748ff94cde", + "sha256:88dc92d9eb5ea4968123e74db146d770b0c8d48f0e2bfb1dbc6c50a8edb12d64", + "sha256:8c62297bc7aea2147b472ca5ca2b4389377ad82898c87cabab2a94aedd75e337", + "sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd", + "sha256:90a4da42aa322de8a3522461e3b5fe999935763b27f69a02fced40f4e3cf9682", + "sha256:95594b2ceeaa94934e3e94dd7bf5f3c3659cf1a26b1fb3edcf6e42dad7e0eaf2", + "sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296", + "sha256:9d16fdfd7d70a6b0ca45d36eb19f7702fa77ef6256652f17594fc9ce534c9da6", + "sha256:9d7b6b90da7285642f480b48c9efd1d25302fd628237d8f6f6ee39ba6b2d2d34", + "sha256:a066ea6ad6218b4c233906adf0ae67830f451ed238419c0db609310dd781fbe7", + "sha256:a27fa11ebaccc099cac4309c799aa33919671a7660e29b3e465b7893bc64ec81", + "sha256:a4aca06ba295aa78bec9b33ec028d1ca08744c36294338c41432b7171060c808", + "sha256:af2ee67b3688b067e20fea3fe36b823a362609a1966e7e7a21883ae6da248804", + "sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd", + "sha256:b007e5dcba684e888fbc90554cb12a2f4e492927c8c2761a80b7590209821743", + "sha256:b25e72e115399a4441aad322258fa8267b873850dc7c276e3f874042728c2b45", + "sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb", + "sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab", + "sha256:b9fd0fda730461f510cfd9a40fafa5355d65f5e3dbdd8d6dfa342b5b3f5d1949", + "sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41", + "sha256:be45a895f98877271e8a0b6cf40925e0369121ce423421c20fa6d7958dc753c2", + "sha256:bee5248d5ec9223545f8cc4f368c2d571477ae828c99409125c3911511d98245", + "sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f", + "sha256:c9180d1f5b4b73e28b64e63cc6c4c097690f102aa14935a62d5dd7426a4e5b5a", + "sha256:c96702e1082eab62ae583d64c4e19c9b848359196697e536a0c57ae9bd165bd5", + "sha256:c9d63d66059114a6756d09169c9209ffceabacb65b9cb0f66e6fc344b20b73e6", + "sha256:ce181dd1a7c6c012d0efa8ab603c34b5ee9d86e570c03415bbb1b8772eeb381c", + "sha256:d0356a21e58c3e99248930ff73cc05b1d302ff50f41a8a47371aefb04327378a", + "sha256:d0b96c30be9f9387b18b18b6133c75a7b1b0065da64e150fe1feb5ebf31ece1c", + "sha256:d2976b7079f09f48d59dc123293ed6282fca6ef96a270f4ea0364e4e54c8e855", + "sha256:d97b59cbd8a6c8b23524a8ce237ff9504d987dc07156258aa68ae06d2dd5f34d", + "sha256:da81512b83032ed6cdf85ca62e020b4c23dda87f1b6c26b932131222ccfdbd27", + "sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed", + "sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a", + "sha256:e4f22d67756518d71901edf73b38dc0eb4765cce22c8fe122cc81748d425262b", + "sha256:e648dca28178fc879c814cf285048dd22fd1f03e1104101106505ec0eea50a4d", + "sha256:e971d8680003d0af6020713e52f92109b46fedb463916e988814e04c8133578a", + "sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530", + "sha256:f293a1419266e3bf3557d1f8778f9e1ffe7e6b2c8df5c9dca191caf60831eb74", + "sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0", + "sha256:f44e7ea288d022d4bf93b9e79dafcb4a7aea45a3cbeae2116792904931cefccf", + "sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577", + "sha256:f65741cc06630652e82aa68ddef4986a3ab9073dd46d59f94ce5f005fa72037c", + "sha256:f8c3b74f1cd577a5a9253eae7ed363f88cbb345a990ca3027e9038301d47c7f4", + "sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808", + "sha256:f98f6b7787bd2beb1f0dde03f23a0621a0c978edf673b7d8f5e7bc039cbe1b60", + "sha256:fde26267416c8478c95432c81489b53f57b0b5d24cd5c8bfaebf5bbaac4dc90c", + "sha256:fe632fa4501154d58dfbe1764a0495734d55f84eaf1feda4549a1f1ca76659e9", + "sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686", + "sha256:ffe6b809a97ac6dea524b3b837d5b28743d8c2f121141056d168ff0ba8f614ef" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.4.0" + "version": "==27.0.1" }, "referencing": { "hashes": [ @@ -1620,141 +1616,182 @@ }, "requests": { "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.32.3" + "markers": "python_version >= '3.9'", + "version": "==2.32.5" }, "rpds-py": { "hashes": [ - "sha256:098d446d76d26e394b440d73921b49c1c90274d46ccbaadf346b1b78f9fdd4b1", - "sha256:0d63a86b457069d669c423f093db4900aa102f0e5a626973eff4db8355c0fd96", - "sha256:0dcdee07ebf76223092666c72a9552db276fbe46b98830ecd1bb836cc98adc81", - "sha256:0ee0cc81f875e853ccdf3badb44b67f771fb9149baa9e752777ccdcaf052ad26", - "sha256:113d134dc5a8d2503630ca2707b58a1bf5b1b3c69b35c7dab8690ee650c111b8", - "sha256:12a84c3851f9e68633d883c01347db3cb87e6160120a489f9c47162cd276b0a5", - "sha256:12b42790c91e0041a98f0ec04244fb334696938793e785a5d4c7e56ca534d7da", - "sha256:16fb28d3a653f67c871a47c5ca0be17bce9fab8adb8bcf7bd09f3771b8c4d860", - "sha256:1e11065b759c38c4945f8c9765ed2910e31fa5b2f7733401eb7d966f468367a2", - "sha256:20af08b0b2d5b196a2bcb70becf0b97ec5af579cee0ae6750b08a2eea3b6c77d", - "sha256:240251fd95b057c26f8538d0e673bf983eba4f38da95fbaf502bfc1a768b3984", - "sha256:2649ff19291928243f90c86e4dc9cd86c8c4c6a73c3693ba2e23bc2fbcd8338c", - "sha256:28bd2969445acc2d6801a22f97a43134ae3cb18e7495d668bfaa8d82b8526cdc", - "sha256:2bd08c82336412a39a598e5baccab2ee2d7bd54e9115c8b64f2febb45da5c368", - "sha256:2f91902fc0c95dd1fa6b30ebd2af83ace91e592f7fd6340a375588a9d4b9341b", - "sha256:35c8cb5dcf7d36d3adf2ae0730b60fb550a8feb6e432bee7ef84162a0d15714b", - "sha256:36a7564deaac3f372e8b8b701eb982ea3113516e8e08cd87e3dc6ccf29bad14b", - "sha256:3752a015db89ea3e9c04d5e185549be4aa29c1882150e094c614c0de8e788feb", - "sha256:383cf0d4288baf5a16812ed70d54ecb7f2064e255eb7fe42c38e926adeae4534", - "sha256:3a21f4584f69547ae03aaa21be98753e85599f3437b84039da5dc20b53abe987", - "sha256:3d7d65aa934899849628137ab391562cdb487c6ffb9b9781319a64a9c66afbce", - "sha256:469054e6b2f8e41f1fe62b95f617082019d343eddeec3219ff3909067e672fb9", - "sha256:4acbe2349a3baac9cc212005b6cb4bbb7e5b34538886cde4f55dfc29173da1d6", - "sha256:4ad37c29adc435e6d8b24be86b03596183ee8d4bb8580cc4c676879b0b896a99", - "sha256:4d97661bf5848dd9e5eb7ded480deccf9d32ce2cd500b88a26acbf7bd2864985", - "sha256:4e5fe366fa53bd6777cf5440245366705338587b2cf8d61348ddaad744eb591a", - "sha256:4fbec54cc42fa90ca69158d75f125febc4116b2d934e71c78f97de1388a8feb2", - "sha256:540cd89d256119845b7f8f56c4bb80cad280cab92d9ca473be49ea13e678fd44", - "sha256:542a6f1d0f400b9ce1facb3e30dd3dc84e4affc60353509b00a7bdcd064be91e", - "sha256:54f925ff8d4443b7cae23a5215954abbf4736a3404188bde53c4d744ac001d89", - "sha256:551897221bbc9de17bce4574810347db8ec1ba4ec2f50f35421790d34bdb6ef9", - "sha256:57e9616a2a9da08fe0994e37a0c6f578fbaf6d35911bcba31e99660542d60c45", - "sha256:587cad3959d3d85127cf5df1624cdce569bb3796372e00420baad46af7c56b9b", - "sha256:58cfaa54752d6d2b4f10e87571688dbb7792327a69eca5417373d77d42787058", - "sha256:5afbff2822016db3c696cb0c1432e6b1f0e34aa9280bc5184dc216812a24e70d", - "sha256:5b049dd0792d51f07193cd934acec89abe84d2607109e6ca223b2f0ff24f0c7d", - "sha256:5bbfbd9c74c4dd74815bd532bf29bedea6d27d38f35ef46f9754172a14e4c655", - "sha256:5e849315963eb08c26167d0f2c0f9319c9bd379daea75092b3c595d70be6209d", - "sha256:6065a489b7b284efb29d57adffae2b9b5e9403d3c8d95cfa04e04e024e6b4e77", - "sha256:637ec39f97e342a3f76af739eda96800549d92f3aa27a2170b6dcbdffd49f480", - "sha256:653a066d2a4a332d4f8a11813e8124b643fa7b835b78468087a9898140469eee", - "sha256:6587ece9f205097c62d0e3d3cb7c06991eb0083ab6a9cf48951ec49c2ab7183c", - "sha256:66087711faf29cb3ac8ab05341939aec29968626aff8ef18e483e229055dd9a7", - "sha256:66568caacf18542f0cf213db7adf3de2da6ad58c7bf2c4fafec0d81ae557443b", - "sha256:673ba018df5ae5e7b6c9a021d51ffe39c0ae1daa0041611ed27a0bca634b2d2e", - "sha256:6a1eda14db1ac7a2ab4536dfe69e4d37fdd765e8e784ae4451e61582ebb76012", - "sha256:6b0c0f671a53c129ea48f9481e95532579cc489ab5a0ffe750c9020787181c48", - "sha256:6b5f457afffb45d3804728a54083e31fbaf460e902e3f7d063e56d0d0814301e", - "sha256:6bcca4d0d24d8c37bfe0cafdaaf4346b6c516db21ccaad5c7fba0a0df818dfc9", - "sha256:6c27156c8d836e7ff760767e93245b286ae028bfd81d305db676662d1f642637", - "sha256:6c72a4a8fab10bc96720ad40941bb471e3b1150fb8d62dab205d495511206cf1", - "sha256:6d95521901896a90a858993bfa3ec0f9160d3d97e8c8fefc279b3306cdadfee0", - "sha256:7715597186a7277be12729c896019226321bad1f047da381ab707b177aa5017c", - "sha256:77814c7a4e1dc43fba73aeb4c1ef0fe37d901f3aa869a4823de5ea843a283fd0", - "sha256:77910d6bec321c9fccfe9cf5e407fed9d2c48a5e510473b4f070d5cf2413c003", - "sha256:7c18cb2f6805861dcdf11fb0b3c111a0335f6475411687db2f6636f32bed66b0", - "sha256:7d34547810bfd61acf8a441e8a3651e7a919e8e8aed29850be14a1b05cfc6f41", - "sha256:8029c19c8a32ef3093c417dd16a5f806e7f529fcceea7c627b2635e9da5104da", - "sha256:805a0dff0674baa3f360c21dcbc622ae544f2bb4753d87a4a56a1881252a477e", - "sha256:80b37b37525492250adc7cbca20ae7084f86eb3eb62414b624d2a400370853b1", - "sha256:8155e21203161e5c78791fc049b99f0bbbf14d1d1839c8c93c8344957f9e8e1e", - "sha256:837fd066f974e5b98c69ac83ec594b79a2724a39a92a157b8651615e5032e530", - "sha256:83e103b48e63fd2b8a8e2b21ab5b5299a7146045626c2ed4011511ea8122d217", - "sha256:85587479f210350e9d9d25e505f422dd636e561658382ee8947357a4bac491ad", - "sha256:864573b6440b770db5a8693547a8728d7fd32580d4903010a8eee0bb5b03b130", - "sha256:87c6ff87b38f46d712418d78b34db1198408a3d9a42eddc640644aea561216b1", - "sha256:89260601d497fa5957c3e46f10b16cfa2a4808ad4dd46cddc0b997461923a7d9", - "sha256:89bb2b20829270aca28b1e5481be8ee24cb9aa86e6c0c81cb4ada2112c9588c5", - "sha256:8abc1a3e29b599bf8bb5ad455256a757e8b0ed5621e7e48abe8209932dc6d11e", - "sha256:8c2ad59c4342a176cb3e0d5753e1c911eabc95c210fc6d0e913c32bf560bf012", - "sha256:8f3a57f08c558d0983a708bfe6d1265f47b5debff9b366b2f2091690fada055c", - "sha256:90dbd2c42cb6463c07020695800ae8f347e7dbeff09da2975a988e467b624539", - "sha256:91a51499be506022b9f09facfc42f0c3a1c45969c0fc8f0bbebc8ff23ab9e531", - "sha256:9442cbff21122e9a529b942811007d65eabe4182e7342d102caf119b229322c6", - "sha256:94f89161a3e358db33310a8a064852a6eb119ed1aa1a3dba927b4e5140e65d00", - "sha256:96742796f499ac23b59856db734e65b286d1214a0d9b57bcd7bece92d9201fa4", - "sha256:98c729193e7abe498565266933c125780fb646e977e94289cadbb36e4eeeb370", - "sha256:9b75b5d3416b00d064a5e6f4814fdfb18a964a7cf38dc00b5c2c02fa30a7dd0b", - "sha256:9cad834f1a8f51eb037c3c4dc72c884c9e1e0644d900e2d45aa76450e4aa6282", - "sha256:9d0041bd9e2d2ef803b32d84a0c8115d178132da5691346465953a2a966ba8ca", - "sha256:9f9a1b15b875160186177f659cde2b0f899182b0aca49457d6396afc4bbda7b9", - "sha256:a05b199c11d2f39c72de8c30668734b5d20974ad44b65324ea3e647a211f135d", - "sha256:a413674eb2bd2ecb2b93fcc928871b19f7220ee04bca4af3375c50a2b32b5a50", - "sha256:a54b94b0e4de95aa92618906fb631779d9fde29b4bf659f482c354a3a79fd025", - "sha256:a60ba9d104f4e8496107b1cb86e45a68a16d13511dc3986e0780e9f85c2136f9", - "sha256:ad4a896896346adab86d52b31163c39d49e4e94c829494b96cc064bff82c5851", - "sha256:af1c2241919304cc2f90e7dcb3eb1c1df6fb4172dd338e629dd6410e48b3d1a0", - "sha256:b0a5651e350997cebcdc23016dca26c4d1993d29015a535284da3159796e30b6", - "sha256:b7d60d42f1b9571341ad2322e748f7a60f9847546cd801a3a0eb72a1b54c6519", - "sha256:bb979162323f3534dce84b59f86e689a0761a2a300e0212bfaedfa80d4eb8100", - "sha256:bc907ea12216cfc5560148fc42459d86740fc739981c6feb94230dab09362679", - "sha256:c146a24a8f0dc4a7846fb4640b88b3a68986585b8ce8397af15e66b7c5817439", - "sha256:c46bd76986e05689376d28fdc2b97d899576ce3e3aaa5a5f80f67a8300b26eb3", - "sha256:c624c82e645f6b5465d08cdc802fb0cd53aa1478782fb2992b9e09f2c9426865", - "sha256:cd36b71f9f3bf195b2dd9be5eafbfc9409e6c8007aebc38a4dc051f522008033", - "sha256:ce0518667855a1598d9b1f4fcf0fed1182c67c5ba4fe6a2c6bce93440a65cead", - "sha256:d21408eaa157063f56e58ca50da27cad67c4395a85fb44cc7a31253ea4e58918", - "sha256:d33aef3914a5b49db12ed3f24d214ffa50caefc8f4b0c7c7b9485bd4b231a898", - "sha256:d3dc8d6ce8f001c80919bdb49d8b0b815185933a0b8e9cdeaea42b0b6f27eeb0", - "sha256:d58258a66255b2500ddaa4f33191ada5ec983a429c09eb151daf81efbb9aa115", - "sha256:d8b41195a6b03280ab00749a438fbce761e7acfd5381051a570239d752376f27", - "sha256:ddf9426b740a7047b2b0dddcba775211542e8053ce1e509a1759b665fe573508", - "sha256:de34a7d1893be76cb015929690dce3bde29f4de08143da2e9ad1cedb11dbf80e", - "sha256:e3d50ac3b772c10e0b918a5ce2e871138896bfb5f35050ff1ff87ddca45961fc", - "sha256:e49e4c3e899c32884d7828c91d6c3aff08d2f18857f50f86cc91187c31a4ca58", - "sha256:eb91471640390a82744b164f8a0be4d7c89d173b1170713f9639c6bad61e9e64", - "sha256:f2e69415e4e33cdeee50ebc2c4d8fcbef12c3181d9274e512ccd2a905a76aad1", - "sha256:f3353a2d7eb7d5e0af8a7ca9fc85a34ba12619119bcdee6b8a28a6373cda65ce", - "sha256:f933b35fa563f047896a70b69414dfb3952831817e4c4b3a6faa96737627f363", - "sha256:fccd24c080850715c58a80200d367bc62b4bff6c9fb84e9564da1ebcafea6418", - "sha256:fd9167e9604cb5a218a2e847aa8cdc5f98b379a673371978ee7b0c11b4d2e140", - "sha256:fdc648d4e81eef5ac4bb35d731562dffc28358948410f3274d123320e125d613", - "sha256:fe7439d9c5b402af2c9911c7facda1808d0c8dbfa9cf085e6aeac511a23f7d87", - "sha256:ffae52cd76837a5c16409359d236b1fced79e42e0792e8adf375095a5e855368" + "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", + "sha256:05284439ebe7d9f5f5a668d4d8a0a1d851d16f7d47c78e1fab968c8ad30cab04", + "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", + "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", + "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0", + "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", + "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", + "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", + "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", + "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", + "sha256:11e8e28c0ba0373d052818b600474cfee2fafa6c9f36c8587d217b13ee28ca7d", + "sha256:130c1ffa5039a333f5926b09e346ab335f0d4ec393b030a18549a7c7e7c2cea4", + "sha256:1321bce595ad70e80f97f998db37356b2e22cf98094eba6fe91782e626da2f71", + "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", + "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", + "sha256:15ea4d2e182345dd1b4286593601d766411b43f868924afe297570658c31a62b", + "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466", + "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", + "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", + "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4", + "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", + "sha256:1d66f45b9399036e890fb9c04e9f70c33857fd8f58ac8db9f3278cfa835440c3", + "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", + "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", + "sha256:2406d034635d1497c596c40c85f86ecf2bf9611c1df73d14078af8444fe48031", + "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", + "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424", + "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", + "sha256:299a245537e697f28a7511d01038c310ac74e8ea213c0019e1fc65f52c0dcb23", + "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", + "sha256:2e307cb5f66c59ede95c00e93cd84190a5b7f3533d7953690b2036780622ba81", + "sha256:2e39169ac6aae06dd79c07c8a69d9da867cef6a6d7883a0186b46bb46ccfb0c3", + "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", + "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", + "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", + "sha256:33ba649a6e55ae3808e4c39e01580dc9a9b0d5b02e77b66bb86ef117922b1264", + "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", + "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5", + "sha256:36184b44bf60a480863e51021c26aca3dfe8dd2f5eeabb33622b132b9d8b8b54", + "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", + "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", + "sha256:419dd9c98bcc9fb0242be89e0c6e922df333b975d4268faa90d58499fd9c9ebe", + "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", + "sha256:42ccc57ff99166a55a59d8c7d14f1a357b7749f9ed3584df74053fd098243451", + "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc", + "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", + "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8", + "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859", + "sha256:46f48482c1a4748ab2773f75fffbdd1951eb59794e32788834b945da857c47a8", + "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1", + "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", + "sha256:4c3f8a0d4802df34fcdbeb3dfe3a4d8c9a530baea8fafdf80816fcaac5379d83", + "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", + "sha256:555ed147cbe8c8f76e72a4c6cd3b7b761cbf9987891b9448808148204aed74a5", + "sha256:55d42a0ef2bdf6bc81e1cc2d49d12460f63c6ae1423c4f4851b828e454ccf6f1", + "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85", + "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be", + "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", + "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7", + "sha256:5df559e9e7644d9042f626f2c3997b555f347d7a855a15f170b253f6c5bfe358", + "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", + "sha256:61490d57e82e23b45c66f96184237994bfafa914433b8cd1a9bb57fecfced59d", + "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", + "sha256:64a0fe3f334a40b989812de70160de6b0ec7e3c9e4a04c0bbc48d97c5d3600ae", + "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", + "sha256:699c346abc73993962cac7bb4f02f58e438840fa5458a048d3a178a7a670ba86", + "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", + "sha256:6bde37765564cd22a676dd8101b657839a1854cfaa9c382c5abf6ff7accfd4ae", + "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", + "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", + "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", + "sha256:737005088449ddd3b3df5a95476ee1c2c5c669f5c30eed909548a92939c0e12d", + "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", + "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", + "sha256:79af163a4b40bbd8cfd7ca86ec8b54b81121d3b213b4435ea27d6568bcba3e9d", + "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14", + "sha256:7cf9bc4508efb18d8dff6934b602324eb9f8c6644749627ce001d6f38a490889", + "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", + "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5", + "sha256:81f81bbd7cdb4bdc418c09a73809abeda8f263a6bf8f9c7f93ed98b5597af39d", + "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", + "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114", + "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e", + "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", + "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", + "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", + "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", + "sha256:8de567dec6d451649a781633d36f5c7501711adee329d76c095be2178855b042", + "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", + "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", + "sha256:935afcdea4751b0ac918047a2df3f720212892347767aea28f5b3bf7be4f27c0", + "sha256:9a0ff7ee28583ab30a52f371b40f54e7138c52ca67f8ca17ccb7ccf0b383cb5f", + "sha256:9ad08547995a57e74fea6abaf5940d399447935faebbd2612b3b0ca6f987946b", + "sha256:9b2a4e17bfd68536c3b801800941c95a1d4a06e3cada11c146093ba939d9638d", + "sha256:9b78430703cfcf5f5e86eb74027a1ed03a93509273d7c705babb547f03e60016", + "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", + "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", + "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", + "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", + "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", + "sha256:a4cf32a26fa744101b67bfd28c55d992cd19438aff611a46cac7f066afca8fd4", + "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", + "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", + "sha256:af22763a0a1eff106426a6e1f13c4582e0d0ad89c1493ab6c058236174cd6c6a", + "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c", + "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", + "sha256:b2eff8ee57c5996b0d2a07c3601fb4ce5fbc37547344a26945dd9e5cbd1ed27a", + "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", + "sha256:b8a4131698b6992b2a56015f51646711ec5d893a0b314a4b985477868e240c87", + "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", + "sha256:ba783541be46f27c8faea5a6645e193943c17ea2f0ffe593639d906a327a9bcc", + "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", + "sha256:be3964f7312ea05ed283b20f87cb533fdc555b2e428cc7be64612c0b2124f08c", + "sha256:be806e2961cd390a89d6c3ce8c2ae34271cfcd05660f716257838bb560f1c3b6", + "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d", + "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f", + "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626", + "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", + "sha256:cb0702c12983be3b2fab98ead349ac63a98216d28dda6f518f52da5498a27a1b", + "sha256:cbc619e84a5e3ab2d452de831c88bdcad824414e9c2d28cd101f94dbdf26329c", + "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", + "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", + "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", + "sha256:d2a81bdcfde4245468f7030a75a37d50400ac2455c3a4819d9d550c937f90ab5", + "sha256:d2cc2b34f9e1d31ce255174da82902ad75bd7c0d88a33df54a77a22f2ef421ee", + "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", + "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", + "sha256:d7cf5e726b6fa977e428a61880fb108a62f28b6d0c7ef675b117eaff7076df49", + "sha256:d85d784c619370d9329bbd670f41ff5f2ae62ea4519761b679d0f57f0f0ee267", + "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", + "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", + "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622", + "sha256:dbd749cff1defbde270ca346b69b3baf5f1297213ef322254bf2a28537f0b046", + "sha256:dc662bc9375a6a394b62dfd331874c434819f10ee3902123200dbcf116963f89", + "sha256:dc6b0d5a1ea0318ef2def2b6a55dccf1dcaf77d605672347271ed7b829860765", + "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", + "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e", + "sha256:dea0808153f1fbbad772669d906cddd92100277533a03845de6893cadeffc8be", + "sha256:e0d7151a1bd5d0a203a5008fc4ae51a159a610cb82ab0a9b2c4d80241745582e", + "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", + "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261", + "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", + "sha256:e377e4cf8795cdbdff75b8f0223d7b6c68ff4fef36799d88ccf3a995a91c0112", + "sha256:e3acb9c16530362aeaef4e84d57db357002dc5cbfac9a23414c3e73c08301ab2", + "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d", + "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089", + "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", + "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60", + "sha256:f09c9d4c26fa79c1bad927efb05aca2391350b8e61c38cbc0d7d3c814e463124", + "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", + "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", + "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171", + "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", + "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", + "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.25.0" + "version": "==0.27.0" }, "setuptools": { "hashes": [ - "sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009", - "sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552" + "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", + "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c" ], "markers": "python_version >= '3.9'", - "version": "==80.7.1" + "version": "==80.9.0" }, "six": { "hashes": [ @@ -1801,12 +1838,12 @@ }, "starlette": { "hashes": [ - "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", - "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5" + "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", + "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.46.2" + "version": "==0.47.2" }, "tinycss2": { "hashes": [ @@ -1858,21 +1895,22 @@ }, "tornado": { "hashes": [ - "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", - "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", - "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", - "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", - "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", - "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", - "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", - "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", - "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", - "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", - "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1" + "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", + "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", + "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", + "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", + "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", + "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", + "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", + "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", + "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", + "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", + "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", + "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==6.4.2" + "markers": "python_version >= '3.9'", + "version": "==6.5.2" }, "traitlets": { "hashes": [ @@ -1892,58 +1930,49 @@ "markers": "python_version >= '3.9'", "version": "==0.30.0" }, - "twisted": { - "hashes": [ - "sha256:695d0556d5ec579dcc464d2856b634880ed1319f45b10d19043f2b57eb0115b5", - "sha256:fe403076c71f04d5d2d789a755b687c5637ec3bcd3b2b8252d76f2ba65f54261" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.0'", - "version": "==24.11.0" - }, "typeguard": { "hashes": [ - "sha256:77a78f11f09777aeae7fa08585f33b5f4ef0e7335af40005b0c422ed398ff48c", - "sha256:a6f1065813e32ef365bc3b3f503af8a96f9dd4e0033a02c28c4a4983de8c6c49" + "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", + "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.4.2" + "version": "==4.4.4" }, "typing-extensions": { "hashes": [ - "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", - "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" + "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", + "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.13.2" + "markers": "python_version >= '3.9'", + "version": "==4.14.1" }, "typing-inspection": { "hashes": [ - "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", - "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122" + "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", + "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28" ], "markers": "python_version >= '3.9'", - "version": "==0.4.0" + "version": "==0.4.1" }, "urllib3": { "hashes": [ - "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", - "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.4.0" + "version": "==2.5.0" }, "uvicorn": { "hashes": [ - "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", - "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403" + "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", + "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.34.2" + "version": "==0.35.0" }, "wcwidth": { "hashes": [ @@ -1980,123 +2009,123 @@ }, "yarl": { "hashes": [ - "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", - "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa", - "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61", - "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2", - "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2", - "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", - "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902", - "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2", - "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914", - "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", - "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", - "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569", - "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", - "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", - "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20", - "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", - "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", - "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc", - "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", - "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a", - "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", - "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", - "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", - "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5", - "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", - "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", - "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", - "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0", - "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26", - "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", - "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c", - "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", - "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", - "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", - "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a", - "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", - "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", - "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", - "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", - "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", - "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2", - "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", - "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", - "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", - "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", - "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e", - "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", - "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", - "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe", - "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", - "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", - "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", - "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", - "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62", - "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", - "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", - "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791", - "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", - "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb", - "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94", - "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", - "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10", - "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", - "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", - "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", - "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da", - "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c", - "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", - "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", - "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195", - "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", - "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8", - "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634", - "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", - "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8", - "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", - "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", - "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076", - "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656", - "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", - "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19", - "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a", - "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4", - "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2", - "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64", - "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", - "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7", - "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995", - "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6", - "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f", - "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", - "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487", - "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9", - "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a", - "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d", - "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", - "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", - "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22", - "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d", - "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c", - "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", - "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5", - "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867", - "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3" + "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", + "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", + "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", + "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", + "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", + "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", + "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", + "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010", + "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", + "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", + "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", + "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", + "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", + "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", + "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", + "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", + "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", + "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8", + "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805", + "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", + "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", + "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", + "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", + "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b", + "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", + "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", + "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", + "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", + "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", + "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", + "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", + "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", + "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", + "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", + "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", + "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", + "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240", + "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", + "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", + "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", + "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba", + "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", + "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", + "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", + "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", + "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", + "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", + "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d", + "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", + "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", + "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", + "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723", + "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", + "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", + "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", + "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", + "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", + "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", + "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", + "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", + "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", + "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", + "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", + "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", + "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", + "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", + "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", + "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", + "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000", + "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", + "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", + "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", + "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", + "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", + "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", + "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06", + "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", + "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", + "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", + "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", + "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", + "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", + "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", + "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c", + "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", + "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", + "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", + "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", + "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e", + "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", + "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee", + "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", + "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", + "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", + "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", + "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", + "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3", + "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00", + "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983", + "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", + "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", + "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", + "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", + "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.20.0" + "version": "==1.20.1" }, "zipp": { "hashes": [ - "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", - "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931" + "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", + "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.21.0" + "version": "==3.23.0" }, "zope.interface": { "hashes": [ diff --git a/onboarding-enabler-python-sample-app/README.md b/onboarding-enabler-python-sample-app/README.md index 215c33af4f..f951fd1d7b 100755 --- a/onboarding-enabler-python-sample-app/README.md +++ b/onboarding-enabler-python-sample-app/README.md @@ -20,14 +20,19 @@ This is an example about how an API service implemented in Python can be registe test_env\Scripts\activate # Windows ``` -2. Install the Python enabler package in editable mode: +2. Download dependencies ```shell - pip install -e onboarding-enabler-python + pipenv sync ``` - +*OPTIONAL:* +In case of need to modify the list of libraries, modify Pipfile and generate new lock file: + ```shell + pipenv lock + ``` + 3. You can now start the service using by running: ```shell - cd onboarding-enabler-python-sample-app/src + cd src python app.py ``` From a5bce629bda65230f24c24d98e509e3df359ff5b Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 21 Aug 2025 16:53:11 +0200 Subject: [PATCH 072/152] fix: WebClient used in API Catalog does not follow redirects (#4278) Signed-off-by: Pablo Carle Signed-off-by: Andrea Tabone Co-authored-by: Pablo Carle Co-authored-by: Andrea Tabone <39694626+taban03@users.noreply.github.com> Co-authored-by: Andrea Tabone Signed-off-by: Gowtham Selvaraj --- .../swagger/ApiDocRetrievalServiceRest.java | 3 +- .../common/config/WebClientConfig.java | 4 +- .../client/api/ApiDocRedirectController.java | 46 +++++++++++++++++++ .../zowe/apiml/client/api/PetController.java | 27 +++++------ .../src/main/resources/application.yml | 2 +- .../api/ApiDocRedirectControllerTest.java | 40 ++++++++++++++++ ...alogDiscoverableClientIntegrationTest.java | 29 ++++++++---- 7 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 discoverable-client/src/main/java/org/zowe/apiml/client/api/ApiDocRedirectController.java create mode 100644 discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiDocRedirectControllerTest.java diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java index 88f90380da..6da0a3e098 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceRest.java @@ -67,7 +67,8 @@ public Mono retrieveApiDoc(ServiceInstance serviceInstance, ApiInfo * @throws ApiDocNotFoundException if the response is error */ private Mono getApiDocContentByUrl(@NonNull String serviceId, String apiDocUrl) { - return webClient.get().uri(apiDocUrl) + return webClient.get() + .uri(apiDocUrl) .header(ACCEPT, MediaType.APPLICATION_JSON_VALUE) .retrieve() .onStatus(httpStatusCode -> httpStatusCode.value() != SC_OK, response -> Mono.error( diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java index 3b15995e64..e0505499dc 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/WebClientConfig.java @@ -86,8 +86,10 @@ HttpClient getHttpClient(HttpClient httpClient, boolean useClientCert) { @Bean @Primary WebClient webClient(HttpClient httpClient) { + HttpClient base = getHttpClient(httpClient, false) + .followRedirect(true); return WebClient.builder() - .clientConnector(new ReactorClientHttpConnector(getHttpClient(httpClient, false))) + .clientConnector(new ReactorClientHttpConnector(base)) .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) .build(); } diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/api/ApiDocRedirectController.java b/discoverable-client/src/main/java/org/zowe/apiml/client/api/ApiDocRedirectController.java new file mode 100644 index 0000000000..b61d50e585 --- /dev/null +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/api/ApiDocRedirectController.java @@ -0,0 +1,46 @@ +/* + * 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.client.api; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Controller to test the support of Catalog to render redirected Swagger + */ +@RestController +@Tag(name = "Other Operations") +public class ApiDocRedirectController { + + @Value("${apiml.service.baseUrl}") + private String baseUrl; + + @Value("${apiml.service.contextPath}") + private String contextPath; + + private static final String REDIRECT_DOC_URL = "/docs/redirect"; + + @GetMapping(REDIRECT_DOC_URL) + public ResponseEntity getDocWithRedirect() { + String location = UriComponentsBuilder.fromUriString(baseUrl) + .path(contextPath) + .path("/v3/api-docs/apiv2").toUriString(); + + return ResponseEntity.status(301) + .header("Location", location) + .build(); + } + +} diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/api/PetController.java b/discoverable-client/src/main/java/org/zowe/apiml/client/api/PetController.java index 5975378143..88135d38c1 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/api/PetController.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/api/PetController.java @@ -16,11 +16,19 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.zowe.apiml.client.exception.PetIdMismatchException; import org.zowe.apiml.client.exception.PetNotFoundException; import org.zowe.apiml.client.model.Pet; @@ -39,19 +47,12 @@ @RequestMapping("/api/v1") @Tag( description = "/api/v1/pets", - name = "The pet API") + name = "The pet API" +) +@RequiredArgsConstructor public class PetController { - private final PetService petService; - /** - * Constructor for {@link PetController}. - * - * @param petService service for working with {@link Pet} objects. - */ - @Autowired - public PetController(PetService petService) { - this.petService = petService; - } + private final PetService petService; /** * The getAllPets method lists all existing pets diff --git a/discoverable-client/src/main/resources/application.yml b/discoverable-client/src/main/resources/application.yml index 57e86c063e..eb7a82666f 100644 --- a/discoverable-client/src/main/resources/application.yml +++ b/discoverable-client/src/main/resources/application.yml @@ -109,7 +109,7 @@ apiml: - apiId: zowe.apiml.discoverableclient.rest version: 2.0.0 gatewayUrl: api/v2 - swaggerUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v3/api-docs/apiv2 + swaggerUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/docs/redirect documentationUrl: https://www.zowe.org catalog: tile: diff --git a/discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiDocRedirectControllerTest.java b/discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiDocRedirectControllerTest.java new file mode 100644 index 0000000000..d5f75356e4 --- /dev/null +++ b/discoverable-client/src/test/java/org/zowe/apiml/client/api/ApiDocRedirectControllerTest.java @@ -0,0 +1,40 @@ +/* + * 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.client.api; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.zowe.apiml.client.configuration.SecurityConfiguration; +import org.zowe.apiml.util.config.TestConfig; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = {ApiDocRedirectController.class}) +@Import({ SecurityConfiguration.class, TestConfig.class }) +class ApiDocRedirectControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void callApiDocRedirect() throws Exception { + this.mockMvc.perform(get("/docs/redirect")) + .andExpect(status().is3xxRedirection()) + .andExpect(header().string("Location", containsString("/v3/api-docs/apiv2"))); + } + +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java index cf17ada819..ee45558798 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java @@ -48,6 +48,7 @@ class ApiCatalogDiscoverableClientIntegrationTest implements TestWithStartedInst class WhenGettingApiDoc { @Nested class ReturnRelevantApiDoc { + @Test void givenV1ApiDocPath() throws Exception { final HttpResponse response = getResponse(DISCOVERABLE_CLIENT_API_DOC_ENDPOINT, HttpStatus.SC_OK); @@ -69,11 +70,11 @@ void givenV2ApiDocPath() throws IOException { "\n**************************\n"; DocumentContext jsonContext = JsonPath.parse(jsonResponse); - LinkedHashMap swaggerInfo = jsonContext.read("$.info"); + LinkedHashMap swaggerInfo = jsonContext.read("$.info"); String swaggerServer = jsonContext.read("$.servers[0].url"); - LinkedHashMap paths = jsonContext.read("$.paths"); - LinkedHashMap definitions = jsonContext.read("$.components.schemas"); - LinkedHashMap externalDoc = jsonContext.read("$.externalDocs"); + LinkedHashMap paths = jsonContext.read("$.paths"); + LinkedHashMap definitions = jsonContext.read("$.components.schemas"); + LinkedHashMap externalDoc = jsonContext.read("$.externalDocs"); assertTrue(swaggerInfo.get("description").toString().contains("API"), apiCatalogSwagger); assertThat(swaggerServer, endsWith("")); @@ -101,7 +102,9 @@ void givenUrlForContainer() throws IOException { validateDiscoverableClientApiV1(dcJsonResponse, dcJsonContext); } + } + } @Test @@ -112,14 +115,14 @@ void givenApis_whenGetContainer_thenApisReturned() throws IOException { validateContainer(containerJsonContext); - LinkedHashMap> apis = containerJsonContext.read("$[0].services[0].apis"); + LinkedHashMap> apis = containerJsonContext.read("$[0].services[0].apis"); assertNotNull(apis.get("default")); assertNotNull(apis.get("zowe.apiml.discoverableclient.rest v2.0.0")); assertNotNull(apis.get("zowe.apiml.discoverableclient.rest v1.0.0")); assertNotNull(apis.get("zowe.apiml.discoverableclient.ws v1.0.0")); - LinkedHashMap defaultApi = apis.get("default"); + LinkedHashMap defaultApi = apis.get("default"); assertEquals("zowe.apiml.discoverableclient.rest", defaultApi.get("apiId")); assertEquals("api/v1", defaultApi.get("gatewayUrl")); assertEquals("1.0.0", defaultApi.get("version")); @@ -129,6 +132,7 @@ void givenApis_whenGetContainer_thenApisReturned() throws IOException { JSONArray codeSnippets = (JSONArray) defaultApi.get("codeSnippet"); assertEquals(2, codeSnippets.size()); + @SuppressWarnings("unchecked") LinkedHashMap codeSnippet = (LinkedHashMap) codeSnippets.get(0); assertEquals("/greeting", codeSnippet.get("endpoint")); assertNotNull(codeSnippet.get("codeBlock")); @@ -138,6 +142,7 @@ void givenApis_whenGetContainer_thenApisReturned() throws IOException { @Nested class WhenGettingDifferenceBetweenVersions { + @Nested class ReturnDifference { @Test @@ -153,10 +158,12 @@ void givenValidServiceAndVersions() throws Exception { assertThat(textResponse, containsString( "

What's Deleted


  1. GET")); } + } @Nested class ReturnNotFound { + @Test void givenWrongVersion() throws Exception { getResponse(API_SERVICE_VERSION_DIFF_ENDPOINT_WRONG_VERSION, HttpStatus.SC_NOT_FOUND); @@ -166,7 +173,9 @@ void givenWrongVersion() throws Exception { void givenWrongService() throws Exception { getResponse(API_SERVICE_VERSION_DIFF_ENDPOINT_WRONG_SERVICE, HttpStatus.SC_NOT_FOUND); } + } + } // Execute the endpoint and check the response for a return code @@ -198,11 +207,11 @@ private void validateDiscoverableClientApiV1(String jsonResponse, DocumentContex "\n**************************\n"; // When - LinkedHashMap swaggerInfo = jsonContext.read("$.info"); + LinkedHashMap swaggerInfo = jsonContext.read("$.info"); String swaggerServer = jsonContext.read("$.servers[0].url"); - LinkedHashMap paths = jsonContext.read("$.paths"); - LinkedHashMap definitions = jsonContext.read("$.components.schemas"); - LinkedHashMap externalDoc = jsonContext.read("$.externalDocs"); + LinkedHashMap paths = jsonContext.read("$.paths"); + LinkedHashMap definitions = jsonContext.read("$.components.schemas"); + LinkedHashMap externalDoc = jsonContext.read("$.externalDocs"); // Then assertTrue(swaggerInfo.get("description").toString().contains("API"), apiCatalogSwagger); From d44da7754e2e4d4fd613bc9361998852925a2e3a Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 22 Aug 2025 00:43:44 +0000 Subject: [PATCH 073/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.3'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9a2f793631..6fec81e695 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.3-SNAPSHOT +version=3.3.3 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From fb116ca3693a0bf9dc42d97b9f6452eea1f68704 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 22 Aug 2025 00:43:45 +0000 Subject: [PATCH 074/152] [Gradle Release plugin] Create new version: 'v3.3.4-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6fec81e695..97502d0154 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.3 +version=3.3.4-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 1936e1e2f997b7a70a5337ca7310e47296ca46a4 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 22 Aug 2025 00:43:46 +0000 Subject: [PATCH 075/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 580c1afa8d..aef548e582 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.3-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.4-SNAPSHOT From e72c30078f86ed80aecfa21eafe89ed48cf20e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Fri, 22 Aug 2025 10:32:47 +0200 Subject: [PATCH 076/152] fix: websocket frame size configuration (#4277) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 12 +-- config/local/gateway-service.yml | 9 +- .../zowe/apiml/client/stomp/StompConfig.java | 41 +++++++++ .../apiml/client/stomp/StompController.java | 33 +++++++ ...DiscoverableClientWebSocketConfigurer.java | 12 +++ .../src/main/resources/bin/start.sh | 2 +- .../apiml/gateway/config/WebSocketConfig.java | 14 ++- .../ApimlRequestUpgradeStrategy.java | 15 +++- .../ApimlRequestUpgradeStrategyTest.java | 3 +- .../integration/proxy/StompProxyTest.java | 87 +++++++++++++++++++ .../integration/proxy/WebSocketProxyTest.java | 4 +- .../zowe/apiml/util/requests/Endpoints.java | 1 + 12 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompConfig.java create mode 100644 discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompController.java create mode 100644 integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 052525dab6..3bc993e5cb 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -49,7 +49,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader APIML_SECURITY_X509_ENABLED: true SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 - SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + SPRING_CLOUD_GATEWAY_SERVER_WEBFLUX_HTTPCLIENT_WEBSOCKET_MAXFRAMEPAYLOADLENGTH: 3145728 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_SERVICESTODISABLERETRY: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken @@ -109,7 +109,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader APIML_SECURITY_X509_ENABLED: true SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 - SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + SPRING_CLOUD_GATEWAY_SERVER_WEBFLUX_HTTPCLIENT_WEBSOCKET_MAXFRAMEPAYLOADLENGTH: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken APIML_SECURITY_AUTH_PROVIDER: saf @@ -124,7 +124,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader APIML_SECURITY_X509_ENABLED: true SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 - SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + SPRING_CLOUD_GATEWAY_SERVER_WEBFLUX_HTTPCLIENT_WEBSOCKET_MAXFRAMEPAYLOADLENGTH: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken APIML_SECURITY_AUTH_PROVIDER: saf @@ -206,7 +206,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader APIML_SECURITY_X509_ENABLED: true SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 - SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + SPRING_CLOUD_GATEWAY_SERVER_WEBFLUX_HTTPCLIENT_WEBSOCKET_MAXFRAMEPAYLOADLENGTH: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka @@ -222,7 +222,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader APIML_SECURITY_X509_ENABLED: true SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 - SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + SPRING_CLOUD_GATEWAY_SERVER_WEBFLUX_HTTPCLIENT_WEBSOCKET_MAXFRAMEPAYLOADLENGTH: 16348 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken APIML_SERVICE_DISCOVERYSERVICEURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka @@ -354,7 +354,7 @@ jobs: APIML_SECURITY_AUTH_PASSTICKET_CUSTOMAUTHHEADER: customPassticketHeader ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348 - SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348 + SPRING_CLOUD_GATEWAY_SERVER_WEBFLUX_HTTPCLIENT_WEBSOCKET_MAXFRAMEPAYLOADLENGTH: 3145728 APIML_GATEWAY_SERVICESTOLIMITREQUESTRATE: discoverableclient APIML_GATEWAY_SERVICESTODISABLERETRY: discoverableclient APIML_GATEWAY_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken diff --git a/config/local/gateway-service.yml b/config/local/gateway-service.yml index 8f9db2eeae..ee139ab68c 100644 --- a/config/local/gateway-service.yml +++ b/config/local/gateway-service.yml @@ -16,8 +16,6 @@ eureka: serviceUrl: defaultZone: https://localhost:10011/eureka/ server: - webSocket: - requestBufferSize: 16348 max-http-request-header-size: 16348 ssl: @@ -35,3 +33,10 @@ spring: enabled: always profiles: include: debug + cloud: + gateway: + server: + webflux: + httpclient: + websocket: + max-frame-payload-length: 3145728 diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompConfig.java b/discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompConfig.java new file mode 100644 index 0000000000..5d2d0aacb7 --- /dev/null +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompConfig.java @@ -0,0 +1,41 @@ +/* + * 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.client.stomp; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; + +@Configuration +@EnableWebSocketMessageBroker +public class StompConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registry) { + registry.setSendBufferSizeLimit(5_898_240); + registry.setMessageSizeLimit(5_898_240); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic"); + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws/stomp") + .setAllowedOriginPatterns("*"); + } +} diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompController.java b/discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompController.java new file mode 100644 index 0000000000..97e2897c08 --- /dev/null +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/stomp/StompController.java @@ -0,0 +1,33 @@ +/* + * 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.client.stomp; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.stereotype.Controller; + +@Controller +@Slf4j +public class StompController { + + @MessageMapping("/replyWithSameSize/{id}") + @SendTo("/topic/replyWithSameSize/{id}") + public String replyWithSameSize(@DestinationVariable String id, @Payload String payload) throws IllegalArgumentException { + var payloadSize = payload.getBytes().length; + log.info("Received stomp message id {} with payload size {}. Sending the same size back.", id, payloadSize); + + char c = 'B'; + return String.valueOf(c).repeat(payloadSize); + } +} diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/ws/DiscoverableClientWebSocketConfigurer.java b/discoverable-client/src/main/java/org/zowe/apiml/client/ws/DiscoverableClientWebSocketConfigurer.java index 8eb92f28b1..e5c55c6ba3 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/ws/DiscoverableClientWebSocketConfigurer.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/ws/DiscoverableClientWebSocketConfigurer.java @@ -10,6 +10,8 @@ package org.zowe.apiml.client.ws; +import org.springframework.context.annotation.Bean; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; import org.zowe.apiml.message.core.MessageType; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; @@ -36,4 +38,14 @@ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new HeaderSocketServerHandler(), webSocketEndpoint); } + + // Configure buffer sizes for inbound messages + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(3 * 1024 * 1024); // 3MB + container.setMaxBinaryMessageBufferSize(3 * 1024 * 1024); // 3MB + return container; + } + } diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index a8e8b7d6a8..6fe35c851e 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -367,7 +367,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Dserver.webSocket.asyncWriteTimeout=${ZWE_configs_server_webSocket_asyncWriteTimeout:-60000} \ -Dserver.webSocket.connectTimeout=${ZWE_configs_server_webSocket_connectTimeout:-45000} \ -Dserver.webSocket.maxIdleTimeout=${ZWE_configs_server_webSocket_maxIdleTimeout:-3600000} \ - -Dserver.webSocket.requestBufferSize=${ZWE_configs_server_webSocket_requestBufferSize:-8192} \ + -Dspring.cloud.gateway.server.webflux.httpclient.websocket.max-frame-payload-length=${ZWE_configs_server_webSocket_requestBufferSize:-8192} \ -Dspring.profiles.active=${ZWE_configs_spring_profiles_active:-} \ -jar "${JAR_FILE}" & diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java index 33134e78c4..8a82478e5a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSocketConfig.java @@ -10,7 +10,9 @@ package org.zowe.apiml.gateway.config; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.config.HttpClientProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -27,9 +29,11 @@ @Slf4j @Configuration +@RequiredArgsConstructor public class WebSocketConfig { private static final ApimlLogger apimlLog = ApimlLogger.empty(); + private final HttpClientProperties httpClientProperties; @Bean @Primary @@ -43,7 +47,13 @@ WebSocketClient webSocketClient(HttpConfig config, HttpClient httpClient) { HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config.httpsConfig()); } var spec = WebsocketClientSpec.builder() - .handlePing(true); + .handlePing(httpClientProperties.getWebsocket().isProxyPing()); + + //Set the gateway outbound frame limit for websockets + var maxFramePayloadLength = httpClientProperties.getWebsocket().getMaxFramePayloadLength(); + if (maxFramePayloadLength != null) { + spec.maxFramePayloadLength(httpClientProperties.getWebsocket().getMaxFramePayloadLength()); + } var client = new ReactorNettyWebSocketClient(secureClient, () -> spec); return client; @@ -52,7 +62,7 @@ WebSocketClient webSocketClient(HttpConfig config, HttpClient httpClient) { @Bean @Primary RequestUpgradeStrategy requestUpgradeStrategy() { - return new ApimlRequestUpgradeStrategy(); + return new ApimlRequestUpgradeStrategy(httpClientProperties); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategy.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategy.java index 95a1568dd9..7275257a14 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategy.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategy.java @@ -14,6 +14,8 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.websocket.Endpoint; import jakarta.websocket.server.ServerEndpointConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.cloud.gateway.config.HttpClientProperties; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; @@ -35,8 +37,11 @@ import static reactor.core.publisher.Mono.*; +@RequiredArgsConstructor public class ApimlRequestUpgradeStrategy extends StandardWebSocketUpgradeStrategy { + private final HttpClientProperties httpClientProperties; + @Override protected void upgradeHttpToWebSocket(HttpServletRequest request, HttpServletResponse response, ServerEndpointConfig endpointConfig, Map pathParams) throws Exception { @@ -62,8 +67,14 @@ public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler, .then(deferContextual(contextView -> { Endpoint endpoint = new StandardWebSocketHandlerAdapter( ContextWebSocketHandler.decorate(handler, contextView), - session -> new ApimlWebSocketSession(session, handshakeInfo, bufferFactory)); - + session -> { + //Set the gateway outbound frame limit for websockets + if (httpClientProperties.getWebsocket().getMaxFramePayloadLength() != null) { + session.setMaxTextMessageBufferSize(httpClientProperties.getWebsocket().getMaxFramePayloadLength()); + session.setMaxBinaryMessageBufferSize(httpClientProperties.getWebsocket().getMaxFramePayloadLength()); + } + return new ApimlWebSocketSession(session, handshakeInfo, bufferFactory); + }); String requestURI = servletRequest.getRequestURI(); var config = new ApimlServerEndpointConfig(requestURI, endpoint); config.setSubprotocols(subProtocol != null ? diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategyTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategyTest.java index 67347d42bf..4af6eebf7d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategyTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/websocket/ApimlRequestUpgradeStrategyTest.java @@ -15,6 +15,7 @@ import jakarta.websocket.DeploymentException; import jakarta.websocket.server.ServerContainer; import org.junit.jupiter.api.Test; +import org.springframework.cloud.gateway.config.HttpClientProperties; import org.springframework.http.server.reactive.AbstractServerHttpRequest; import org.springframework.http.server.reactive.AbstractServerHttpResponse; import org.springframework.test.util.ReflectionTestUtils; @@ -48,7 +49,7 @@ void givenHttpRequest_thenUpgrade() throws IOException, DeploymentException { when(resp.getNativeResponse()).thenReturn(nativeResp); var handler = mock(WebSocketHandler.class); var handShakeInfo = mock(HandshakeInfo.class); - var updateStrategy = new ApimlRequestUpgradeStrategy(); + var updateStrategy = new ApimlRequestUpgradeStrategy(new HttpClientProperties()); var serverContainer = mock(ServerContainer.class); ReflectionTestUtils.setField(updateStrategy, "serverContainer", serverContainer); diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java new file mode 100644 index 0000000000..2297ca2563 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java @@ -0,0 +1,87 @@ +/* + * 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 jakarta.websocket.ContainerProvider; +import jakarta.websocket.WebSocketContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.simp.stomp.*; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; +import org.zowe.apiml.util.SecurityUtils; + +import java.lang.reflect.Type; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.zowe.apiml.util.requests.Endpoints.DISCOVERABLE_STOMP; + +public class StompProxyTest extends WebSocketProxyTest { + + private static final String SEND_ENDPOINT = "/app/replyWithSameSize/"; + private static final String SUBSCRIBE_ENDPOINT = "/topic/replyWithSameSize/"; + + private static WebSocketStompClient stompClient; + + private CompletableFuture completableFuture; + + @BeforeAll + public static void setUpStompClient() { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + container.setDefaultMaxTextMessageBufferSize(3 * 1024 * 1024); + container.setDefaultMaxBinaryMessageBufferSize(3 * 1024 * 1024); + + var standardWebSocketClient = new StandardWebSocketClient(container); + standardWebSocketClient.setSslContext(SecurityUtils.getSslContext()); + + stompClient = new WebSocketStompClient(standardWebSocketClient); + stompClient.setMessageConverter(new StringMessageConverter()); + + stompClient.setInboundMessageSizeLimit(Integer.MAX_VALUE); + stompClient.setOutboundMessageSizeLimit(Integer.MAX_VALUE); + } + + @Test + void stompOverWebsocketLargeMessageExchange() throws Exception { + completableFuture = new CompletableFuture<>(); + String uuid = UUID.randomUUID().toString(); + + StompSession stompSession = stompClient.connectAsync( + discoverableClientGatewayUrl(DISCOVERABLE_STOMP), VALID_AUTH_HEADERS, new StompSessionHandlerAdapter() { + }).get(1, SECONDS); + stompSession.subscribe(SUBSCRIBE_ENDPOINT + uuid, new StringStompFrameHandler()); + + char c = 'A'; + int payloadSize = 1024 * 1024; + stompSession.send(SEND_ENDPOINT + uuid, String.valueOf(c).repeat(payloadSize)); + + String response = completableFuture.get(10, SECONDS); + stompSession.disconnect(); + + assertEquals(payloadSize, response.getBytes().length); + } + + private class StringStompFrameHandler implements StompFrameHandler { + @Override + public Type getPayloadType(StompHeaders stompHeaders) { + return String.class; + } + + @Override + public void handleFrame(StompHeaders stompHeaders, Object o) { + completableFuture.complete((String) o); + } + } +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/WebSocketProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/WebSocketProxyTest.java index 992a25a633..6bbe5147d9 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/WebSocketProxyTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/WebSocketProxyTest.java @@ -60,7 +60,7 @@ class WebSocketProxyTest implements TestWithStartedInstances { private static final String BASE64_CREDENTIALS = Base64.getEncoder().encodeToString("user:pass".getBytes()); private static final String INVALID_BASE64_CREDENTIALS = Base64.getEncoder().encodeToString("user:invalidPass".getBytes()); - private WebSocketHttpHeaders VALID_AUTH_HEADERS; + protected WebSocketHttpHeaders VALID_AUTH_HEADERS; private WebSocketHttpHeaders INVALID_AUTH_HEADERS; @BeforeEach @@ -101,7 +101,7 @@ public boolean supportsPartialMessages() { }; } - private String discoverableClientGatewayUrl(String gatewayUrl) throws URISyntaxException { + protected String discoverableClientGatewayUrl(String gatewayUrl) throws URISyntaxException { String scheme = gatewayServiceConfiguration.getScheme().equals("http") ? "ws" : "wss"; String host = StringUtils.isNotBlank(gatewayServiceConfiguration.getDvipaHost()) ? gatewayServiceConfiguration.getDvipaHost() : gatewayServiceConfiguration.getHost(); int port = gatewayServiceConfiguration.getPort(); 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 7a9673c2fe..76ffa1e9d7 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 @@ -57,6 +57,7 @@ public class Endpoints { 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_STOMP = "/discoverableclient/ws/v1/stomp"; 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"; From c381a8d598681ef120f9a6418e6e05619eddaa38 Mon Sep 17 00:00:00 2001 From: Nafi Xhafa <164854562+nxhafa@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:13:20 +0200 Subject: [PATCH 077/152] chore: updates to attlsClient profile in Gateway (#4279) Signed-off-by: nxhafa Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../apiml/security/common/error/AuthExceptionHandler.java | 6 +++--- caching-service/src/main/resources/application.yml | 8 ++++---- gateway-service/src/main/resources/application.yml | 8 +++----- zaas-service/src/main/resources/application.yml | 4 ++-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java index 7ae4bfc832..ce2e951d7e 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java @@ -112,7 +112,7 @@ private Map.Entry, ExceptionHandler> entry(Cla (ex, ctx) -> handleForbidden(ctx.function, ex) ), entry(GatewayNotAvailableException.class, - (ex, ctx) -> handleGatewayNotAvailable(ctx.function, ex) + (ex, ctx) -> handleGatewayNotAvailable(ctx.function, ex, ctx.requestUri) ) ); @@ -275,9 +275,9 @@ private void handleForbidden(BiConsumer function, Ac writeErrorResponse("org.zowe.apiml.security.forbidden", HttpStatus.FORBIDDEN, function); } - private void handleGatewayNotAvailable(BiConsumer function, GatewayNotAvailableException ex) { + private void handleGatewayNotAvailable(BiConsumer function, GatewayNotAvailableException ex, String uri) { log.debug(MESSAGE_FORMAT, HttpStatus.SERVICE_UNAVAILABLE.value(), ex.getMessage()); - writeErrorResponse("org.zowe.apiml.security.gatewayNotAvailable", HttpStatus.SERVICE_UNAVAILABLE, function); + writeErrorResponse("org.zowe.apiml.security.gatewayNotAvailable", HttpStatus.SERVICE_UNAVAILABLE, function, uri); } diff --git a/caching-service/src/main/resources/application.yml b/caching-service/src/main/resources/application.yml index d0560e3ffd..ff08675dd8 100644 --- a/caching-service/src/main/resources/application.yml +++ b/caching-service/src/main/resources/application.yml @@ -197,14 +197,14 @@ server: --- spring.config.activate.on-profile: attlsClient -apiml: - service: - scheme: http - server: attlsClient: enabled: true +apiml: + service: + scheme: http + eureka: instance: nonSecurePortEnabled: true diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 594925346e..de292d25e2 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -214,14 +214,9 @@ server: ssl: enabled: false - service: - scheme: http apiml: service: corsEnabled: true - scheme: http - nonSecurePortEnabled: true - securePortEnabled: false --- spring.config.activate.on-profile: attlsClient @@ -232,6 +227,9 @@ server: apiml: service: + scheme: http + nonSecurePortEnabled: true + securePortEnabled: false discoveryServiceUrls: http://localhost:10011/eureka/ eureka: diff --git a/zaas-service/src/main/resources/application.yml b/zaas-service/src/main/resources/application.yml index 7174c4db8e..3ce5e7ba71 100644 --- a/zaas-service/src/main/resources/application.yml +++ b/zaas-service/src/main/resources/application.yml @@ -258,8 +258,6 @@ server: enabled: true ssl: enabled: false - service: - scheme: http apiml: service: @@ -281,6 +279,8 @@ apiml: service: corsEnabled: true scheme: http + nonSecurePortEnabled: true + securePortEnabled: false eureka: instance: securePortEnabled: false From 388b8b356327e93a4d43da6e85d8f8b8b39da4ad Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:44:58 -0400 Subject: [PATCH 078/152] chore: Update all non-major dependencies (v3.x.x) (#4281) Signed-off-by: Renovate Bot Signed-off-by: ac892247 Co-authored-by: Renovate Bot Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Co-authored-by: ac892247 Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 16 +- api-catalog-ui/frontend/package.json | 4 +- gradle/versions.gradle | 8 +- onboarding-enabler-python/Pipfile | 2 +- onboarding-enabler-python/Pipfile.lock | 1047 +++++++++++---------- 5 files changed, 548 insertions(+), 529 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index 399490084d..9fa0018b1d 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -71,13 +71,13 @@ "@eslint/js": "9.33.0", "@reduxjs/toolkit": "2.8.2", "@testing-library/dom": "10.4.1", - "@testing-library/jest-dom": "6.7.0", + "@testing-library/jest-dom": "6.8.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "ajv": "8.17.1", "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001735", + "caniuse-lite": "1.0.30001736", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", @@ -6599,9 +6599,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.7.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", - "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", + "version": "6.8.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", + "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9406,9 +9406,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "version": "1.0.30001736", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz", + "integrity": "sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==", "funding": [ { "type": "opencollective", diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index f08c7eb748..41d898aa9b 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -91,14 +91,14 @@ "@eslint/compat": "1.3.2", "@eslint/js": "9.33.0", "@testing-library/dom": "10.4.1", - "@testing-library/jest-dom": "6.7.0", + "@testing-library/jest-dom": "6.8.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@reduxjs/toolkit": "2.8.2", "ajv": "8.17.1", "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001735", + "caniuse-lite": "1.0.30001736", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", diff --git a/gradle/versions.gradle b/gradle/versions.gradle index e401e4e225..7b8bbbf377 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -5,8 +5,8 @@ dependencyResolutionManagement { version('projectNode', '20.14.0') version('projectNpm', '10.7.0') - version('springBoot', '3.5.4') - version('springBootGraphQl', '3.5.4') + version('springBoot', '3.5.5') + version('springBootGraphQl', '3.5.5') version('springCloudNetflix', '4.3.0') version('springCloudCommons', '4.3.0') version('springCloudCB', '3.3.0') @@ -62,7 +62,7 @@ dependencyResolutionManagement { version('jettyWebSocketClient', '12.1.0') version('jettison', '1.5.4') //0.12.x version contains breaking changes - version('jjwt', '0.12.7') + version('jjwt', '0.13.0') version('jodaTime', '2.14.0') version('jsonPath', '2.9.0') version('jsonSmart', '2.6.0') @@ -83,7 +83,7 @@ dependencyResolutionManagement { version('reactor', '3.7.9') version('restAssured', '5.5.6') version('rhino', '1.8.0') - version('springDoc', '2.8.9') + version('springDoc', '2.8.10') version('swaggerCore', '2.2.36') version('swaggerInflector', '2.0.13') version('swagger2Parser', '1.0.75') diff --git a/onboarding-enabler-python/Pipfile b/onboarding-enabler-python/Pipfile index dbe8e8c14e..4293b81f37 100644 --- a/onboarding-enabler-python/Pipfile +++ b/onboarding-enabler-python/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] aiohappyeyeballs = ">=2.6.1" -aiohttp = ">=3.11.18" +aiohttp = ">=3.12.15" aiosignal = ">=1.3.2" attrs = ">=25.3.0" build = ">=1.2.2.post1" diff --git a/onboarding-enabler-python/Pipfile.lock b/onboarding-enabler-python/Pipfile.lock index b5635a2610..aef03b97b3 100644 --- a/onboarding-enabler-python/Pipfile.lock +++ b/onboarding-enabler-python/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d564d5a61c75a198dad7253ffcf51b9b7bccdd90ccc15ce81f7d812a092f92fb" + "sha256": "5d3ae31e38e8c6e9356bf84ac343bc5b6953558762674abafd3daee983e59ba9" }, "pipfile-spec": 6, "requires": { @@ -27,100 +27,105 @@ }, "aiohttp": { "hashes": [ - "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717", - "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d", - "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8", - "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", - "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421", - "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", - "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d", - "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8", - "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3", - "sha256:1596ebf17e42e293cbacc7a24c3e0dc0f8f755b40aff0402cb74c1ff6baec1d3", - "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361", - "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137", - "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b", - "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd", - "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8", - "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", - "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d", - "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", - "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", - "sha256:3cec21dd68924179258ae14af9f5418c1ebdbba60b98c667815891293902e5e0", - "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756", - "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", - "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9", - "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", - "sha256:46533e6792e1410f9801d09fd40cbbff3f3518d1b501d6c3c5b218f427f6ff08", - "sha256:469ac32375d9a716da49817cd26f1916ec787fc82b151c1c832f58420e6d3533", - "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811", - "sha256:5199be2a2f01ffdfa8c3a6f5981205242986b9e63eb8ae03fd18f736e4840721", - "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118", - "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55", - "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609", - "sha256:5bc0ae0a5e9939e423e065a3e5b00b24b8379f1db46046d7ab71753dfc7dd0e1", - "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66", - "sha256:5d61df4a05476ff891cff0030329fee4088d40e4dc9b013fac01bc3c745542c2", - "sha256:5e7007b8d1d09bce37b54111f593d173691c530b80f27c6493b928dabed9e6ef", - "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", - "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", - "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804", - "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f", - "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94", - "sha256:7ccec9e72660b10f8e283e91aa0295975c7bd85c204011d9f5eb69310555cf30", - "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", - "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1", - "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", - "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f", - "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4", - "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f", - "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6", - "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4", - "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f", - "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7", - "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421", - "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e", - "sha256:a2fd04ae4971b914e54fe459dd7edbbd3f2ba875d69e057d5e3c8e8cac094935", - "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c", - "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", - "sha256:ad2f41203e2808616292db5d7170cccf0c9f9c982d02544443c7eb0296e8b0c7", - "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868", - "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", - "sha256:b2f317d1678002eee6fe85670039fb34a757972284614638f82b903a03feacdc", - "sha256:b426495fb9140e75719b3ae70a5e8dd3a79def0ae3c6c27e012fc59f16544a4a", - "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643", - "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01", - "sha256:c1b90407ced992331dd6d4f1355819ea1c274cc1ee4d5b7046c6761f9ec11829", - "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93", - "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9", - "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78", - "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", - "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd", - "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", - "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2", - "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6", - "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261", - "sha256:eab7b040a8a873020113ba814b7db7fa935235e4cbaf8f3da17671baa1024863", - "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1", - "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb", - "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415", - "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000", - "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1", - "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7", - "sha256:fe7cdd3f7d1df43200e1c80f1aed86bb36033bf65e3c7cf46a2b97a253ef8798" + "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", + "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", + "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", + "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263", + "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142", + "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6", + "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", + "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09", + "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", + "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", + "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", + "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", + "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", + "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", + "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", + "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", + "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75", + "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", + "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", + "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", + "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", + "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", + "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", + "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", + "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", + "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf", + "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", + "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", + "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", + "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", + "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", + "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02", + "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", + "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05", + "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", + "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", + "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", + "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98", + "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", + "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", + "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", + "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89", + "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", + "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", + "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", + "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", + "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", + "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", + "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", + "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", + "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8", + "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", + "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406", + "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", + "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", + "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", + "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", + "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", + "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", + "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", + "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", + "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", + "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", + "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", + "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", + "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530", + "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", + "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d", + "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", + "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", + "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54", + "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d", + "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", + "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", + "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", + "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", + "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", + "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", + "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", + "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0", + "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", + "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", + "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", + "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", + "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", + "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.11.18" + "version": "==3.12.15" }, "aiosignal": { "hashes": [ - "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", - "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54" + "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.3.2" + "version": "==1.4.0" }, "attrs": { "hashes": [ @@ -133,12 +138,12 @@ }, "build": { "hashes": [ - "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", - "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7" + "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", + "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.2.2.post1" + "markers": "python_version >= '3.9'", + "version": "==1.3.0" }, "dnspython": { "hashes": [ @@ -151,114 +156,114 @@ }, "frozenlist": { "hashes": [ - "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117", - "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2", - "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42", - "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812", - "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3", - "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a", - "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572", - "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa", - "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b", - "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626", - "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e", - "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d", - "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c", - "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530", - "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", - "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e", - "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869", - "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd", - "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603", - "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606", - "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85", - "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64", - "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f", - "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0", - "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c", - "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4", - "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", - "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02", - "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", - "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0", - "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e", - "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791", - "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983", - "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f", - "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff", - "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8", - "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860", - "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8", - "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", - "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f", - "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba", - "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24", - "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e", - "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd", - "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911", - "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c", - "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595", - "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c", - "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", - "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2", - "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75", - "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4", - "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0", - "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe", - "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249", - "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", - "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", - "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b", - "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", - "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", - "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584", - "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497", - "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f", - "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f", - "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b", - "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", - "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d", - "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a", - "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", - "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", - "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189", - "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", - "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", - "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", - "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0", - "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3", - "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", - "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0", - "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215", - "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", - "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c", - "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", - "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1", - "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769", - "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506", - "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3", - "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348", - "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad", - "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a", - "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad", - "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6", - "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45", - "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188", - "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e", - "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", - "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70", - "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1", - "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106", - "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd", - "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc", - "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352", - "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91", - "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1", - "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f" + "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", + "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", + "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", + "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", + "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6", + "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", + "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", + "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", + "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677", + "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", + "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", + "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", + "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", + "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", + "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", + "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", + "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", + "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938", + "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", + "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", + "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", + "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", + "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", + "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", + "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", + "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", + "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", + "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", + "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", + "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", + "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", + "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", + "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63", + "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", + "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", + "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", + "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", + "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", + "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", + "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", + "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", + "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", + "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", + "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", + "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", + "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", + "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", + "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", + "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", + "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87", + "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", + "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", + "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71", + "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", + "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", + "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2", + "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", + "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", + "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", + "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", + "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", + "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878", + "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", + "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890", + "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", + "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", + "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb", + "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", + "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", + "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", + "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", + "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", + "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", + "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", + "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", + "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", + "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", + "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e", + "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", + "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", + "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", + "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", + "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", + "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", + "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb", + "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", + "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", + "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", + "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630", + "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", + "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", + "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", + "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44", + "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319", + "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", + "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", + "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35", + "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", + "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", + "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd", + "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", + "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", + "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", + "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.6.0" + "version": "==1.7.0" }, "idna": { "hashes": [ @@ -288,114 +293,120 @@ }, "multidict": { "hashes": [ - "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", - "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", - "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef", - "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", - "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", - "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8", - "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", - "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", - "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", - "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", - "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", - "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", - "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", - "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", - "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9", - "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", - "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e", - "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b", - "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", - "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", - "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", - "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", - "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", - "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", - "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", - "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5", - "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", - "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", - "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", - "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", - "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666", - "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", - "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf", - "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", - "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", - "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", - "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", - "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", - "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", - "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21", - "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7", - "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", - "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343", - "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", - "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", - "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", - "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", - "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459", - "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6", - "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", - "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", - "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", - "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", - "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", - "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", - "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", - "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5", - "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", - "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", - "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1", - "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", - "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7", - "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", - "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", - "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39", - "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752", - "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", - "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188", - "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451", - "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078", - "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7", - "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", - "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f", - "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", - "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f", - "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", - "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291", - "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897", - "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887", - "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", - "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685", - "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", - "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", - "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", - "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", - "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", - "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", - "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", - "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", - "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", - "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df", - "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", - "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", - "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", - "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c", - "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840", - "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", - "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", - "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", - "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", - "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e", - "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2", - "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1", - "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad" + "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", + "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", + "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", + "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", + "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", + "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", + "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f", + "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1", + "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", + "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", + "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", + "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", + "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae", + "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", + "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", + "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", + "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", + "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", + "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", + "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0", + "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", + "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", + "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", + "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", + "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0", + "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", + "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", + "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", + "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", + "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", + "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", + "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", + "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", + "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", + "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", + "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", + "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", + "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", + "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb", + "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", + "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", + "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", + "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", + "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", + "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", + "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", + "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb", + "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", + "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", + "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", + "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", + "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", + "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", + "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", + "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", + "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", + "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", + "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", + "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", + "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", + "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b", + "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", + "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", + "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", + "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", + "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", + "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978", + "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", + "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", + "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", + "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", + "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", + "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", + "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4", + "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665", + "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", + "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", + "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", + "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", + "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", + "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17", + "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", + "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", + "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", + "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", + "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", + "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", + "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", + "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", + "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", + "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", + "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd", + "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", + "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", + "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", + "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", + "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", + "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", + "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", + "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", + "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", + "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb", + "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210", + "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53", + "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", + "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", + "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9", + "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", + "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a", + "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==6.4.3" + "version": "==6.6.4" }, "packaging": { "hashes": [ @@ -417,108 +428,108 @@ }, "propcache": { "hashes": [ - "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", - "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", - "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf", - "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", - "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5", - "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c", - "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", - "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", - "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", - "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", - "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", - "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42", - "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035", - "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0", - "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e", - "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46", - "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", - "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", - "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", - "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", - "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", - "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", - "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", - "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833", - "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259", - "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136", - "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", - "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", - "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", - "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", - "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f", - "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", - "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", - "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", - "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566", - "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", - "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908", - "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf", - "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", - "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64", - "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", - "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71", - "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", - "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", - "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", - "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5", - "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894", - "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe", - "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", - "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", - "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", - "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", - "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641", - "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", - "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649", - "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", - "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd", - "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", - "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", - "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229", - "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", - "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", - "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", - "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", - "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", - "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294", - "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", - "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", - "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", - "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", - "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", - "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7", - "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", - "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", - "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", - "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", - "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7", - "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519", - "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", - "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180", - "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", - "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", - "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", - "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", - "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", - "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", - "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", - "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6", - "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c", - "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", - "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", - "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98", - "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", - "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", - "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", - "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", - "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", - "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5" + "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", + "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", + "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", + "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", + "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", + "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", + "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", + "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", + "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", + "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4", + "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", + "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", + "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea", + "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", + "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2", + "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", + "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", + "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", + "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb", + "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", + "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef", + "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe", + "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", + "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", + "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", + "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", + "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", + "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", + "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", + "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1", + "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", + "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", + "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", + "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", + "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", + "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", + "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", + "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", + "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", + "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", + "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", + "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", + "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", + "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", + "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", + "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", + "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", + "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", + "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701", + "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9", + "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", + "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", + "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", + "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", + "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", + "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", + "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", + "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", + "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", + "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", + "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", + "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", + "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", + "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", + "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", + "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", + "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5", + "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", + "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1", + "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", + "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", + "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", + "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", + "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", + "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", + "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", + "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", + "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", + "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", + "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b", + "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", + "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", + "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", + "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec", + "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886", + "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb", + "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", + "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", + "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d", + "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", + "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", + "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", + "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", + "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", + "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", + "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", + "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", + "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.3.1" + "version": "==0.3.2" }, "py-eureka-client": { "hashes": [ @@ -529,6 +540,14 @@ "markers": "python_version >= '3.7'", "version": "==0.11.13" }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, "pyproject-hooks": { "hashes": [ "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", @@ -540,30 +559,30 @@ }, "pytest": { "hashes": [ - "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", - "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845" + "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", + "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==8.3.5" + "markers": "python_version >= '3.9'", + "version": "==8.4.1" }, "pytest-asyncio": { "hashes": [ - "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", - "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f" + "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", + "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.26.0" + "version": "==1.1.0" }, "pytest-mock": { "hashes": [ - "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", - "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" + "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", + "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.14.0" + "version": "==3.14.1" }, "pyyaml": { "hashes": [ @@ -627,114 +646,114 @@ }, "yarl": { "hashes": [ - "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", - "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa", - "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61", - "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2", - "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2", - "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", - "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902", - "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2", - "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914", - "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", - "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", - "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569", - "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", - "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", - "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20", - "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", - "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", - "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc", - "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", - "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a", - "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", - "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", - "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", - "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5", - "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", - "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", - "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", - "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0", - "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26", - "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", - "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c", - "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", - "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", - "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", - "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a", - "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", - "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", - "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", - "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", - "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", - "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2", - "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", - "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", - "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", - "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", - "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e", - "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", - "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", - "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe", - "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", - "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", - "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", - "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", - "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62", - "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", - "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", - "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791", - "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", - "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb", - "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94", - "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", - "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10", - "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", - "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", - "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", - "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da", - "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c", - "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", - "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", - "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195", - "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", - "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8", - "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634", - "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", - "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8", - "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", - "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", - "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076", - "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656", - "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", - "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19", - "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a", - "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4", - "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2", - "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64", - "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", - "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7", - "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995", - "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6", - "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f", - "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", - "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487", - "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9", - "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a", - "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d", - "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", - "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", - "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22", - "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d", - "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c", - "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", - "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5", - "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867", - "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3" + "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", + "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", + "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", + "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", + "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", + "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", + "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", + "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010", + "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", + "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", + "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", + "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", + "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", + "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", + "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", + "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", + "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", + "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8", + "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805", + "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", + "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", + "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", + "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", + "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b", + "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", + "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", + "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", + "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", + "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", + "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", + "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", + "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", + "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", + "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", + "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", + "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", + "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240", + "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", + "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", + "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", + "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba", + "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", + "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", + "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", + "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", + "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", + "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", + "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d", + "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", + "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", + "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", + "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723", + "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", + "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", + "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", + "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", + "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", + "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", + "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", + "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", + "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", + "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", + "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", + "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", + "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", + "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", + "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", + "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", + "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000", + "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", + "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", + "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", + "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", + "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", + "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", + "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06", + "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", + "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", + "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", + "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", + "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", + "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", + "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", + "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c", + "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", + "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", + "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", + "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", + "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e", + "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", + "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee", + "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", + "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", + "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", + "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", + "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", + "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3", + "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00", + "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983", + "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", + "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", + "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", + "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", + "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.20.0" + "version": "==1.20.1" } }, "develop": {} From 18efa13b9bab7e2fc9685087d0f4c96b7eacca4e Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:23:53 +0200 Subject: [PATCH 079/152] fix: attls filter in modulith mode and Ltpa2 token (#4285) Signed-off-by: ac892247 Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../java/org/zowe/apiml/ApimlApplication.java | 10 ++-- .../java/org/zowe/apiml/ModulithConfig.java | 36 ++++++------- .../apiml/acceptance/AttlsConfigTest.java | 52 +++++++++++-------- .../filters/AbstractTokenFilterFactory.java | 8 ++- .../client/services/apars/FunctionalApar.java | 15 ++++-- .../apiml/client/services/apars/PHBase.java | 6 ++- 6 files changed, 73 insertions(+), 54 deletions(-) diff --git a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java index e3eaa5ec8e..1e329130e5 100644 --- a/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java +++ b/apiml/src/main/java/org/zowe/apiml/ApimlApplication.java @@ -16,13 +16,12 @@ import org.springframework.cloud.netflix.eureka.server.EurekaController; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; -import org.zowe.apiml.gateway.config.GatewayHealthIndicator; import org.zowe.apiml.enable.EnableApiDiscovery; import org.zowe.apiml.enable.config.EnableApiDiscoveryConfig; import org.zowe.apiml.enable.register.RegisterToApiLayer; +import org.zowe.apiml.gateway.config.GatewayHealthIndicator; -@SpringBootApplication( - exclude = { ReactiveOAuth2ClientAutoConfiguration.class }, +@SpringBootApplication(exclude = {ReactiveOAuth2ClientAutoConfiguration.class}, scanBasePackages = { "org.zowe.apiml.filter", "org.zowe.apiml.gateway", @@ -34,8 +33,7 @@ "org.zowe.apiml.product.service", "org.zowe.apiml.security", "org.zowe.apiml.discovery" - } -) + }) @ComponentScan( excludeFilters = { @ComponentScan.Filter( @@ -44,7 +42,7 @@ ), @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, - classes = { EnableApiDiscoveryConfig.class, EurekaController.class, RegisterToApiLayer.class, GatewayHealthIndicator.class } + classes = {EnableApiDiscoveryConfig.class, EurekaController.class, RegisterToApiLayer.class, GatewayHealthIndicator.class} ), @ComponentScan.Filter( type = FilterType.ANNOTATION, diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index 25e31d755d..48b00c4f8d 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -19,13 +19,7 @@ import com.netflix.discovery.shared.Applications; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; +import jakarta.servlet.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.Context; @@ -33,9 +27,13 @@ import org.apache.catalina.connector.Connector; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; +import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; +import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.cloud.client.ServiceInstance; @@ -72,15 +70,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Timer; -import java.util.TimerTask; +import java.util.*; import static org.zowe.apiml.services.ServiceInfoUtils.getInstances; import static org.zowe.apiml.services.ServiceInfoUtils.getStatus; @@ -324,10 +314,12 @@ public List getServicesInfo() { @Primary TomcatReactiveWebServerFactory tomcatReactiveWebServerWithFiltersFactory( HttpHandler httpHandler, - List preFluxFilters, + List preFluxFilters, ObjectProvider connectorCustomizers, + ObjectProvider contextCustomizers, + ObjectProvider> protocolHandlerCustomizers, List servletContextAwareListeners) { - return new TomcatReactiveWebServerFactory() { + var factory = new TomcatReactiveWebServerFactory() { @Override protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet) { super.prepareContext(host, new ServletWithFilters(httpHandler, servlet, preFluxFilters)); @@ -339,8 +331,13 @@ protected void configureContext(Context context) { super.configureContext(context); } }; + factory.getTomcatConnectorCustomizers().addAll(connectorCustomizers.orderedStream().toList()); + factory.getTomcatContextCustomizers().addAll(contextCustomizers.orderedStream().toList()); + factory.getTomcatProtocolHandlerCustomizers().addAll(protocolHandlerCustomizers.orderedStream().toList()); + return factory; } + /** * Create a custom Tomcat connector with same customizations as the main * external (GW) connector to handle @@ -352,7 +349,7 @@ protected void configureContext(Context context) { */ @Bean WebServerFactoryCustomizer internalPortCustomizer( - @Value("${apiml.internal-discovery.port:10011}") int internalDiscoveryPort) { + @Value("${apiml.internal-discovery.port:10011}") int internalDiscoveryPort, List connectorCustomizers) { return factory -> { var connector = new Connector(); @@ -368,6 +365,7 @@ WebServerFactoryCustomizer internalPortCustomize connector.setPort(internalDiscoveryPort); factory.addAdditionalTomcatConnectors(connector); + factory.addConnectorCustomizers(connectorCustomizers.toArray(new TomcatConnectorCustomizer[0])); }; } diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java index 8e49f922a8..04bb930206 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java @@ -34,6 +34,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; import org.zowe.apiml.ApimlApplication; import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.filter.AttlsHttpHandler; @@ -47,11 +48,7 @@ import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @TestInstance(Lifecycle.PER_CLASS) class AttlsConfigTest { @@ -61,7 +58,7 @@ private String getGatewayUrlWithPath(String hostname, int port, String scheme, S } @Nested - @ActiveProfiles({ "attlsClient", "attlsServer" }) + @ActiveProfiles({"attlsClient", "attlsServer"}) @DirtiesContext @SpringBootTest( classes = ApimlApplication.class, @@ -89,9 +86,9 @@ void whenContextloads_requestFailsWithHttps() { assertThrows(SSLException.class, () -> { given() .log().all() - .when() + .when() .get(getGatewayUrlWithPath(hostname, port, "https", "application/version")) - .then() + .then() .log().all(); }); } @@ -106,18 +103,18 @@ void requestFailsWithAttlsReasonWithHttp() { doNothing().when(apimlTomcatCustomizer).customize(any()); given() - .log().all() + .log().all() .when() - .get(getGatewayUrlWithPath(hostname, port, "http", "application/version")) + .get(getGatewayUrlWithPath(hostname, port, "http", "application/version")) .then() - .log().all() - .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) - .body(containsString("org.zowe.apiml.common.internalServerError")); + .log().all() + .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .body(containsString("org.zowe.apiml.common.internalServerError")); - verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); - assertThat(loggingEventCaptor.getAllValues()) - .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) - .isNotEmpty(); + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + assertThat(loggingEventCaptor.getAllValues()) + .filteredOn(element -> element.getMessage().contains("Cannot verify AT-TLS status")) + .isNotEmpty(); } } @@ -135,8 +132,16 @@ void requestFailsWithAttlsReasonWithHttp() { "server.ssl.keyStore=" } ) - @ActiveProfiles({ "attlsServer", "attlsClient", "ApimlModulithAcceptanceTest" }) - @AcceptanceTest + @ActiveProfiles({"attlsServer", "attlsClient", "ApimlModulithAcceptanceTest"}) + @DirtiesContext + @SpringBootTest( + classes = { + ApimlApplication.class, + FreeMarkerConfigurer.class, + TestConfig.class + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) class GivenSslDisabled { @MockitoBean @@ -150,21 +155,24 @@ class GivenSslDisabled { @Value("${apiml.service.hostname:localhost}") private String hostname; + @MockitoBean + private ApimlTomcatCustomizer apimlTomcatCustomizer; @BeforeEach void setUp() { when(apimlInstanceRegistry.getApplications()).thenReturn(new Applications()); + doNothing().when(apimlTomcatCustomizer).customize(any()); } @Test void whenNoKeystore_thenStartupSuccess() { given() .log().all() - .when() + .when() .get(getGatewayUrlWithPath(hostname, port, "http", "application/version")) - .then() + .then() .statusCode(SC_OK); - + verify(apimlTomcatCustomizer, times(1)).customize(any()); verify(attlsHttpHandler, times(1)).postProcessAfterInitialization(any(HttpHandler.class), any()); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java index 0de2b3252d..59ecf7cf9d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java @@ -76,7 +76,9 @@ protected Mono processResponse(ServerWebExchange exchange, GatewayFilterCh response.get().getToken() ); headers.set(HttpHeaders.COOKIE, cookieHeader); - headers.set(HttpHeaders.AUTHORIZATION, BEARER_AUTHENTICATION_PREFIX + " " + response.get().getToken()); + if (!isLtpaToken(response)) { + headers.set(HttpHeaders.AUTHORIZATION, BEARER_AUTHENTICATION_PREFIX + " " + response.get().getToken()); + } }).build(); exchange = exchange.mutate().request(request).build(); } @@ -103,6 +105,10 @@ protected Mono processResponse(ServerWebExchange exchange, GatewayFilterCh return chain.filter(exchange); } + private boolean isLtpaToken(AtomicReference response) { + return "LtpaToken2".equals(response.get().getCookieName()); + } + @EqualsAndHashCode(callSuper = true) public static class Config extends AbstractAuthSchemeFactory.AbstractConfig { diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java index 87592292e5..0cfa8fbd3a 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java @@ -10,6 +10,8 @@ package org.zowe.apiml.client.services.apars; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -17,8 +19,6 @@ import org.zowe.apiml.client.model.LoginBody; import org.zowe.apiml.client.services.JwtTokenService; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; import java.util.Base64; import java.util.List; import java.util.Map; @@ -29,6 +29,7 @@ public class FunctionalApar implements Apar { private static final String COOKIE_HEADER = "cookie"; private static final String JWT_TOKEN_NAME = "jwtToken"; private static final String LTPA_TOKEN_NAME = "LtpaToken2"; + private static final String LTPA_TOKEN_VALUE = "paMypL7yRO/IBroQtro21/uSC2LTrJvOuYebHaPc6JAUNWQ7lEHHt1l3CYeXa/nP6aKLFHTuyWy3qlRXvt10PjVdVl+7Q+wavgIsro7odz+PvTaJBp/+r0AH+DHYcdZikKe8dytGYZRH2c2gw8Gv3PliDIMd1iPEazY4HeYTU5VCFM5cBJkeIoTXCfL5ud9wTzrkY2c4h1PQPtx+hYCF4kEpiVkqIypVwjQLzWdJGV1Ihz7NqH/UU9MMJRXY1xMqsWZSibs2fX5MVK77dnyBrNYjVXA7PqYL6U/v5/1UCvuYQ/iEU9+Uy95J+xFEsnTX"; protected static final String AUTHORIZATION_HEADER = "authorization"; @@ -225,19 +226,23 @@ protected boolean isValidAuthHeader(String authHeader) { } if (authHeader.startsWith("Bearer")) { - var jwtToken = authHeader.length() > 8 ? authHeader.substring(7) : ""; - return jwtTokenService.validateJwtToken(jwtToken); + return isValidTokenInAuthHeader(authHeader); } return true; } + public boolean isValidTokenInAuthHeader(String authHeader) { + var jwtToken = authHeader.length() > 8 ? authHeader.substring(7) : ""; + return jwtTokenService.validateJwtToken(jwtToken); + } + private String getAuthCookie(Map headers) { return headers.get(COOKIE_HEADER) != null ? headers.get(COOKIE_HEADER) : headers.get(HttpHeaders.COOKIE); } protected void setLtpaToken(HttpServletResponse response) { - Cookie ltpaToken = new Cookie(LTPA_TOKEN_NAME, "paMypL7yRO/IBroQtro21/uSC2LTrJvOuYebHaPc6JAUNWQ7lEHHt1l3CYeXa/nP6aKLFHTuyWy3qlRXvt10PjVdVl+7Q+wavgIsro7odz+PvTaJBp/+r0AH+DHYcdZikKe8dytGYZRH2c2gw8Gv3PliDIMd1iPEazY4HeYTU5VCFM5cBJkeIoTXCfL5ud9wTzrkY2c4h1PQPtx+hYCF4kEpiVkqIypVwjQLzWdJGV1Ihz7NqH/UU9MMJRXY1xMqsWZSibs2fX5MVK77dnyBrNYjVXA7PqYL6U/v5/1UCvuYQ/iEU9+Uy95J+xFEsnTX"); + Cookie ltpaToken = new Cookie(LTPA_TOKEN_NAME, LTPA_TOKEN_VALUE); ltpaToken.setSecure(true); ltpaToken.setHttpOnly(true); diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java index 19633d5e13..4e7ac052e4 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java @@ -10,10 +10,10 @@ package org.zowe.apiml.client.services.apars; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import jakarta.servlet.http.HttpServletResponse; import java.util.List; import java.util.Map; @@ -73,6 +73,10 @@ protected ResponseEntity handleFiles(Map headers) { if (!isValidAuthHeader(authorization) && !ltpaIsPresent(headers)) { return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } + // if auth header contains invalid token, zOSMF returns 401 + if (authorization.startsWith("Bearer") && !isValidTokenInAuthHeader(authorization)) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } } else { if (!isValidJwtCookie(headers) && !ltpaIsPresent(headers)) { return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); From 11c36ecd63ffe3b59498f9cff1cb69b1038d7e3c Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Mon, 25 Aug 2025 04:14:14 -0400 Subject: [PATCH 080/152] chore: Update all non-major dependencies (v3.x.x) (#4291) Signed-off-by: Renovate Bot Signed-off-by: ac892247 Co-authored-by: Renovate Bot Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Co-authored-by: ac892247 Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/package-lock.json | 42 ++-- api-catalog-ui/frontend/package.json | 10 +- gradle/versions.gradle | 6 +- onboarding-enabler-nodejs/package-lock.json | 18 +- onboarding-enabler-nodejs/package.json | 4 +- onboarding-enabler-python-sample-app/Pipfile | 2 +- .../Pipfile.lock | 206 +++++++++--------- .../package-lock.json | 18 +- zowe-cli-id-federation-plugin/package.json | 4 +- 9 files changed, 155 insertions(+), 155 deletions(-) diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index 9fa0018b1d..e96f1f9fd1 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -21,7 +21,7 @@ "@react-loadable/revised": "1.5.0", "@types/enzyme": "3.10.19", "@types/jest": "29.5.14", - "@types/react": "18.3.23", + "@types/react": "18.3.24", "agentkeepalive": "4.6.0", "buffer": "6.0.3", "emotion-theming": "11.0.0", @@ -43,7 +43,7 @@ "react-error-boundary": "5.0.0", "react-hook-form": "7.62.0", "react-redux": "9.2.0", - "react-router": "7.8.1", + "react-router": "7.8.2", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -68,7 +68,7 @@ "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", "@eslint/compat": "1.3.2", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@reduxjs/toolkit": "2.8.2", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.8.0", @@ -77,14 +77,14 @@ "ajv": "8.17.1", "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001736", + "caniuse-lite": "1.0.30001737", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.33.0", + "eslint": "9.34.0", "eslint-config-airbnb": "19.0.4", "eslint-config-prettier": "9.1.2", "eslint-plugin-cypress": "4.3.0", @@ -3057,9 +3057,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.34.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -7114,9 +7114,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "version": "18.3.24", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -9406,9 +9406,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001736", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz", - "integrity": "sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==", + "version": "1.0.30001737", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", + "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", "funding": [ { "type": "opencollective", @@ -12214,9 +12214,9 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.34.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -12226,7 +12226,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -24044,9 +24044,9 @@ } }, "node_modules/react-router": { - "version": "7.8.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.8.1.tgz", - "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "version": "7.8.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-router/-/react-router-7.8.2.tgz", + "integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 41d898aa9b..e1bdfa2f2f 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -17,7 +17,7 @@ "@react-loadable/revised": "1.5.0", "@types/enzyme": "3.10.19", "@types/jest": "29.5.14", - "@types/react": "18.3.23", + "@types/react": "18.3.24", "agentkeepalive": "4.6.0", "buffer": "6.0.3", "emotion-theming": "11.0.0", @@ -39,7 +39,7 @@ "react-error-boundary": "5.0.0", "react-hook-form": "7.62.0", "react-redux": "9.2.0", - "react-router": "7.8.1", + "react-router": "7.8.2", "react-toastify": "10.0.6", "redux": "5.0.1", "redux-catch": "1.3.1", @@ -89,7 +89,7 @@ "@babel/preset-react": "7.27.1", "@cfaester/enzyme-adapter-react-18": "0.8.0", "@eslint/compat": "1.3.2", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.8.0", "@testing-library/react": "16.3.0", @@ -98,14 +98,14 @@ "ajv": "8.17.1", "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001736", + "caniuse-lite": "1.0.30001737", "concurrently": "9.2.0", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", "cypress-file-upload": "5.0.8", "enzyme": "3.11.0", - "eslint": "9.33.0", + "eslint": "9.34.0", "eslint-config-airbnb": "19.0.4", "eslint-config-prettier": "9.1.2", "eslint-plugin-cypress": "4.3.0", diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 7b8bbbf377..bb7e172d52 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -14,7 +14,7 @@ dependencyResolutionManagement { version('springFramework', '6.2.10') version('springRetry', '2.0.12') - version('modulith', '1.4.2') + version('modulith', '1.4.3') version('jmolecules', '2023.3.2') version('glassfishHk2', '3.1.1') @@ -83,7 +83,7 @@ dependencyResolutionManagement { version('reactor', '3.7.9') version('restAssured', '5.5.6') version('rhino', '1.8.0') - version('springDoc', '2.8.10') + version('springDoc', '2.8.11') version('swaggerCore', '2.2.36') version('swaggerInflector', '2.0.13') version('swagger2Parser', '1.0.75') @@ -108,7 +108,7 @@ dependencyResolutionManagement { version('jacoco', '0.8.11') version('gradle', '8.6') version('commonsCompress', '1.28.0') - version('bucket4j', '8.14.0') + version('bucket4j', '8.15.0') version('xstream', '1.4.21') library('zowe_zos_utils', 'org.zowe.apiml.sdk', 'zos-utils').versionRef('zosUtils') diff --git a/onboarding-enabler-nodejs/package-lock.json b/onboarding-enabler-nodejs/package-lock.json index b2bcef1358..48da0f7c40 100644 --- a/onboarding-enabler-nodejs/package-lock.json +++ b/onboarding-enabler-nodejs/package-lock.json @@ -17,7 +17,7 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.3.1", + "chai": "5.3.3", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", @@ -30,7 +30,7 @@ "gulp-mocha": "10.0.1", "nock": "13.5.6", "sinon": "19.0.5", - "sinon-chai": "4.0.0" + "sinon-chai": "4.0.1" }, "engines": { "node": "=20.19.4", @@ -1621,9 +1621,9 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.1.tgz", - "integrity": "sha512-48af6xm9gQK8rhIcOxWwdGzIervm8BVTin+yRp9HEvU20BtVZ2lBywlIJBzwaDtvo0FvjeL7QdCADoUoqIbV3A==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -7133,13 +7133,13 @@ } }, "node_modules/sinon-chai": { - "version": "4.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sinon-chai/-/sinon-chai-4.0.0.tgz", - "integrity": "sha512-cWqO7O2I4XfJDWyWElAQ9D/dtdh5Mo0RHndsfiiYyjWnlPzBJdIvjCVURO4EjyYaC3BjV+ISNXCfTXPXTEIEWA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-4.0.1.tgz", + "integrity": "sha512-xMKEEV3cYHC1G+boyr7QEqi80gHznYsxVdC9CdjP5JnCWz/jPGuXQzJz3PtBcb0CcHAxar15Y5sjLBoAs6a0yA==", "dev": true, "license": "(BSD-2-Clause OR WTFPL)", "peerDependencies": { - "chai": "^5.0.0", + "chai": "^5.0.0 || ^6.0.0", "sinon": ">=4.0.0" } }, diff --git a/onboarding-enabler-nodejs/package.json b/onboarding-enabler-nodejs/package.json index 449397a293..3fc0e21b55 100644 --- a/onboarding-enabler-nodejs/package.json +++ b/onboarding-enabler-nodejs/package.json @@ -27,7 +27,7 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.3.1", + "chai": "5.3.3", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", @@ -40,7 +40,7 @@ "gulp-mocha": "10.0.1", "nock": "13.5.6", "sinon": "19.0.5", - "sinon-chai": "4.0.0" + "sinon-chai": "4.0.1" }, "greenkeeper": { "ignore": [ diff --git a/onboarding-enabler-python-sample-app/Pipfile b/onboarding-enabler-python-sample-app/Pipfile index 0e4428f438..d36308dc84 100644 --- a/onboarding-enabler-python-sample-app/Pipfile +++ b/onboarding-enabler-python-sample-app/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] aiohappyeyeballs = ">=2.4.3" -aiohttp = ">=3.10.10" +aiohttp = ">=3.12.15" aiosignal = ">=1.3.1" annotated-types = ">=0.7.0" anyio = ">=4.6.2.post1" diff --git a/onboarding-enabler-python-sample-app/Pipfile.lock b/onboarding-enabler-python-sample-app/Pipfile.lock index a06f961d16..ea9d23ca17 100644 --- a/onboarding-enabler-python-sample-app/Pipfile.lock +++ b/onboarding-enabler-python-sample-app/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b3123a4848d5cdfcd209e4e2d64c458a968ffcbbb04fb7a45d1db0b3dceef75c" + "sha256": "854444b3435b77dad94779320238ac3a38eae44266441c8bf1442c7354a0dfd7" }, "pipfile-spec": 6, "requires": { @@ -209,12 +209,12 @@ }, "beautifulsoup4": { "hashes": [ - "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", - "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195" + "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", + "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a" ], "index": "pypi", "markers": "python_full_version >= '3.7.0'", - "version": "==4.13.4" + "version": "==4.13.5" }, "bleach": { "extras": [ @@ -1054,12 +1054,12 @@ }, "parso": { "hashes": [ - "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", - "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" + "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", + "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==0.8.4" + "version": "==0.8.5" }, "pexpect": { "hashes": [ @@ -1508,102 +1508,102 @@ }, "pyzmq": { "hashes": [ - "sha256:05a94233fdde585eb70924a6e4929202a747eea6ed308a6171c4f1c715bbe39e", - "sha256:092f4011b26d6b0201002f439bd74b38f23f3aefcb358621bdc3b230afc9b2d5", - "sha256:0ec09073ed67ae236785d543df3b322282acc0bdf6d1b748c3e81f3043b21cb5", - "sha256:0f772eea55cccce7f45d6ecdd1d5049c12a77ec22404f6b892fae687faa87bee", - "sha256:0fc24bf45e4a454e55ef99d7f5c8b8712539200ce98533af25a5bfa954b6b390", - "sha256:119ce8590409702394f959c159d048002cbed2f3c0645ec9d6a88087fc70f0f1", - "sha256:1843fd0daebcf843fe6d4da53b8bdd3fc906ad3e97d25f51c3fed44436d82a49", - "sha256:19dce6c93656f9c469540350d29b128cd8ba55b80b332b431b9a1e9ff74cfd01", - "sha256:1c363c6dc66352331d5ad64bb838765c6692766334a6a02fdb05e76bd408ae18", - "sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a", - "sha256:1da8e645c655d86f0305fb4c65a0d848f461cd90ee07d21f254667287b5dbe50", - "sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55", - "sha256:27a78bdd384dbbe7b357af95f72efe8c494306b5ec0a03c31e2d53d6763e5307", - "sha256:2852f67371918705cc18b321695f75c5d653d5d8c4a9b946c1eec4dab2bd6fdf", - "sha256:313a7b374e3dc64848644ca348a51004b41726f768b02e17e689f1322366a4d9", - "sha256:351bf5d8ca0788ca85327fda45843b6927593ff4c807faee368cc5aaf9f809c2", - "sha256:4401649bfa0a38f0f8777f8faba7cd7eb7b5b8ae2abc7542b830dd09ad4aed0d", - "sha256:44909aa3ed2234d69fe81e1dade7be336bcfeab106e16bdaa3318dcde4262b93", - "sha256:45c3e00ce16896ace2cd770ab9057a7cf97d4613ea5f2a13f815141d8b6894b9", - "sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b", - "sha256:497bd8af534ae55dc4ef67eebd1c149ff2a0b0f1e146db73c8b5a53d83c1a5f5", - "sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb", - "sha256:4bca8abc31799a6f3652d13f47e0b0e1cab76f9125f2283d085a3754f669b607", - "sha256:4c3874344fd5fa6d58bb51919708048ac4cab21099f40a227173cddb76b4c20b", - "sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1", - "sha256:5268a5a9177afff53dc6d70dffe63114ba2a6e7b20d9411cc3adeba09eeda403", - "sha256:544b995a6a1976fad5d7ff01409b4588f7608ccc41be72147700af91fd44875d", - "sha256:56a3b1853f3954ec1f0e91085f1350cc57d18f11205e4ab6e83e4b7c414120e0", - "sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed", - "sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb", - "sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e", - "sha256:64ca3c7c614aefcdd5e358ecdd41d1237c35fe1417d01ec0160e7cdb0a380edc", - "sha256:678e50ec112bdc6df5a83ac259a55a4ba97a8b314c325ab26b3b5b071151bc61", - "sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1", - "sha256:6dcbcb34f5c9b0cefdfc71ff745459241b7d3cda5b27c7ad69d45afc0821d1e1", - "sha256:6f02f30a4a6b3efe665ab13a3dd47109d80326c8fd286311d1ba9f397dc5f247", - "sha256:70b719a130b81dd130a57ac0ff636dc2c0127c5b35ca5467d1b67057e3c7a4d2", - "sha256:72d235d6365ca73d8ce92f7425065d70f5c1e19baa458eb3f0d570e425b73a96", - "sha256:7418fb5736d0d39b3ecc6bec4ff549777988feb260f5381636d8bd321b653038", - "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811", - "sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f", - "sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94", - "sha256:87aebf4acd7249bdff8d3df03aed4f09e67078e6762cfe0aecf8d0748ff94cde", - "sha256:88dc92d9eb5ea4968123e74db146d770b0c8d48f0e2bfb1dbc6c50a8edb12d64", - "sha256:8c62297bc7aea2147b472ca5ca2b4389377ad82898c87cabab2a94aedd75e337", - "sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd", - "sha256:90a4da42aa322de8a3522461e3b5fe999935763b27f69a02fced40f4e3cf9682", - "sha256:95594b2ceeaa94934e3e94dd7bf5f3c3659cf1a26b1fb3edcf6e42dad7e0eaf2", - "sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296", - "sha256:9d16fdfd7d70a6b0ca45d36eb19f7702fa77ef6256652f17594fc9ce534c9da6", - "sha256:9d7b6b90da7285642f480b48c9efd1d25302fd628237d8f6f6ee39ba6b2d2d34", - "sha256:a066ea6ad6218b4c233906adf0ae67830f451ed238419c0db609310dd781fbe7", - "sha256:a27fa11ebaccc099cac4309c799aa33919671a7660e29b3e465b7893bc64ec81", - "sha256:a4aca06ba295aa78bec9b33ec028d1ca08744c36294338c41432b7171060c808", - "sha256:af2ee67b3688b067e20fea3fe36b823a362609a1966e7e7a21883ae6da248804", - "sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd", - "sha256:b007e5dcba684e888fbc90554cb12a2f4e492927c8c2761a80b7590209821743", - "sha256:b25e72e115399a4441aad322258fa8267b873850dc7c276e3f874042728c2b45", - "sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb", - "sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab", - "sha256:b9fd0fda730461f510cfd9a40fafa5355d65f5e3dbdd8d6dfa342b5b3f5d1949", - "sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41", - "sha256:be45a895f98877271e8a0b6cf40925e0369121ce423421c20fa6d7958dc753c2", - "sha256:bee5248d5ec9223545f8cc4f368c2d571477ae828c99409125c3911511d98245", - "sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f", - "sha256:c9180d1f5b4b73e28b64e63cc6c4c097690f102aa14935a62d5dd7426a4e5b5a", - "sha256:c96702e1082eab62ae583d64c4e19c9b848359196697e536a0c57ae9bd165bd5", - "sha256:c9d63d66059114a6756d09169c9209ffceabacb65b9cb0f66e6fc344b20b73e6", - "sha256:ce181dd1a7c6c012d0efa8ab603c34b5ee9d86e570c03415bbb1b8772eeb381c", - "sha256:d0356a21e58c3e99248930ff73cc05b1d302ff50f41a8a47371aefb04327378a", - "sha256:d0b96c30be9f9387b18b18b6133c75a7b1b0065da64e150fe1feb5ebf31ece1c", - "sha256:d2976b7079f09f48d59dc123293ed6282fca6ef96a270f4ea0364e4e54c8e855", - "sha256:d97b59cbd8a6c8b23524a8ce237ff9504d987dc07156258aa68ae06d2dd5f34d", - "sha256:da81512b83032ed6cdf85ca62e020b4c23dda87f1b6c26b932131222ccfdbd27", - "sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed", - "sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a", - "sha256:e4f22d67756518d71901edf73b38dc0eb4765cce22c8fe122cc81748d425262b", - "sha256:e648dca28178fc879c814cf285048dd22fd1f03e1104101106505ec0eea50a4d", - "sha256:e971d8680003d0af6020713e52f92109b46fedb463916e988814e04c8133578a", - "sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530", - "sha256:f293a1419266e3bf3557d1f8778f9e1ffe7e6b2c8df5c9dca191caf60831eb74", - "sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0", - "sha256:f44e7ea288d022d4bf93b9e79dafcb4a7aea45a3cbeae2116792904931cefccf", - "sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577", - "sha256:f65741cc06630652e82aa68ddef4986a3ab9073dd46d59f94ce5f005fa72037c", - "sha256:f8c3b74f1cd577a5a9253eae7ed363f88cbb345a990ca3027e9038301d47c7f4", - "sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808", - "sha256:f98f6b7787bd2beb1f0dde03f23a0621a0c978edf673b7d8f5e7bc039cbe1b60", - "sha256:fde26267416c8478c95432c81489b53f57b0b5d24cd5c8bfaebf5bbaac4dc90c", - "sha256:fe632fa4501154d58dfbe1764a0495734d55f84eaf1feda4549a1f1ca76659e9", - "sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686", - "sha256:ffe6b809a97ac6dea524b3b837d5b28743d8c2f121141056d168ff0ba8f614ef" + "sha256:05288947797dcd6724702db2056972dceef9963a83041eb734aea504416094ec", + "sha256:063845960df76599ad4fad69fa4d884b3ba38304272104fdcd7e3af33faeeb1d", + "sha256:0f6e9b00d81b58f859fffc112365d50413954e02aefe36c5b4c8fb4af79f8cc3", + "sha256:1326500792a9cb0992db06bbaf5d0098459133868932b81a6e90d45c39eca99d", + "sha256:25a100d2de2ac0c644ecf4ce0b509a720d12e559c77aff7e7e73aa684f0375bc", + "sha256:272d772d116615397d2be2b1417b3b8c8bc8671f93728c2f2c25002a4530e8f6", + "sha256:2cb5bcfc51c7a4fce335d3bc974fd1d6a916abbcdd2b25f6e89d37b8def25f57", + "sha256:2e73cf3b127a437fef4100eb3ac2ebe6b49e655bb721329f667f59eca0a26221", + "sha256:2ef3067cb5b51b090fb853f423ad7ed63836ec154374282780a62eb866bf5768", + "sha256:31c26a5d0b00befcaeeb600d8b15ad09f5604b6f44e2057ec5e521a9e18dcd9a", + "sha256:340e7cddc32f147c6c00d116a3f284ab07ee63dbd26c52be13b590520434533c", + "sha256:36508466a266cf78bba2f56529ad06eb38ba827f443b47388d420bec14d331ba", + "sha256:3660d85e2b6a28eb2d586dedab9c61a7b7c64ab0d89a35d2973c7be336f12b0d", + "sha256:38ff75b2a36e3a032e9fef29a5871e3e1301a37464e09ba364e3c3193f62982a", + "sha256:3b02ba0c0b2b9ebe74688002e6c56c903429924a25630804b9ede1f178aa5a3f", + "sha256:3e44e665d78a07214b2772ccbd4b9bcc6d848d7895f1b2d7653f047b6318a4f6", + "sha256:3e8f833dd82af11db5321c414638045c70f61009f72dd61c88db4a713c1fb1d2", + "sha256:400f34321e3bd89b1165b91ea6b18ad26042ba9ad0dfed8b35049e2e24eeab9b", + "sha256:4108785f2e5ac865d06f678a07a1901e3465611356df21a545eeea8b45f56265", + "sha256:41f0bd56d9279392810950feb2785a419c2920bbf007fdaaa7f4a07332ae492d", + "sha256:47c5dda2018c35d87be9b83de0890cb92ac0791fd59498847fc4eca6ff56671d", + "sha256:47eb65bb25478358ba3113dd9a08344f616f417ad3ffcbb190cd874fae72b1b1", + "sha256:49d8d05d9844d83cddfbc86a82ac0cafe7ab694fcc9c9618de8d015c318347c3", + "sha256:4e4520577971d01d47e2559bb3175fce1be9103b18621bf0b241abe0a933d040", + "sha256:4e4d88b6cff156fed468903006b24bbd85322612f9c2f7b96e72d5016fd3f543", + "sha256:4ecfc7999ac44c9ef92b5ae8f0b44fb935297977df54d8756b195a3cd12f38f0", + "sha256:515d20b5c3c86db95503faa989853a8ab692aab1e5336db011cd6d35626c4cb1", + "sha256:565bee96a155fe6452caed5fb5f60c9862038e6b51a59f4f632562081cdb4004", + "sha256:56d7de7bf73165b90bd25a8668659ccb134dd28449116bf3c7e9bab5cf8a8ec9", + "sha256:58d4cc9b6b768478adfc40a5cbee545303db8dbc81ba688474e0f499cc581028", + "sha256:59a50f5eedf8ed20b7dbd57f1c29b2de003940dea3eedfbf0fbfea05ee7f9f61", + "sha256:5b45153cb8eadcab14139970643a84f7a7b08dda541fbc1f6f4855c49334b549", + "sha256:5da05e3c22c95e23bfc4afeee6ff7d4be9ff2233ad6cb171a0e8257cd46b169a", + "sha256:5de735c745ca5cefe9c2d1547d8f28cfe1b1926aecb7483ab1102fd0a746c093", + "sha256:5e558be423631704803bc6a642e2caa96083df759e25fe6eb01f2d28725f80bd", + "sha256:5ee9560cb1e3094ef01fc071b361121a57ebb8d4232912b6607a6d7d2d0a97b4", + "sha256:6156ad5e8bbe8a78a3f5b5757c9a883b0012325c83f98ce6d58fcec81e8b3d06", + "sha256:61678b7407b04df8f9423f188156355dc94d0fb52d360ae79d02ed7e0d431eea", + "sha256:6b2b74aac3392b8cf508ccb68c980a8555298cd378434a2d065d6ce0f4211dff", + "sha256:734be4f44efba0aa69bf5f015ed13eb69ff29bf0d17ea1e21588b095a3147b8e", + "sha256:795c4884cfe7ea59f2b67d82b417e899afab889d332bfda13b02f8e0c155b2e4", + "sha256:7a5709abe8d23ca158a9d0a18c037f4193f5b6afeb53be37173a41e9fb885792", + "sha256:7db5db88c24cf9253065d69229a148ff60821e5d6f8ff72579b1f80f8f348bab", + "sha256:7f01118133427cd7f34ee133b5098e2af5f70303fa7519785c007bca5aa6f96a", + "sha256:8426c0ebbc11ed8416a6e9409c194142d677c2c5c688595f2743664e356d9e9b", + "sha256:845a35fb21b88786aeb38af8b271d41ab0967985410f35411a27eebdc578a076", + "sha256:849123fd9982c7f63911fdceba9870f203f0f32c953a3bab48e7f27803a0e3ec", + "sha256:85e3c6fb0d25ea046ebcfdc2bcb9683d663dc0280645c79a616ff5077962a15b", + "sha256:862aedec0b0684a5050cdb5ec13c2da96d2f8dffda48657ed35e312a4e31553b", + "sha256:86898f5c9730df23427c1ee0097d8aa41aa5f89539a79e48cd0d2c22d059f1b7", + "sha256:8b32c4636ced87dce0ac3d671e578b3400215efab372f1b4be242e8cf0b11384", + "sha256:8ffe40c216c41756ca05188c3e24a23142334b304f7aebd75c24210385e35573", + "sha256:989066d51686415f1da646d6e2c5364a9b084777c29d9d1720aa5baf192366ef", + "sha256:9cbad4ef12e4c15c94d2c24ecd15a8ed56bf091c62f121a2b0c618ddd4b7402b", + "sha256:9e4dc5c9a6167617251dea0d024d67559795761aabb4b7ea015518be898be076", + "sha256:a00e6390e52770ba1ec753b2610f90b4f00e74c71cfc5405b917adf3cc39565e", + "sha256:a0621ec020c49fc1b6e31304f1a820900d54e7d9afa03ea1634264bf9387519e", + "sha256:a1acf091f53bb406e9e5e7383e467d1dd1b94488b8415b890917d30111a1fef3", + "sha256:a6fc24f00293f10aff04d55ca37029b280474c91f4de2cad5e911e5e10d733b7", + "sha256:aa9c1c208c263b84386ac25bed6af5672397dc3c232638114fc09bca5c7addf9", + "sha256:ad38daf57495beadc0d929e8901b2aa46ff474239b5a8a46ccc7f67dc01d2335", + "sha256:b18045668d09cf0faa44918af2a67f0dbbef738c96f61c2f1b975b1ddb92ccfc", + "sha256:b38e01f11e9e95f6668dc8a62dccf9483f454fed78a77447507a0e8dcbd19a63", + "sha256:b398dd713b18de89730447347e96a0240225e154db56e35b6bb8447ffdb07798", + "sha256:b751914a73604d40d88a061bab042a11d4511b3ddbb7624cd83c39c8a498564c", + "sha256:ba95693f9df8bb4a9826464fb0fe89033936f35fd4a8ff1edff09a473570afa0", + "sha256:bbbb7e2f3ac5a22901324e7b086f398b8e16d343879a77b15ca3312e8cd8e6d5", + "sha256:bccfee44b392f4d13bbf05aa88d8f7709271b940a8c398d4216fde6b717624ae", + "sha256:c4833e02fcf2751975457be1dfa2f744d4d09901a8cc106acaa519d868232175", + "sha256:c4c20ba8389f495c7b4f6b896bb1ca1e109a157d4f189267a902079699aaf787", + "sha256:c5be232f7219414ff672ff7ab8c5a7e8632177735186d8a42b57b491fafdd64e", + "sha256:c5ee06945f3069e3609819890a01958c4bbfea7a2b31ae87107c6478838d309e", + "sha256:ca42a6ce2d697537da34f77a1960d21476c6a4af3e539eddb2b114c3cf65a78c", + "sha256:cb77923ea163156da14295c941930bd525df0d29c96c1ec2fe3c3806b1e17cb3", + "sha256:cc283595b82f0db155a52f6462945c7b6b47ecaae2f681746eeea537c95cf8c9", + "sha256:cea2f26c5972796e02b222968a21a378d09eb4ff590eb3c5fafa8913f8c2bdf5", + "sha256:d00e81cb0afd672915257a3927124ee2ad117ace3c256d39cd97ca3f190152ad", + "sha256:d2b4b261dce10762be5c116b6ad1f267a9429765b493c454f049f33791dd8b8a", + "sha256:d67a0960803a37b60f51b460c58444bc7033a804c662f5735172e21e74ee4902", + "sha256:dd4d3e6a567ffd0d232cfc667c49d0852d0ee7481458a2a1593b9b1bc5acba88", + "sha256:de84e1694f9507b29e7b263453a2255a73e3d099d258db0f14539bad258abe41", + "sha256:dff9198adbb6810ad857f3bfa59b4859c45acb02b0d198b39abeafb9148474f3", + "sha256:e297784aea724294fe95e442e39a4376c2f08aa4fae4161c669f047051e31b02", + "sha256:e3659a79ded9745bc9c2aef5b444ac8805606e7bc50d2d2eb16dc3ab5483d91f", + "sha256:e3c824b70925963bdc8e39a642672c15ffaa67e7d4b491f64662dd56d6271263", + "sha256:e4b860edf6379a7234ccbb19b4ed2c57e3ff569c3414fadfb49ae72b61a8ef07", + "sha256:ea4f498f8115fd90d7bf03a3e83ae3e9898e43362f8e8e8faec93597206e15cc", + "sha256:f0944d65ba2b872b9fcece08411d6347f15a874c775b4c3baae7f278550da0fb", + "sha256:f1151b33aaf3b4fa9da26f4d696e38eebab67d1b43c446184d733c700b3ff8ce", + "sha256:f3dba49ff037d02373a9306b58d6c1e0be031438f822044e8767afccfdac4c6b", + "sha256:f54ca3e98f8f4d23e989c7d0edcf9da7a514ff261edaf64d1d8653dd5feb0a8b", + "sha256:f9528a4b3e24189cb333a9850fddbbafaa81df187297cfbddee50447cdb042cf" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==27.0.1" + "version": "==27.0.2" }, "referencing": { "hashes": [ @@ -1838,12 +1838,12 @@ }, "starlette": { "hashes": [ - "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", - "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b" + "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", + "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.47.2" + "version": "==0.47.3" }, "tinycss2": { "hashes": [ diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index 48e7bb8849..49ce62f1f3 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -12,7 +12,7 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@types/jest": "29.5.14", "@types/node": "20.19.11", "@typescript-eslint/eslint-plugin": "8.40.0", @@ -22,7 +22,7 @@ "@zowe/imperative": "8.26.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.33.0", + "eslint": "9.34.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.2.0", @@ -740,9 +740,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.34.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -7629,9 +7629,9 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.34.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -7641,7 +7641,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 8b63522b59..318473564f 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -49,7 +49,7 @@ "csv-parse": "5.6.0" }, "devDependencies": { - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@types/jest": "29.5.14", "@types/node": "20.19.11", "@typescript-eslint/eslint-plugin": "8.40.0", @@ -59,7 +59,7 @@ "@zowe/imperative": "8.26.2", "copyfiles": "2.4.1", "env-cmd": "10.1.0", - "eslint": "9.33.0", + "eslint": "9.34.0", "eslint-plugin-jest": "28.14.0", "eslint-plugin-license-header": "0.8.0", "eslint-plugin-unused-imports": "4.2.0", From 33dc7c246475ef6668a80021b763db91e5943510 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Mon, 25 Aug 2025 08:33:43 +0000 Subject: [PATCH 081/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.4'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 97502d0154..2646e065cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.4-SNAPSHOT +version=3.3.4 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From a5d656eeda57e238744a3cc23aee57a2bb20eab9 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Mon, 25 Aug 2025 08:33:45 +0000 Subject: [PATCH 082/152] [Gradle Release plugin] Create new version: 'v3.3.5-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2646e065cd..ec956c2e6e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.4 +version=3.3.5-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From c247611373c96d61c05d1eb17d1bc3c33a40904b Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Mon, 25 Aug 2025 08:33:46 +0000 Subject: [PATCH 083/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index aef548e582..a104f8eef1 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.4-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.5-SNAPSHOT From 77eff20a0bb9e31dde57843c4765e15a523baa7d Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:33:12 -0400 Subject: [PATCH 084/152] chore: Update all non-major dependencies (v3.x.x) (#4293) Signed-off-by: Renovate Bot Signed-off-by: ac892247 Co-authored-by: Renovate Bot Co-authored-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .../cypress/e2e/graphql/graphql-apiml.cy.js | 77 +- api-catalog-ui/frontend/package-lock.json | 978 ++++++++++-------- api-catalog-ui/frontend/package.json | 7 +- .../src/components/GraphQL/GraphQLUI.test.jsx | 4 +- .../src/components/GraphQL/GraphQLUIApiml.jsx | 2 +- package-lock.json | 44 +- package.json | 2 +- .../package-lock.json | 120 +-- zowe-cli-id-federation-plugin/package.json | 6 +- 9 files changed, 676 insertions(+), 564 deletions(-) diff --git a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js index 47641e12d6..ffd608b8ca 100644 --- a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js @@ -11,28 +11,6 @@ /// const expectedKeyWords = ['name', 'getAllBooks', 'Effective Java', "Hitchhiker's Guide to the Galaxy", 'Down Under']; -const PATH_TO_SERVICE_DESCRIPTION = - '#root > div > div.content > div.main > div.main-content2.detail-content > div.content-description-container > div > div.tabs-swagger > div.serviceTab > div.header > h6:nth-child(4)'; -const PATH_TO_PLAYGROUND_INPUT_TEXTAREA = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(1) > section > div.graphiql-editor > div > div:nth-child(1) > textarea'; -const PATH_TO_QUERY_OUTPUT = - '#graphiql-session > div:nth-child(3) > div > section > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code'; -const PATH_TO_DEFAULT_QUERY = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(1) > section > div.graphiql-editor > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code > div > pre > span > span'; -const PATH_TO_RUN_QUERY_BUTTON = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(1) > section > div.graphiql-toolbar > button'; -const PATH_TO_REMOVE_SPECIFIC_TAB_BUTTON = - '#graphiql-container > div > div.graphiql-main > div.graphiql-sessions > div.graphiql-session-header > ul > li.graphiql-tab.graphiql-tab-active > button.graphiql-un-styled.graphiql-tab-close'; -const PATH_TO_VARIABLES_INPUT_TEXTAREA = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(3) > section > div:nth-child(1) > div > div:nth-child(1) > textarea'; -const PATH_TO_VARIABLE_DATA = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(3) > section > div:nth-child(1) > div'; -const PATH_TO_HEADER_INPUT_TEXTAREA = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(3) > section > div:nth-child(2) > div > div:nth-child(1) > textarea'; -const PATH_TO_HEADER_DATA = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(3) > section > div:nth-child(2) > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code'; -const PATH_TO_PLAYGROUND_INPUT_DATA = - '#graphiql-session > div:nth-child(1) > div > div:nth-child(1) > section > div.graphiql-editor > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code'; function login() { cy.visit(`${Cypress.env('catalogHomePage')}/index.html#/login`); @@ -80,9 +58,8 @@ describe('>>> GraphiQL Playground page test', () => { cy.get('#version-div').should('not.exist'); - cy.get(PATH_TO_SERVICE_DESCRIPTION) - .should('exist') - .should('contain', 'Sample for data demonstration using GraphiQL Playround.'); + cy.contains('Sample for data demonstration using GraphiQL Playround.'); + cy.get('#swagger-label').should('contain', 'GraphQL'); cy.get(`a[href="#/service/graphqlclient"]`).should('have.class', 'Mui-selected'); @@ -95,45 +72,33 @@ describe('>>> GraphiQL Playground page test', () => { cy.get('#graphiql-container').should('exist'); - cy.get(PATH_TO_DEFAULT_QUERY).should('exist').should('be.visible').and('have.text', '# Write your query here!'); - const query = '{ "query": "{ hello }" }'; - cy.get(PATH_TO_PLAYGROUND_INPUT_TEXTAREA) - .first() - .focus() - .type('{ctrl}a') - .type('{del}') - .type(query, { parseSpecialCharSequences: false }); - cy.get(PATH_TO_PLAYGROUND_INPUT_DATA).then(($container) => { - const text = $container.text().trim(); - expect(text).to.include('{ "query": "{ hello }" }'); - }); + cy.contains('# Write your query here!') // clear out the placeholder/default text + .type(query, {parseSpecialCharSequences: false}); + }); it('Should run query', () => { login(); cy.contains('Discoverable client with GraphQL').click(); - - const query = 'query {' + 'getAllBooks{' + 'name' + ' }'; - cy.get(PATH_TO_PLAYGROUND_INPUT_TEXTAREA) - .first() - .focus() + cy.contains('# Write your query here!').click() .type('{ctrl}a') .type('{del}') - .type(query, { parseSpecialCharSequences: false }); - - cy.get(PATH_TO_RUN_QUERY_BUTTON).click(); - + const query = 'query {' + 'getAllBooks{' + 'name' + ' }'; + cy.get('.graphiql-editor').first() + .type(query, {parseSpecialCharSequences: false}) + cy.get('.graphiql-execute-button').click(); cy.get('span.cm-def').should('contain.text', 'data'); - cy.get(PATH_TO_QUERY_OUTPUT).then(($container) => { + cy.get('.result-window').then(($container) => { const text = $container.text().trim(); expectedKeyWords.forEach((word) => { expect(text).to.include(word); }); }); + }); it('Should add and remove a tab in the playground', () => { @@ -141,11 +106,11 @@ describe('>>> GraphiQL Playground page test', () => { cy.contains('Discoverable client with GraphQL').click(); - cy.get('button[aria-label="Add tab"]').click(); + cy.get('.graphiql-un-styled.graphiql-tab-add').click(); cy.get('#graphiql-session-tab-1').should('exist').should('contain.text', 'My Query 2'); - cy.get(PATH_TO_REMOVE_SPECIFIC_TAB_BUTTON).click(); + cy.get('.graphiql-un-styled.graphiql-tab-close').last().click(); cy.get('#graphiql-session-tab-1').should('not.exist'); }); @@ -208,15 +173,15 @@ describe('>>> GraphiQL Playground page test', () => { const variable = '{"id" :"book-1"}'; - cy.get(PATH_TO_VARIABLES_INPUT_TEXTAREA).first().focus().type(variable, { parseSpecialCharSequences: false }); + cy.get('.graphiql-editor').first() + .type(variable, {parseSpecialCharSequences: false}); - cy.get(PATH_TO_VARIABLE_DATA).then(($container) => { + cy.get('.graphiql-editor').then(($container) => { const text = $container.text().trim(); expect(text).to.include(variable); }); }); - - it('Header usage', () => { + it('Variable usage', () => { login(); cy.contains('Discoverable client with GraphQL').click(); @@ -226,11 +191,13 @@ describe('>>> GraphiQL Playground page test', () => { const header = '{"X-Custom-Header": "CustomValue"}'; - cy.get(PATH_TO_HEADER_INPUT_TEXTAREA).first().focus().type(header, { parseSpecialCharSequences: false }); + cy.get('.graphiql-editor').first() + .type(header, {parseSpecialCharSequences: false}); - cy.get(PATH_TO_HEADER_DATA).then(($container) => { + cy.get('.graphiql-editor').then(($container) => { const text = $container.text().trim(); expect(text).to.include(header); }); }); + }); diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index e96f1f9fd1..cce3c37b26 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -26,7 +26,7 @@ "buffer": "6.0.3", "emotion-theming": "11.0.0", "exception-formatter": "2.1.2", - "graphiql": "3.9.0", + "graphiql": "4.1.2", "graphql": "16.11.0", "graphql-ws": "5.16.2", "history": "5.3.0", @@ -53,7 +53,7 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.90.0", + "sass": "1.91.0", "stream": "0.0.3", "swagger-ui-react": "5.27.1", "url": "0.11.4", @@ -78,7 +78,7 @@ "ansi-regex": "6.2.0", "body-parser": "1.20.3", "caniuse-lite": "1.0.30001737", - "concurrently": "9.2.0", + "concurrently": "9.2.1", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", @@ -3113,31 +3113,46 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "version": "1.7.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "version": "1.7.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "version": "2.1.6", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -3145,45 +3160,78 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@graphiql/plugin-doc-explorer": { + "version": "0.2.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@graphiql/plugin-doc-explorer/-/plugin-doc-explorer-0.2.2.tgz", + "integrity": "sha512-0Pj0vsNFfZJZJ3moC7O9xF1Dt2KWyvdxke+cFFZQLN1L2VxdTUa4cfGSg5ujv/ikDUUyyPQNTZuyXzagM4NG5g==", + "license": "MIT", + "dependencies": { + "@graphiql/react": "^0.34.1", + "@headlessui/react": "^2.2", + "react-compiler-runtime": "19.1.0-rc.1", + "zustand": "^5" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/@graphiql/plugin-history": { + "version": "0.2.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@graphiql/plugin-history/-/plugin-history-0.2.2.tgz", + "integrity": "sha512-ta1k8ichGVfMg6eDwRYa/2e92PfLPh+wgtJTuTQagmXBLXhM7g8Hbk+CaE2clUahQ8/4CZOIRXJHbQ7B5tI9dg==", + "license": "MIT", + "dependencies": { + "@graphiql/react": "^0.34.1", + "@graphiql/toolkit": "^0.11.3", + "react-compiler-runtime": "19.1.0-rc.1", + "zustand": "^5" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/@graphiql/react": { - "version": "0.29.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@graphiql/react/-/react-0.29.0.tgz", - "integrity": "sha512-4Zd57+DeK5t1KYiqy9hnuaQhR8fnZ1CkKUkmZqINpAe0OlpbszOE661zwNGuW5NjbXgepfI89ICnh1ZVinSJvg==", + "version": "0.34.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@graphiql/react/-/react-0.34.1.tgz", + "integrity": "sha512-Ykqt5uzIRKcbriyVisr/y6BwxK9ZIsilaJFPbajDAcoDiXlGEx/Pl86OcJNjslsqn79M9BkrphWvWa7cBGz3Sw==", "license": "MIT", "dependencies": { - "@graphiql/toolkit": "^0.11.2", - "@headlessui/react": "^1.7.15", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-dropdown-menu": "^2.0.5", - "@radix-ui/react-tooltip": "^1.0.6", - "@radix-ui/react-visually-hidden": "^1.0.3", + "@graphiql/toolkit": "^0.11.3", + "@radix-ui/react-dialog": "^1.1", + "@radix-ui/react-dropdown-menu": "^2.1", + "@radix-ui/react-tooltip": "^1.2", + "@radix-ui/react-visually-hidden": "^1.2", "@types/codemirror": "^5.60.8", "clsx": "^1.2.1", "codemirror": "^5.65.3", "codemirror-graphql": "^2.2.1", "copy-to-clipboard": "^3.2.0", - "framer-motion": "^6.5.1", + "framer-motion": "^12", "get-value": "^3.0.1", "graphql-language-service": "^5.3.1", "markdown-it": "^14.1.0", "react-compiler-runtime": "19.1.0-rc.1", - "set-value": "^4.1.0" + "set-value": "^4.1.0", + "zustand": "^5" }, "peerDependencies": { "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "react": "^18 || ^19", + "react-dom": "^18 || ^19" } }, "node_modules/@graphiql/toolkit": { - "version": "0.11.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@graphiql/toolkit/-/toolkit-0.11.2.tgz", - "integrity": "sha512-C6O3f7KsLaoGjZHRT5jLjUmZeGUUewJtASbiAjPsJWeuONvQjLIxHnM+wjXYIVRqAVUVDtb4iadcZK0O9fzh0w==", + "version": "0.11.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@graphiql/toolkit/-/toolkit-0.11.3.tgz", + "integrity": "sha512-Glf0fK1cdHLNq52UWPzfSrYIJuNxy8h4451Pw1ZVpJ7dtU+tm7GVVC64UjEDQ/v2j3fnG4cX8jvR75IvfL6nzQ==", "license": "MIT", "dependencies": { "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", @@ -3217,20 +3265,23 @@ } }, "node_modules/@headlessui/react": { - "version": "1.7.19", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@headlessui/react/-/react-1.7.19.tgz", - "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "version": "2.2.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@headlessui/react/-/react-2.2.7.tgz", + "integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==", "license": "MIT", "dependencies": { - "@tanstack/react-virtual": "^3.0.0-beta.60", - "client-only": "^0.0.1" + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/@humanfs/core": { @@ -4111,70 +4162,6 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, - "node_modules/@motionone/animation": { - "version": "10.18.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@motionone/animation/-/animation-10.18.0.tgz", - "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", - "license": "MIT", - "dependencies": { - "@motionone/easing": "^10.18.0", - "@motionone/types": "^10.17.1", - "@motionone/utils": "^10.18.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/dom": { - "version": "10.12.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@motionone/dom/-/dom-10.12.0.tgz", - "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", - "license": "MIT", - "dependencies": { - "@motionone/animation": "^10.12.0", - "@motionone/generators": "^10.12.0", - "@motionone/types": "^10.12.0", - "@motionone/utils": "^10.12.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/easing": { - "version": "10.18.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@motionone/easing/-/easing-10.18.0.tgz", - "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", - "license": "MIT", - "dependencies": { - "@motionone/utils": "^10.18.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/generators": { - "version": "10.18.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@motionone/generators/-/generators-10.18.0.tgz", - "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", - "license": "MIT", - "dependencies": { - "@motionone/types": "^10.17.1", - "@motionone/utils": "^10.18.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/types": { - "version": "10.17.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@motionone/types/-/types-10.17.1.tgz", - "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==", - "license": "MIT" - }, - "node_modules/@motionone/utils": { - "version": "10.18.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@motionone/utils/-/utils-10.18.0.tgz", - "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", - "license": "MIT", - "dependencies": { - "@motionone/types": "^10.17.1", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, "node_modules/@mui/core-downloads-tracker": { "version": "5.17.1", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz", @@ -4910,18 +4897,18 @@ } }, "node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "version": "1.1.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", - "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "version": "1.1.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -4939,15 +4926,15 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", - "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "version": "1.1.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4965,9 +4952,9 @@ } }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "version": "1.1.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4980,9 +4967,9 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "version": "1.1.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4995,23 +4982,23 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.6", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", - "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", + "version": "1.1.15", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -5031,9 +5018,9 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5046,16 +5033,16 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "version": "1.1.11", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5073,18 +5060,18 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.6", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", - "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "version": "2.1.16", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-menu": "2.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -5102,9 +5089,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "version": "1.1.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5117,14 +5104,14 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "version": "1.1.7", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5142,12 +5129,12 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5160,27 +5147,27 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.6", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", - "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-callback-ref": "1.1.0", + "version": "2.1.16", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -5200,21 +5187,21 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", - "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "version": "1.2.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5232,13 +5219,13 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "version": "1.1.9", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5256,13 +5243,13 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", - "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "version": "1.1.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5280,12 +5267,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "version": "2.1.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -5303,20 +5290,20 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", - "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "version": "1.1.11", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -5334,12 +5321,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "version": "1.2.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -5352,23 +5339,23 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", - "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "version": "1.2.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -5386,9 +5373,9 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5401,12 +5388,31 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "version": "1.2.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5419,12 +5425,12 @@ } }, "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5437,9 +5443,9 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5452,12 +5458,12 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.0" + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5470,12 +5476,12 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5488,12 +5494,12 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", - "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "version": "1.2.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -5511,11 +5517,96 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "version": "1.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@react-aria/focus": { + "version": "3.21.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-aria/focus/-/focus-3.21.1.tgz", + "integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.5", + "@react-aria/utils": "^3.30.1", + "@react-types/shared": "^3.32.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/focus/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.5", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-aria/interactions/-/interactions-3.25.5.tgz", + "integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.30.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.30.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-aria/utils/-/utils-3.30.1.tgz", + "integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.32.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@react-loadable/revised": { "version": "1.5.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-loadable/revised/-/revised-1.5.0.tgz", @@ -5526,6 +5617,36 @@ "webpack": "^4.46.0 || ^5.28.0" } }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.32.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@react-types/shared/-/shared-3.32.0.tgz", + "integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.8.2", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", @@ -6551,13 +6672,22 @@ "node": ">=12.20.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.6", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", - "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==", + "version": "3.13.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.6" + "@tanstack/virtual-core": "3.13.12" }, "funding": { "type": "github", @@ -6569,9 +6699,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.6", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz", - "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==", + "version": "3.13.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", "license": "MIT", "funding": { "type": "github", @@ -6800,9 +6930,9 @@ } }, "node_modules/@types/codemirror": { - "version": "5.60.15", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/codemirror/-/codemirror-5.60.15.tgz", - "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "version": "5.60.16", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/codemirror/-/codemirror-5.60.16.tgz", + "integrity": "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==", "license": "MIT", "dependencies": { "@types/tern": "*" @@ -8029,9 +8159,9 @@ "license": "Python-2.0" }, "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "version": "1.2.6", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -9778,12 +9908,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/cliui/-/cliui-8.0.1.tgz", @@ -9913,19 +10037,19 @@ } }, "node_modules/codemirror": { - "version": "5.65.19", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/codemirror/-/codemirror-5.65.19.tgz", - "integrity": "sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==", + "version": "5.65.20", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/codemirror/-/codemirror-5.65.20.tgz", + "integrity": "sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA==", "license": "MIT" }, "node_modules/codemirror-graphql": { - "version": "2.2.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/codemirror-graphql/-/codemirror-graphql-2.2.1.tgz", - "integrity": "sha512-//E1IjSVfScGBMpLgylfbMQNQnejpeqyP+Weqh/Kl45kr0XkSUiOCImsG5OL5Crm6waFbx4H7GAAI6KFXy27IQ==", + "version": "2.2.4", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/codemirror-graphql/-/codemirror-graphql-2.2.4.tgz", + "integrity": "sha512-VW4rpbAqgAIEWKXwE3nDFz2QEaY5Cme0CnKET43ZE3RSKAAeOaUcecR6wBf3LYfxWwOyyzeCzqxRoUj+HNAEQA==", "license": "MIT", "dependencies": { "@types/codemirror": "^0.0.90", - "graphql-language-service": "5.3.1" + "graphql-language-service": "5.5.0" }, "peerDependencies": { "@codemirror/language": "6.0.0", @@ -10131,19 +10255,18 @@ "license": "MIT" }, "node_modules/concurrently": { - "version": "9.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.2.0.tgz", - "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "version": "9.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", @@ -14007,50 +14130,30 @@ } }, "node_modules/framer-motion": { - "version": "6.5.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "version": "12.23.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", "license": "MIT", "dependencies": { - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" + "motion-dom": "^12.23.12", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" }, "peerDependencies": { - "react": ">=16.8 || ^17.0.0 || ^18.0.0", - "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/framer-motion/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "license": "MIT", - "optional": true - }, - "node_modules/framesync": { - "version": "6.0.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/framesync/-/framesync-6.0.1.tgz", - "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, "node_modules/fresh": { @@ -14502,18 +14605,20 @@ "license": "MIT" }, "node_modules/graphiql": { - "version": "3.9.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/graphiql/-/graphiql-3.9.0.tgz", - "integrity": "sha512-rJYFlHdBbug9+YbFSwRVvgfPjCayC7wHecZkx/qB4XLuQPFQZw/yB9pmHFtOJV6RiNBBHro3u1GdocKa3YcGRA==", + "version": "4.1.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/graphiql/-/graphiql-4.1.2.tgz", + "integrity": "sha512-rNlzQIAzm6i2KBXmVQWBgLvt0TloxFVB2d18bukvdsvjEuFDHsKx5IVOomL1W1o5WW+tE54+ioHb2AEKzO2cUg==", "license": "MIT", "dependencies": { - "@graphiql/react": "^0.29.0", + "@graphiql/plugin-doc-explorer": "^0.2.2", + "@graphiql/plugin-history": "^0.2.2", + "@graphiql/react": "^0.34.1", "react-compiler-runtime": "19.1.0-rc.1" }, "peerDependencies": { "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "react": "^18 || ^19", + "react-dom": "^18 || ^19" } }, "node_modules/graphql": { @@ -14526,9 +14631,9 @@ } }, "node_modules/graphql-language-service": { - "version": "5.3.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/graphql-language-service/-/graphql-language-service-5.3.1.tgz", - "integrity": "sha512-6u/f6rxQ6sgNfHwHYGsFiR+zYP93uFEStNO0j9WexoOVd2hQK7BPsNyBz/WsB8URHPAjoVhSKs1wkAqUrcxj1g==", + "version": "5.5.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/graphql-language-service/-/graphql-language-service-5.5.0.tgz", + "integrity": "sha512-9EvWrLLkF6Y5e29/2cmFoAO6hBPPAZlCyjznmpR11iFtRydfkss+9m6x+htA8h7YznGam+TtJwS6JuwoWWgb2Q==", "license": "MIT", "dependencies": { "debounce-promise": "^3.1.2", @@ -14867,12 +14972,6 @@ "he": "bin/he" } }, - "node_modules/hey-listen": { - "version": "1.0.8", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", - "license": "MIT" - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -16508,7 +16607,6 @@ "version": "2.0.5", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -20225,9 +20323,9 @@ } }, "node_modules/meros": { - "version": "1.3.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/meros/-/meros-1.3.0.tgz", - "integrity": "sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==", + "version": "1.3.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/meros/-/meros-1.3.1.tgz", + "integrity": "sha512-eV7dRObfTrckdmAz4/n7pT1njIsIJXRIZkgCiX43xEsPNy4gjXQzOYYxmGcolAMtF7HyfqRuDBh3Lgs4hmhVEw==", "license": "MIT", "engines": { "node": ">=13" @@ -20437,6 +20535,21 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/motion-dom": { + "version": "12.23.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/ms/-/ms-2.1.3.tgz", @@ -21817,18 +21930,6 @@ "node": ">=4" } }, - "node_modules/popmotion": { - "version": "11.0.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/popmotion/-/popmotion-11.0.3.tgz", - "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", - "license": "MIT", - "dependencies": { - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - } - }, "node_modules/popper.js": { "version": "1.16.1-lts", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/popper.js/-/popper.js-1.16.1-lts.tgz", @@ -23997,9 +24098,9 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "version": "2.7.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -27431,9 +27532,9 @@ "license": "CC0-1.0" }, "node_modules/sass": { - "version": "1.90.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.90.0.tgz", - "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", + "version": "1.91.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.91.0.tgz", + "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -27876,16 +27977,23 @@ "license": "ISC" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -27912,9 +28020,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.8.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { @@ -29212,16 +29320,6 @@ "webpack": "^5.0.0" } }, - "node_modules/style-value-types": { - "version": "5.0.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/style-value-types/-/style-value-types-5.0.0.tgz", - "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", - "license": "MIT", - "dependencies": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" - } - }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -29744,6 +29842,12 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -30140,6 +30244,20 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "license": "BSD-3-Clause" }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -30482,7 +30600,6 @@ "version": "1.0.3", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -32175,6 +32292,35 @@ "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/zenscroll/-/zenscroll-4.0.2.tgz", "integrity": "sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==", "license": "Unlicense" + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index e1bdfa2f2f..8dd0cdad96 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -22,7 +22,7 @@ "buffer": "6.0.3", "emotion-theming": "11.0.0", "exception-formatter": "2.1.2", - "graphiql": "3.9.0", + "graphiql": "4.1.2", "graphql": "16.11.0", "graphql-ws": "5.16.2", "history": "5.3.0", @@ -49,7 +49,7 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.90.0", + "sass": "1.91.0", "stream": "0.0.3", "swagger-ui-react": "5.27.1", "url": "0.11.4", @@ -99,7 +99,7 @@ "ansi-regex": "6.2.0", "body-parser": "1.20.3", "caniuse-lite": "1.0.30001737", - "concurrently": "9.2.0", + "concurrently": "9.2.1", "cors": "2.8.5", "cross-env": "7.0.3", "cypress": "13.17.0", @@ -157,6 +157,7 @@ "@babel/traverse": "7.28.3", "axios": "1.11.0", "form-data": "3.0.4", + "sha.js": "2.4.12", "swagger-ui-react": { "react-syntax-highlighter": { "refractor": "5.0.0" diff --git a/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUI.test.jsx b/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUI.test.jsx index b62c7428e1..97496e2430 100644 --- a/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUI.test.jsx +++ b/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUI.test.jsx @@ -87,7 +87,7 @@ describe('>>> GraphQL component tests', () => { it('should render the GraphiQL container', async () => { await act(async () => render()); - expect(screen.getByTestId('graphiql-container')).toBeInTheDocument(); + expect(document.getElementById('graphiql-container')).toBeInTheDocument(); }, 10000); it('getUrl constructs the correct URL', async () => { @@ -125,7 +125,7 @@ describe('>>> GraphQL component tests', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); await act(async () => render()); expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching data:', expect.any(Error)); - expect(screen.getByTestId('graphiql-container')).toBeInTheDocument(); + expect(document.getElementById('graphiql-container')).toBeInTheDocument(); consoleErrorSpy.mockRestore(); }); diff --git a/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUIApiml.jsx b/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUIApiml.jsx index e126270717..a58bb2bd5a 100644 --- a/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUIApiml.jsx +++ b/api-catalog-ui/frontend/src/components/GraphQL/GraphQLUIApiml.jsx @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ import React, { useEffect, useRef, useState } from 'react'; -import GraphiQL from 'graphiql'; +import { GraphiQL } from 'graphiql'; import 'graphiql/graphiql.css'; import './GraphQLUIApiml.css'; import { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities'; diff --git a/package-lock.json b/package-lock.json index 43e2065164..267a61f1ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.0.0", "license": "EPL-2.0", "devDependencies": { - "concurrently": "9.2.0" + "concurrently": "9.2.1" } }, "node_modules/ansi-regex": { @@ -97,19 +97,18 @@ "dev": true }, "node_modules/concurrently": { - "version": "9.2.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.2.0.tgz", - "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "version": "9.2.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", @@ -164,12 +163,6 @@ "node": ">=8" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://zowe.jfrog.io/zowe/api/npm/npm-release/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw=", - "dev": true - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/require-directory/-/require-directory-2.1.1.tgz", @@ -180,19 +173,24 @@ } }, "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.3", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } diff --git a/package.json b/package.json index 592c2bc6ec..c84af09993 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,6 @@ }, "homepage": "https://github.com/zowe/api-layer#readme", "devDependencies": { - "concurrently": "9.2.0" + "concurrently": "9.2.1" } } diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index 49ce62f1f3..123f19cc73 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -15,8 +15,8 @@ "@eslint/js": "9.34.0", "@types/jest": "29.5.14", "@types/node": "20.19.11", - "@typescript-eslint/eslint-plugin": "8.40.0", - "@typescript-eslint/parser": "8.40.0", + "@typescript-eslint/eslint-plugin": "8.41.0", + "@typescript-eslint/parser": "8.41.0", "@zowe/cli": "8.26.2", "@zowe/cli-test-utils": "8.26.2", "@zowe/imperative": "8.26.2", @@ -38,7 +38,7 @@ "madge": "8.0.0", "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.10", + "typedoc": "0.28.11", "typescript": "5.9.2" }, "engines": { @@ -2105,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", - "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", + "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/type-utils": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/type-utils": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2129,7 +2129,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2158,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.40.0.tgz", - "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.41.0.tgz", + "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4" }, "engines": { @@ -2183,14 +2183,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", + "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", + "@typescript-eslint/tsconfig-utils": "^8.41.0", + "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "engines": { @@ -2205,14 +2205,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", - "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", + "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0" + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,9 +2223,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", + "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", "dev": true, "license": "MIT", "engines": { @@ -2240,15 +2240,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", - "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", + "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2278,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.41.0.tgz", + "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", "dev": true, "license": "MIT", "engines": { @@ -2292,16 +2292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", + "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/project-service": "8.41.0", + "@typescript-eslint/tsconfig-utils": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2360,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.40.0.tgz", - "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0" + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2384,13 +2384,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "version": "8.41.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", + "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -12546,9 +12546,9 @@ } }, "node_modules/typedoc": { - "version": "0.28.10", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.10.tgz", - "integrity": "sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==", + "version": "0.28.11", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.11.tgz", + "integrity": "sha512-1FqgrrUYGNuE3kImAiEDgAVVVacxdO4ZVTKbiOVDGkoeSB4sNwQaDpa8mta+Lw5TEzBFmGXzsg0I1NLRIoaSFw==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 318473564f..811d3e00d0 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -52,8 +52,8 @@ "@eslint/js": "9.34.0", "@types/jest": "29.5.14", "@types/node": "20.19.11", - "@typescript-eslint/eslint-plugin": "8.40.0", - "@typescript-eslint/parser": "8.40.0", + "@typescript-eslint/eslint-plugin": "8.41.0", + "@typescript-eslint/parser": "8.41.0", "@zowe/cli": "8.26.2", "@zowe/cli-test-utils": "8.26.2", "@zowe/imperative": "8.26.2", @@ -75,7 +75,7 @@ "madge": "8.0.0", "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.10", + "typedoc": "0.28.11", "typescript": "5.9.2" }, "overrides": { From ca6de110f76b3b7d741ed6933e8958f15b5efc85 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Tue, 26 Aug 2025 15:54:58 +0000 Subject: [PATCH 085/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.5'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ec956c2e6e..98a4fdb1d6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.5-SNAPSHOT +version=3.3.5 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 7bc622b5d3325eed5285bdf86c5f7ce6244680bf Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Tue, 26 Aug 2025 15:55:00 +0000 Subject: [PATCH 086/152] [Gradle Release plugin] Create new version: 'v3.3.6-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 98a4fdb1d6..6b814edbbe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.5 +version=3.3.6-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 2261c1478bf8bc9585845f911973a3dc174b8ad8 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Tue, 26 Aug 2025 15:55:01 +0000 Subject: [PATCH 087/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index a104f8eef1..ccb5bd1f73 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.5-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.6-SNAPSHOT From 6ddb3b18cc327117b19218a4b48f793d5987363d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Thu, 28 Aug 2025 16:26:01 +0200 Subject: [PATCH 088/152] fix: Increase stomp tests connection timeout for miniplex (#4296) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- .../java/org/zowe/apiml/integration/proxy/StompProxyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java index 2297ca2563..3574d2188b 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/StompProxyTest.java @@ -60,7 +60,7 @@ void stompOverWebsocketLargeMessageExchange() throws Exception { StompSession stompSession = stompClient.connectAsync( discoverableClientGatewayUrl(DISCOVERABLE_STOMP), VALID_AUTH_HEADERS, new StompSessionHandlerAdapter() { - }).get(1, SECONDS); + }).get(5, SECONDS); // lower connection timeout fails on z/os test system stompSession.subscribe(SUBSCRIBE_ENDPOINT + uuid, new StringStompFrameHandler()); char c = 'A'; From 665e0465a9f70c8884b90c76bd0357dd4cf46040 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 29 Aug 2025 00:43:57 +0000 Subject: [PATCH 089/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.6'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6b814edbbe..9bc750b107 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.6-SNAPSHOT +version=3.3.6 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 591779baf8b7f98e91a078879b501200bf96d833 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 29 Aug 2025 00:43:59 +0000 Subject: [PATCH 090/152] [Gradle Release plugin] Create new version: 'v3.3.7-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9bc750b107..decf7d9c88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.6 +version=3.3.7-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From d23de085b3e768a917f85050912868a5d54a5405 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 29 Aug 2025 00:44:00 +0000 Subject: [PATCH 091/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index ccb5bd1f73..5f0546501a 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.6-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.7-SNAPSHOT From f35168039861984e6b42e8f0d487150f80794247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Fri, 29 Aug 2025 10:11:39 +0200 Subject: [PATCH 092/152] chore: Exclude discoverable-client from sonar analysis (#4298) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- gradle/sonar.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gradle/sonar.gradle b/gradle/sonar.gradle index 4bd594e1eb..cbaf3deb68 100644 --- a/gradle/sonar.gradle +++ b/gradle/sonar.gradle @@ -53,6 +53,12 @@ project(":zowe-cli-id-federation-plugin") { } } +project(":discoverable-client") { + sonar { + skipProject = true + } +} + project(":onboarding-enabler-nodejs") { sonar { skipProject = true From 750889486eeacf223d4f890bf010f1a7409ed7ba Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:08:41 +0200 Subject: [PATCH 093/152] feat: multiple OIDC providers at the same time (#4295) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/cypress.config.js | 3 +- .../cypress/e2e/graphql/graphql-apiml.cy.js | 13 +- .../ReactivePublicJWKController.java | 6 +- .../ReactivePublicJWKControllerTest.java | 4 +- .../zaas/controllers/AuthController.java | 6 +- ...roviderJWK.java => OIDCTokenProvider.java} | 107 +++---- .../token/OIDCTokenProviderEndpoint.java | 56 ---- ...ava => OIDCTokenProviderEndpointTest.java} | 8 +- .../zaas/controllers/AuthControllerTest.java | 4 +- .../token/OIDCTokenProviderJWKTest.java | 232 ---------------- .../service/token/OIDCTokenProviderTest.java | 262 ++++++++++++++++++ 11 files changed, 345 insertions(+), 356 deletions(-) rename zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/{OIDCTokenProviderJWK.java => OIDCTokenProvider.java} (60%) delete mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java rename zaas-service/src/test/java/org/zowe/apiml/acceptance/{OIDCTokenProviderJWKEndpointTest.java => OIDCTokenProviderEndpointTest.java} (98%) delete mode 100644 zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderJWKTest.java create mode 100644 zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java diff --git a/api-catalog-ui/frontend/cypress.config.js b/api-catalog-ui/frontend/cypress.config.js index 37b372ffc6..348cd00c2b 100644 --- a/api-catalog-ui/frontend/cypress.config.js +++ b/api-catalog-ui/frontend/cypress.config.js @@ -18,7 +18,8 @@ module.exports = defineConfig({ 'https://localhost:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Flocalhost%3A10010%2Fapplication', username: 'USER', password: 'validPassword', - }, + microservices: true, +}, viewportWidth: 1400, viewportHeight: 980, chromeWebSecurity: false, diff --git a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js index ffd608b8ca..9fe35ef1f6 100644 --- a/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js @@ -163,6 +163,10 @@ describe('>>> GraphiQL Playground page test', () => { cy.get('.graphiql-dialog-header h2').should('be.visible').should('contain', 'Settings'); }); + // Skip flaky tests in the microservice setup + if (Cypress.env('microservices')) { + return; + } it('Variable usage', () => { login(); cy.contains('Discoverable client with GraphQL').click(); @@ -173,14 +177,15 @@ describe('>>> GraphiQL Playground page test', () => { const variable = '{"id" :"book-1"}'; - cy.get('.graphiql-editor').first() + cy.get('.graphiql-editor-tool').first() .type(variable, {parseSpecialCharSequences: false}); - cy.get('.graphiql-editor').then(($container) => { + cy.get('.graphiql-editor-tool').then(($container) => { const text = $container.text().trim(); expect(text).to.include(variable); }); }); + it('Variable usage', () => { login(); cy.contains('Discoverable client with GraphQL').click(); @@ -191,10 +196,10 @@ describe('>>> GraphiQL Playground page test', () => { const header = '{"X-Custom-Header": "CustomValue"}'; - cy.get('.graphiql-editor').first() + cy.get('.graphiql-editor-tool').first() .type(header, {parseSpecialCharSequences: false}); - cy.get('.graphiql-editor').then(($container) => { + cy.get('.graphiql-editor-tool').then(($container) => { const text = $container.text().trim(); expect(text).to.include(header); }); diff --git a/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java b/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java index b81d67e972..73a0339235 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java @@ -33,7 +33,7 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.zaas.security.service.JwtSecurity; -import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK; +import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import reactor.core.publisher.Mono; @@ -91,8 +91,8 @@ public Mono> getAllPublicKeys() { } Optional key = jwtSecurity.getJwkPublicKey(); key.ifPresent(keys::add); - if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProviderJWK oidcTokenProviderJwk)) { - JWKSet oidcSet = oidcTokenProviderJwk.getJwkSet(); + if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProvider oidcTokenProvider)) { + JWKSet oidcSet = oidcTokenProvider.getJwkSet(); if (oidcSet != null) { keys.addAll(oidcSet.getKeys()); } diff --git a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java index 8333a406d2..bac2233d84 100644 --- a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java @@ -27,7 +27,7 @@ import org.zowe.apiml.message.core.MessageType; import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.zaas.security.service.JwtSecurity; -import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK; +import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -69,7 +69,7 @@ void getAllPublicKeys_zosmfProducer_withOidc() throws Exception { JWK oidcJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("oidcKey").build(); JWKSet oidcKeySet = new JWKSet(oidcJwk); - OIDCTokenProviderJWK mockOidcProviderJwk = mock(OIDCTokenProviderJWK.class); + OIDCTokenProvider mockOidcProviderJwk = mock(OIDCTokenProvider.class); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); when(zosmfService.getPublicKeys()).thenReturn(zosmfKeySet); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java index 332497d3b0..6a498631af 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java @@ -45,7 +45,7 @@ import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.JwtSecurity; -import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK; +import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import org.zowe.apiml.zaas.security.webfinger.WebFingerProvider; import org.zowe.apiml.zaas.security.webfinger.WebFingerResponse; @@ -354,8 +354,8 @@ public Map getAllPublicKeys() { } Optional key = jwtSecurity.getJwkPublicKey(); key.ifPresent(keys::add); - if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProviderJWK oidcTokenProviderJwk)) { - JWKSet oidcSet = oidcTokenProviderJwk.getJwkSet(); + if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProvider oidcTokenProvider)) { + JWKSet oidcSet = oidcTokenProvider.getJwkSet(); if (oidcSet != null) { keys.addAll(oidcSet.getKeys()); } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderJWK.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java similarity index 60% rename from zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderJWK.java rename to zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java index 6b4dbb66db..f4c8aa6cfd 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderJWK.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java @@ -14,59 +14,50 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.KeyType; -import com.nimbusds.jose.jwk.KeyUse; import com.nimbusds.jose.util.DefaultResourceRetriever; import com.nimbusds.jose.util.Resource; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Clock; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.LocatorAdapter; -import io.jsonwebtoken.ProtectedHeader; +import io.jsonwebtoken.*; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.UnsupportedKeyException; import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.http.HttpHeaders; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.http.HttpStatus; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.security.common.token.OIDCProvider; import java.io.IOException; import java.net.URL; import java.security.Key; -import java.security.PublicKey; import java.text.ParseException; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; @RequiredArgsConstructor @Service @Slf4j -@ConditionalOnExpression("'${apiml.security.oidc.validationType:JWK}' == 'JWK' && '${apiml.security.oidc.enabled:false}' == 'true'") -public class OIDCTokenProviderJWK implements OIDCProvider { +@ConditionalOnExpression("'${apiml.security.oidc.enabled:false}' == 'true'") +public class OIDCTokenProvider implements OIDCProvider { private final LocatorAdapterKid keyLocator = new LocatorAdapterKid(); - @InjectApimlLogger - protected final ApimlLogger logger = ApimlLogger.empty(); - - @Value("${apiml.security.oidc.registry:}") - String registry; @Value("${apiml.security.oidc.jwks.uri}") - private String jwksUri; + private List jwksUri; @Value("${apiml.security.oidc.jwks.refreshInternalHours:1}") private int jwkRefreshInterval; @@ -75,11 +66,16 @@ public class OIDCTokenProviderJWK implements OIDCProvider { private final Clock clock; private final DefaultResourceRetriever resourceRetriever; + @Value("${apiml.security.oidc.userInfo.uri}") + private String endpointUrl; + + private final CloseableHttpClient secureHttpClientWithKeystore; @Getter - private final Map publicKeys = new ConcurrentHashMap<>(); + private final Map publicKeys = new ConcurrentHashMap<>(); @Getter private JWKSet jwkSet; + @PostConstruct public void afterPropertiesSet() { this.fetchJWKSet(); @@ -89,47 +85,60 @@ public void afterPropertiesSet() { @Retryable void fetchJWKSet() { - if (StringUtils.isBlank(jwksUri)) { + if (Collections.isEmpty(jwksUri)) { log.debug("OIDC JWK URI not provided, JWK refresh not performed"); return; } log.debug("Refreshing JWK endpoints {}", jwksUri); - try { - publicKeys.clear(); - jwkSet = null; - Resource resource = resourceRetriever.retrieveResource(new URL(jwksUri)); - jwkSet = JWKSet.parse(resource.getContent()); - publicKeys.putAll(processKeys(jwkSet)); - } catch (IOException | ParseException | IllegalStateException e) { - log.error("Error processing response from URI {} message: {}", jwksUri, e.getMessage()); + publicKeys.clear(); + for (String url : jwksUri) { + try { + Resource resource = resourceRetriever.retrieveResource(new URL(url)); + var tmpJwk = JWKSet.parse(resource.getContent()); + tmpJwk.getKeys().forEach(jwk -> publicKeys.put(jwk.getKeyID(), jwk)); + } catch (IOException | ParseException | IllegalStateException e) { + log.error("Error processing response from URI {} message: {}", url, e.getMessage()); + } } - } + jwkSet = new JWKSet(publicKeys.values().stream().toList()); - private Map processKeys(JWKSet jwkKeys) { - return jwkKeys.getKeys().stream() - .filter(jwkKey -> { - KeyUse keyUse = jwkKey.getKeyUse(); - KeyType keyType = jwkKey.getKeyType(); - return keyUse != null && keyType != null && "sig".equals(keyUse.getValue()) && "RSA".equals(keyType.getValue()); - }) - .collect(Collectors.toMap(JWK::getKeyID, jwkKey -> { - try { - return jwkKey.toRSAKey().toRSAPublicKey(); - } catch (JOSEException e) { - log.debug("Problem with getting RSA Public key from JWK. ", e.getCause()); - throw new IllegalStateException("Failed to parse public key", e); - } - })); } @Override public boolean isValid(String token) { try { log.debug("Validating the token with JWK: {}", jwksUri); - return !getClaims(token).isEmpty(); + if (Collections.isEmpty(jwksUri) || getClaims(token).isEmpty()) { + return isValidExternal(token); + } + return true; + } catch (MalformedJwtException jwte) { + log.debug("Malformed JWT: {}", jwte.getMessage(), jwte.getCause()); + return false; } catch (JwtException jwte) { log.debug("JWK token validation failed with the exception {}", jwte.getMessage(), jwte.getCause()); + return isValidExternal(token); + } + } + + public boolean isValidExternal(String token) { + try { + if (StringUtils.isBlank(endpointUrl)) { + log.debug("JWT can't be validated externally because endpoint URL was not provided."); + return false; + } + log.debug("Validating the token against URL: {}", endpointUrl); + var httpGet = new HttpGet(endpointUrl); + httpGet.addHeader(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + token); + + return secureHttpClientWithKeystore.execute(httpGet, response -> { + final int responseCode = response.getCode(); + log.debug("Response code: {}", responseCode); + return HttpStatus.valueOf(responseCode).is2xxSuccessful(); + }); + } catch (IOException e) { + log.error("An error occurred during validation of OIDC token using userInfo URI {}: {}", endpointUrl, e.getMessage()); return false; } } @@ -155,10 +164,10 @@ class LocatorAdapterKid extends LocatorAdapter { @Override protected Key locate(ProtectedHeader header) { - if (jwkSet == null) { + if (jwkSet == null || jwkSet.isEmpty()) { throw new JwtException("Could not validate the token due to missing public key."); } - String kid = header.getKeyId(); + var kid = header.getKeyId(); if (kid == null) { throw new UnsupportedKeyException("Token does not provide kid. It uses an unsupported type of signature."); } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java deleted file mode 100644 index 404c28c0eb..0000000000 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderEndpoint.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.zaas.security.service.token; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.http.HttpHeaders; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.zowe.apiml.constants.ApimlConstants; -import org.zowe.apiml.security.common.token.OIDCProvider; - -import java.io.IOException; - -@RequiredArgsConstructor -@Service -@Slf4j -@ConditionalOnExpression("'${apiml.security.oidc.validationType:JWK}' == 'endpoint' && '${apiml.security.oidc.enabled:false}' == 'true'") -public class OIDCTokenProviderEndpoint implements OIDCProvider { - - @Value("${apiml.security.oidc.userInfo.uri}") - private String endpointUrl; - - private final CloseableHttpClient secureHttpClientWithKeystore; - - @Override - public boolean isValid(String token) { - try { - log.debug("Validating the token against URL: {}", endpointUrl); - HttpGet httpGet = new HttpGet(endpointUrl); - httpGet.addHeader(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + token); - - return secureHttpClientWithKeystore.execute(httpGet, response -> { - final int responseCode = response.getCode(); - log.debug("Response code: {}", responseCode); - return HttpStatus.valueOf(responseCode).is2xxSuccessful(); - }); - } catch (IOException e) { - log.error("An error occurred during validation of OIDC token using userInfo URI {}: {}", endpointUrl, e.getMessage()); - return false; - } - } - -} diff --git a/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderJWKEndpointTest.java b/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderEndpointTest.java similarity index 98% rename from zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderJWKEndpointTest.java rename to zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderEndpointTest.java index d39905fffd..0f5b586eb3 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderJWKEndpointTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/acceptance/OIDCTokenProviderEndpointTest.java @@ -44,7 +44,7 @@ import org.zowe.apiml.util.HttpClientMockHelper; import org.zowe.apiml.zaas.ZaasApplication; import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; -import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderEndpoint; +import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import javax.net.ssl.SSLContext; import java.io.IOException; @@ -62,14 +62,14 @@ }, classes = { ZaasApplication.class, - OIDCTokenProviderEndpoint.class, - OIDCTokenProviderJWKEndpointTest.Config.class + OIDCTokenProvider.class, + OIDCTokenProviderEndpointTest.Config.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @ActiveProfiles("OIDCTokenProviderEndpointTest") -class OIDCTokenProviderJWKEndpointTest { +class OIDCTokenProviderEndpointTest { private static final String MF_USER = "USER"; private static final String VALID_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java index 1ba0bb2ff7..82d92d80b2 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java @@ -36,7 +36,7 @@ import org.zowe.apiml.security.common.token.TokenAuthentication; import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.JwtSecurity; -import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK; +import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; import org.zowe.apiml.zaas.security.webfinger.WebFingerProvider; import org.zowe.apiml.zaas.security.webfinger.WebFingerResponse; @@ -74,7 +74,7 @@ class AuthControllerTest { private AccessTokenProvider tokenProvider; @Mock - private OIDCTokenProviderJWK oidcProvider; + private OIDCTokenProvider oidcProvider; @Mock private WebFingerProvider webFingerProvider; diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderJWKTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderJWKTest.java deleted file mode 100644 index 26ef7188a8..0000000000 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderJWKTest.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.zaas.security.service.token; - -import com.google.common.io.Resources; -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.Resource; -import io.jsonwebtoken.impl.DefaultClock; -import io.jsonwebtoken.impl.FixedClock; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.zaas.cache.CachingServiceClientException; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.Key; -import java.security.PublicKey; -import java.time.Instant; -import java.util.Date; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class OIDCTokenProviderJWKTest { - - private static final String OKTA_JWKS_RESOURCE = "test_samples/okta_jwks.json"; - - private static final String EXPIRED_TOKEN = "eyJraWQiOiJMY3hja2tvcjk0cWtydW54SFA3VGtpYjU0N3J6bWtYdnNZVi1uYzZVLU40IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULlExakp2UkZ0dUhFUFpGTXNmM3A0enQ5aHBRRHZrSU1CQ3RneU9IcTdlaEkiLCJpc3MiOiJodHRwczovL2Rldi05NTcyNzY4Ni5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2OTcwNjA3NzMsImV4cCI6MTY5NzA2NDM3MywiY2lkIjoiMG9hNmE0OG1uaVhBcUVNcng1ZDciLCJ1aWQiOiIwMHU5OTExOGgxNmtQT1dBbTVkNyIsInNjcCI6WyJvcGVuaWQiXSwiYXV0aF90aW1lIjoxNjk3MDYwMDY0LCJzdWIiOiJzajg5NTA5MkBicm9hZGNvbS5uZXQiLCJncm91cHMiOlsiRXZlcnlvbmUiXX0.Cuf1JVq_NnfBxaCwiLsR5O6DBmVV1fj9utAfKWIF1hlek2hCJsDLQM4ii_ucQ0MM1V3nVE1ZatPB-W7ImWPlGz7NeNBv7jEV9DkX70hchCjPHyYpaUhAieTG75obdufiFpI55bz3qH5cPRvsKv0OKKI9T8D7GjEWsOhv6CevJJZZvgCFLGFfnacKLOY5fEBN82bdmCulNfPVrXF23rOregFjOBJ1cKWfjmB0UGWgI8VBGGemMNm3ACX3OYpTOek2PBfoCIZWOSGnLZumFTYA0F_3DsWYhIJNoFv16_EBBJcp_C0BYE_fiuXzeB0fieNUXASsKp591XJMflDQS_Zt1g"; - - private static final String TOKEN = "token"; - - private OIDCTokenProviderJWK oidcTokenProviderJwk; - - @Mock private DefaultResourceRetriever resourceRetriever; - - @BeforeEach - void setup() throws CachingServiceClientException, IOException { - oidcTokenProviderJwk = new OIDCTokenProviderJWK(new DefaultClock(), resourceRetriever); - ReflectionTestUtils.setField(oidcTokenProviderJwk, "jwkRefreshInterval", 1); - ReflectionTestUtils.setField(oidcTokenProviderJwk, "jwksUri", "https://jwksurl"); - - String oktaJwks = Resources.toString(Resources.getResource(OKTA_JWKS_RESOURCE), StandardCharsets.UTF_8); - - lenient().when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(oktaJwks, null)); - } - - @Nested - class GivenInitializationWithJwks { - - @Test - void initialized_thenJwksFullfilled() { - oidcTokenProviderJwk.afterPropertiesSet(); - Map publicKeys = oidcTokenProviderJwk.getPublicKeys(); - - assertFalse(publicKeys.isEmpty()); - assertTrue(publicKeys.containsKey("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); - assertTrue(publicKeys.containsKey("-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4")); - assertNotNull(publicKeys.get("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); - assertInstanceOf(Key.class, publicKeys.get("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); - } - - @Test - void whenRequestFails_thenNotInitialized() throws IOException { - doThrow(new IOException("failed request")).when(resourceRetriever).retrieveResource(any()); - oidcTokenProviderJwk.afterPropertiesSet(); - assertTrue(oidcTokenProviderJwk.getPublicKeys().isEmpty()); - } - - @Test - void whenUriNotProvided_thenNotInitialized() { - ReflectionTestUtils.setField(oidcTokenProviderJwk, "jwksUri", ""); - oidcTokenProviderJwk.afterPropertiesSet(); - assertTrue(oidcTokenProviderJwk.getPublicKeys().isEmpty()); - } - - @Test - void whenInvalidKeyResponse_thenNotInitialized() throws IOException { - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource("invalid_json", null)); - oidcTokenProviderJwk.afterPropertiesSet(); - assertTrue(oidcTokenProviderJwk.getPublicKeys().isEmpty()); - } - } - - @Nested - class GivenTokenForValidation { - - @Test - void whenValidTokenExpired_thenReturnInvalid() { - assertFalse(oidcTokenProviderJwk.isValid(EXPIRED_TOKEN)); - } - - @Test - void whenValidToken_thenReturnValid() { - ReflectionTestUtils.setField(oidcTokenProviderJwk, "clock", new FixedClock(new Date(Instant.ofEpochSecond(1697060773 + 1000L).toEpochMilli()))); - assertTrue(oidcTokenProviderJwk.isValid(EXPIRED_TOKEN)); - } - - @Test - void whenInvalidToken_thenReturnInvalid() { - assertFalse(oidcTokenProviderJwk.isValid(TOKEN)); - } - - @Test - void whenNoJwk_thenReturnInvalid() { - assumeTrue(oidcTokenProviderJwk.getPublicKeys().isEmpty()); - assertFalse(oidcTokenProviderJwk.isValid(TOKEN)); - } - - } - - @Nested - class GivenEmptyTokenProvided { - @Test - void whenTokenIsNull_thenReturnInvalid() { - assertFalse(oidcTokenProviderJwk.isValid(null)); - } - - @Test - void whenTokenIsEmpty_thenReturnInvalid() { - assertFalse(oidcTokenProviderJwk.isValid("")); - } - } - - @Nested - class JwksUriLoad { - - @BeforeEach - public void setUp() { - oidcTokenProviderJwk = new OIDCTokenProviderJWK(new DefaultClock(), resourceRetriever); - ReflectionTestUtils.setField(oidcTokenProviderJwk, "jwksUri", "https://jwksurl"); - ReflectionTestUtils.setField(oidcTokenProviderJwk, "resourceRetriever", resourceRetriever); - } - - @Test - void shouldNotModifyJwksUri() throws IOException { - var json = "{}"; - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); - - assertDoesNotThrow(() -> oidcTokenProviderJwk.fetchJWKSet()); - } - - @Test - void shouldHandleNullPointer_whenJWKKeyNull() throws IOException { - var json = """ - { - "keys": [ - { - "kty": RSA, - "alg": "RS256", - "kid": "Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4", - "use": null, - "e": "AQAB", - "n": "v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw" - } - ] - } - """; - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); - assertDoesNotThrow(() -> oidcTokenProviderJwk.fetchJWKSet()); - } - - - @Test - void shouldHandleNullPointer_whenJWKTypeNull() throws IOException { - var json = """ - { - "keys": [ - { - "kty": null, - "alg": "RS256", - "kid": "Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4", - "use": "sig", - "e": "AQAB", - "n": "v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw" - } - ] - } - """; - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); - - assertDoesNotThrow(() -> oidcTokenProviderJwk.fetchJWKSet()); - } - - @Test - void throwsCorrectException() throws IOException { - var json = """ - { - "keys": [ - { - "kty": RSA, - "kid": "123", - "use": "sig", - "e": "AQAB", - "n": "v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw" - } - ] - } - """; - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); - - assertDoesNotThrow(() -> oidcTokenProviderJwk.fetchJWKSet()); - } - - } -} diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java new file mode 100644 index 0000000000..438e585f97 --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java @@ -0,0 +1,262 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.security.service.token; + +import com.google.common.io.Resources; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.Resource; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.impl.DefaultClock; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.http.HttpHeaders; +import org.junit.jupiter.api.BeforeAll; +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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.constants.ApimlConstants; +import org.zowe.apiml.zaas.cache.CachingServiceClientException; + +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; +import java.util.*; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class OIDCTokenProviderTest { + + private static final String OKTA_JWKS_RESOURCE = "test_samples/okta_jwks.json"; + + private static final String EXPIRED_TOKEN = "eyJraWQiOiJMY3hja2tvcjk0cWtydW54SFA3VGtpYjU0N3J6bWtYdnNZVi1uYzZVLU40IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULlExakp2UkZ0dUhFUFpGTXNmM3A0enQ5aHBRRHZrSU1CQ3RneU9IcTdlaEkiLCJpc3MiOiJodHRwczovL2Rldi05NTcyNzY4Ni5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2OTcwNjA3NzMsImV4cCI6MTY5NzA2NDM3MywiY2lkIjoiMG9hNmE0OG1uaVhBcUVNcng1ZDciLCJ1aWQiOiIwMHU5OTExOGgxNmtQT1dBbTVkNyIsInNjcCI6WyJvcGVuaWQiXSwiYXV0aF90aW1lIjoxNjk3MDYwMDY0LCJzdWIiOiJzajg5NTA5MkBicm9hZGNvbS5uZXQiLCJncm91cHMiOlsiRXZlcnlvbmUiXX0.Cuf1JVq_NnfBxaCwiLsR5O6DBmVV1fj9utAfKWIF1hlek2hCJsDLQM4ii_ucQ0MM1V3nVE1ZatPB-W7ImWPlGz7NeNBv7jEV9DkX70hchCjPHyYpaUhAieTG75obdufiFpI55bz3qH5cPRvsKv0OKKI9T8D7GjEWsOhv6CevJJZZvgCFLGFfnacKLOY5fEBN82bdmCulNfPVrXF23rOregFjOBJ1cKWfjmB0UGWgI8VBGGemMNm3ACX3OYpTOek2PBfoCIZWOSGnLZumFTYA0F_3DsWYhIJNoFv16_EBBJcp_C0BYE_fiuXzeB0fieNUXASsKp591XJMflDQS_Zt1g"; + + private static String VALID_TOKEN; + private static final String MALFORMED_TOKEN = "token"; + private static JWKSet localJwkSet; + private static String oktaJwks; + + private OIDCTokenProvider oidcTokenProvider; + + @Mock + private DefaultResourceRetriever resourceRetriever; + @Mock + private CloseableHttpClient httpClient; + + static Stream invalidTokens() { + return Stream.of( + EXPIRED_TOKEN, MALFORMED_TOKEN, "", null + ); + } + + @BeforeAll + static void init() throws Exception { + var now = Instant.now(); + var pKey = loadPrivateKey("../keystore/localhost/localhost.keystore.p12", "localhost", "password"); + VALID_TOKEN = Jwts.builder() + .header().keyId("0987").and() + .subject("user") + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(1200))) + .issuer("API ML") + .id(UUID.randomUUID().toString()) + .signWith(pKey, Jwts.SIG.RS256).compact(); + } + + static PrivateKey loadPrivateKey(String path, String alias, String password) throws Exception { + KeyStore ks = KeyStore.getInstance("PKCS12"); + try (FileInputStream fis = new FileInputStream(path)) { + ks.load(fis, password.toCharArray()); + } + Key key = ks.getKey(alias, password.toCharArray()); + var cert = ks.getCertificate(alias); + var pubKey = cert.getPublicKey(); + if (pubKey instanceof RSAPublicKey rsaPublicKey) { + var k = new RSAKey.Builder(rsaPublicKey).keyID("0987").build().toPublicJWK(); + localJwkSet = new JWKSet(k); + } + + return (PrivateKey) key; + } + + @BeforeEach + void setup() throws CachingServiceClientException, IOException { + oidcTokenProvider = new OIDCTokenProvider(new DefaultClock(), resourceRetriever, httpClient); + ReflectionTestUtils.setField(oidcTokenProvider, "jwkRefreshInterval", 1); + oktaJwks = Resources.toString(Resources.getResource(OKTA_JWKS_RESOURCE), StandardCharsets.UTF_8); + } + + @Nested + class GivenInitializationWithJwks { + + @Test + void whenUriNotProvided_thenNotInitialized() throws Exception { + ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", Collections.emptyList()); + oidcTokenProvider.afterPropertiesSet(); + verify(resourceRetriever, times(0)).retrieveResource(any()); + } + } + + @Nested + class GivenCorrectConfiguration { + + + @Nested + class WhenJWKValidation { + + @BeforeEach + void init() throws Exception { + ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", Arrays.asList("https://localjwk", "https://jwksurl")); + when(resourceRetriever.retrieveResource(eq(new URL("https://jwksurl")))).thenReturn(new Resource(oktaJwks, null)); + when(resourceRetriever.retrieveResource(eq(new URL("https://localjwk")))).thenReturn(new Resource(localJwkSet.toString(), null)); + } + + @ParameterizedTest(name = "#{index} return invalid when given invalid token: {0}") + @MethodSource("org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderTest#invalidTokens") + void whenInvalidToken_thenReturnInvalid(String token) { + assertFalse(oidcTokenProvider.isValid(token)); + } + + @Test + void whenValidToken_thenReturnValid() { + assertTrue(oidcTokenProvider.isValid(VALID_TOKEN)); + } + + } + + @Nested + class WhenEndpointValidation { + + + @BeforeEach + void init() throws Exception { + ReflectionTestUtils.setField(oidcTokenProvider, "endpointUrl", "https://entra.com"); + var httpGet = new HttpGet("https://entra.com"); + httpGet.addHeader(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN); + try (var mockResponse = mock(ClassicHttpResponse.class)) { + + when(httpClient.execute(any(HttpGet.class), any(HttpClientResponseHandler.class))) + .thenAnswer(invocation -> { + HttpGet get = invocation.getArgument(0); + if (get.getHeader(HttpHeaders.AUTHORIZATION).getValue().equals(ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN)) { + when(mockResponse.getCode()).thenReturn(200); + } else { + when(mockResponse.getCode()).thenReturn(401); + } + HttpClientResponseHandler handler = invocation.getArgument(1); + return handler.handleResponse(mockResponse); + }); + } + } + + @ParameterizedTest(name = "#{index} return invalid when given invalid token: {0}") + @MethodSource("org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderTest#invalidTokens") + void whenInvalidToken_thenReturnInvalid(String token) { + assertFalse(oidcTokenProvider.isValid(token)); + } + + @Test + void whenValidToken_thenReturnValid() { + assertTrue(oidcTokenProvider.isValid(VALID_TOKEN)); + } + + } + } + + + @Nested + class JwksUriLoad { + + @BeforeEach + public void setUp() { + oidcTokenProvider = new OIDCTokenProvider(new DefaultClock(), resourceRetriever, httpClient); + ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", List.of("https://jwksurl")); + ReflectionTestUtils.setField(oidcTokenProvider, "resourceRetriever", resourceRetriever); + } + + @Test + void shouldNotModifyJwksUri() throws IOException { + var json = "{}"; + + when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); + + assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); + assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); + } + + + @Test + void givenMissingParameterInJWK_doNotThrowException() throws IOException { + var json = """ + { + "keys": [ + { + "kty": null, + "alg": "RS256", + "kid": "Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4", + "use": "sig", + "e": "AQAB", + "n": "v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw" + } + ] + } + """; + + when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); + + assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); + assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); + } + + @Test + void giveValidJWK_setPublicKey() throws IOException { + var json = """ + { + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "kid": "-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4", + "use": "sig", + "e": "AQAB", + "n": "5rYyqFsxel0Pv-xRDHPbg3IfumE4ks9ffLvJrfZVgrTQyiFmFfBnyD3r7y6626Yr5-68Pj0I5SHlCBPkkgTU_e9Z3tCYiegtIOeJdSdumWR2JDVAsbpwFJDG_kxP9czgX7HL0T2BPSapx7ba0ZBXd2-SfSDDL-c1Q0rJ1uQEJwDXAGZV4qy_oXuQf5DuV65Xj8y2Qn1DtVEBThxita-kis_H35CTWgW2zyyaS_08wa00R98mnQ2SHfmO5fZABITmH0DO0coDHqKZ429VNNpELLX9e95dirQ1jfngDbBCmy-XsT8yc6NpAaXmd8P2NHdsO2oK46EQEaFRyMcoDTs3-w" + } + ] + } + """; + + when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); + + oidcTokenProvider.fetchJWKSet(); + assertFalse(oidcTokenProvider.getPublicKeys().isEmpty()); + } + + } +} From 65f979faab22ef416b4f17ba959b608eb9e6ba56 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:03:41 +0200 Subject: [PATCH 094/152] fix: start caching service with disabled cert verification (#4299) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .../main/java/org/zowe/apiml/WebSecurityConfig.java | 2 +- .../apiml/caching/config/SpringSecurityConfig.java | 10 +++++----- .../zaas/security/service/token/OIDCTokenProvider.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java index ee8511891d..3700706144 100644 --- a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java @@ -122,7 +122,7 @@ SecurityWebFilterChain errorFilterChain(ServerHttpSecurity http) { @Bean @Order(0) SecurityWebFilterChain discoveryServiceClientCertificateFilterChain(ServerHttpSecurity http) { - http + http.csrf(ServerHttpSecurity.CsrfSpec::disable) .securityMatcher(new AndServerWebExchangeMatcher( discoveryPortMatcher, pathMatchers("/eureka/**"), diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java index 16cc3cb04c..55f899f705 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java @@ -61,15 +61,15 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { .securityMatcher(new AndServerWebExchangeMatcher( ServerWebExchangeMatchers.pathMatchers("/cachingservice/**") )) - .authorizeExchange(exchange -> exchange - .pathMatchers(antMatchersToIgnore.toArray(new String[0])).permitAll() - .anyExchange().authenticated() - ).exceptionHandling(exceptionHandlingSpec -> + .exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.FORBIDDEN)) ); if (verifyCertificates || !nonStrictVerifyCerts) { - http.x509(x509spec -> x509spec.principalExtractor(X509Util.x509PrincipalExtractor()) + http.authorizeExchange(exchange -> exchange + .pathMatchers(antMatchersToIgnore.toArray(new String[0])).permitAll() + .anyExchange().authenticated() + ).x509(x509spec -> x509spec.principalExtractor(X509Util.x509PrincipalExtractor()) .authenticationManager(X509Util.x509ReactiveAuthenticationManager())); } else { http.authorizeExchange(exchange -> exchange.anyExchange().permitAll()); diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java index f4c8aa6cfd..42bcf2694c 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java @@ -89,10 +89,10 @@ void fetchJWKSet() { log.debug("OIDC JWK URI not provided, JWK refresh not performed"); return; } - log.debug("Refreshing JWK endpoints {}", jwksUri); publicKeys.clear(); for (String url : jwksUri) { + log.debug("Refreshing JWK endpoints {}", url); try { Resource resource = resourceRetriever.retrieveResource(new URL(url)); var tmpJwk = JWKSet.parse(resource.getContent()); @@ -108,7 +108,7 @@ void fetchJWKSet() { @Override public boolean isValid(String token) { try { - log.debug("Validating the token with JWK: {}", jwksUri); + if (Collections.isEmpty(jwksUri) || getClaims(token).isEmpty()) { return isValidExternal(token); } @@ -151,7 +151,7 @@ Claims getClaims(String token) { if (StringUtils.isBlank(token)) { throw new JwtException("Empty string provided instead of a token."); } - + log.debug("Validating the token with JWK"); return Jwts.parser() .clock(clock) .keyLocator(keyLocator) From 489916129b033a1ef349f5a41002d64f134eff2b Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Tue, 2 Sep 2025 07:23:24 +0000 Subject: [PATCH 095/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.7'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index decf7d9c88..fcdeba7a48 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.7-SNAPSHOT +version=3.3.7 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 46e85a65d1409bb47f2c76a27d68a028f4c7a6de Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Tue, 2 Sep 2025 07:23:26 +0000 Subject: [PATCH 096/152] [Gradle Release plugin] Create new version: 'v3.3.8-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fcdeba7a48..fab8568f8b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.7 +version=3.3.8-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From f39e1040fa4f92caa7b120d919d72257172cf8e1 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Tue, 2 Sep 2025 07:23:27 +0000 Subject: [PATCH 097/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 5f0546501a..8a6d082236 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.7-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.8-SNAPSHOT From 7a974e483d73df69847e2582bab88e8a0b747279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Wed, 3 Sep 2025 13:05:16 +0200 Subject: [PATCH 098/152] chore: remove obsolete caching from OIDCAuthSourceService (#4302) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- .../schema/source/OIDCAuthSourceService.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java index 40631d7262..ab6b495a56 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java @@ -10,24 +10,23 @@ package org.zowe.apiml.zaas.security.service.schema.source; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import org.zowe.apiml.security.common.token.TokenNotValidException; -import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; -import org.zowe.apiml.zaas.security.service.AuthenticationService; -import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.message.core.MessageType; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; +import org.zowe.apiml.zaas.security.service.AuthenticationService; +import org.zowe.apiml.zaas.security.service.TokenCreationService; -import jakarta.servlet.http.HttpServletRequest; import java.util.Optional; import java.util.function.Function; @@ -67,7 +66,6 @@ public Optional getToken(HttpServletRequest request) { } @Override - @Cacheable(value = "validationOIDCToken", key = "#oidcToken", condition = "#oidcToken != null") public boolean isValid(AuthSource authSource) { if (authSource instanceof OIDCAuthSource) { String token = ((OIDCAuthSource) authSource).getRawSource(); @@ -88,7 +86,6 @@ public boolean isValid(AuthSource authSource) { } @Override - @Cacheable(value = "parseOIDCToken", key = "#parsedOIDCToken", condition = "#parsedOIDCToken != null") public AuthSource.Parsed parse(AuthSource authSource) { if (authSource instanceof OIDCAuthSource) { if (isValid(authSource)) { From 2521df5276bc9f871b72d47942accfa69fd17c43 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:12:53 +0200 Subject: [PATCH 099/152] chore: test ssl verification disabled (#4301) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .../src/main/resources/bin/start.sh | 1 - .../config/SecurityConfiguration.java | 27 ++-- .../functional/SecurityConfigTest.java | 136 ++++++++++++++++++ apiml-package/src/main/resources/bin/start.sh | 1 - .../src/main/resources/bin/start.sh | 1 - .../caching/config/SpringSecurityConfig.java | 5 +- .../caching/config/SecurityConfigTest.java | 115 +++++++++++++++ .../functional/InMemoryFunctionalTest.java | 13 +- .../src/main/resources/bin/start.sh | 1 - .../config/HttpsWebSecurityConfig.java | 7 +- .../functional/SecurityConfigTest.java | 108 ++++++++++++++ .../src/main/resources/bin/start.sh | 1 - zaas-package/src/main/resources/bin/start.sh | 1 - 13 files changed, 380 insertions(+), 37 deletions(-) create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/SecurityConfigTest.java create mode 100644 caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java create mode 100644 discovery-service/src/test/java/org/zowe/apiml/discovery/functional/SecurityConfigTest.java diff --git a/api-catalog-package/src/main/resources/bin/start.sh b/api-catalog-package/src/main/resources/bin/start.sh index df8e5fd723..2df3f1581e 100755 --- a/api-catalog-package/src/main/resources/bin/start.sh +++ b/api-catalog-package/src/main/resources/bin/start.sh @@ -108,7 +108,6 @@ fi verify_certificates_config=$(echo "${ZWE_zowe_verifyCertificates}" | tr '[:lower:]' '[:upper:]') if [ "${verify_certificates_config}" = "DISABLED" ]; then verifySslCertificatesOfServices=false - nonStrictVerifySslCertificatesOfServices=true elif [ "${verify_certificates_config}" = "NONSTRICT" ]; then verifySslCertificatesOfServices=true nonStrictVerifySslCertificatesOfServices=true diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java index 09c8090e25..b9c8d7068e 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SecurityConfiguration.java @@ -93,9 +93,6 @@ public class SecurityConfiguration { @Value("${apiml.security.ssl.verifySslCertificatesOfServices:true}") private boolean verifySslCertificatesOfServices; - @Value("${apiml.security.ssl.nonStrictVerifySslCertificatesOfServices:false}") - private boolean nonStrictVerifySslCertificatesOfServices; - private WebFilter basicAuthenticationFilter; private WebFilter tokenAuthenticationFilter; private WebFilter oidcAuthenticationFilter; @@ -107,7 +104,7 @@ void initFilters() { oidcAuthenticationFilter = oidcAuthenticationFilter(gatewaySecurity); } - private String[] getFullUrls(String...baseUrl) { + private String[] getFullUrls(String... baseUrl) { String prefix = applicationInfo.isModulith() ? "/apicatalog/api/v1" : "/apicatalog"; for (int i = 0; i < baseUrl.length; i++) { baseUrl[i] = prefix + baseUrl[i]; @@ -154,9 +151,9 @@ SecurityWebFilterChain basicAuthOrTokenOrCertApiDocFilterChain( serverAuthenticationEntryPoint, basicAuthenticationFilter, tokenAuthenticationFilter, oidcAuthenticationFilter ) - .authorizeExchange(exchange -> exchange.anyExchange().authenticated()); + .authorizeExchange(exchange -> exchange.anyExchange().authenticated()); - if (verifySslCertificatesOfServices || !nonStrictVerifySslCertificatesOfServices) { + if (verifySslCertificatesOfServices) { http.x509(x509 -> x509 .principalExtractor(X509Util.x509PrincipalExtractor()) .authenticationManager(X509Util.x509ReactiveAuthenticationManager()) @@ -198,9 +195,9 @@ SecurityWebFilterChain basicAuthOrTokenAllEndpointsFilterChain( ) { return baseConfiguration(http.securityMatcher(ServerWebExchangeMatchers.pathMatchers( getFullUrls("/static-api/**", "/containers", "/containers/**", "/application/**", "/services/**", APIDOC_ROUTES))), - serverAuthenticationEntryPoint, - basicAuthenticationFilter, tokenAuthenticationFilter, oidcAuthenticationFilter - ) + serverAuthenticationEntryPoint, + basicAuthenticationFilter, tokenAuthenticationFilter, oidcAuthenticationFilter + ) .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .build(); } @@ -233,7 +230,7 @@ SecurityWebFilterChain webSecurityCustomizer( private ServerHttpSecurity baseConfiguration( ServerHttpSecurity http, ServerAuthenticationEntryPoint serverAuthenticationEntryPoint, - WebFilter...webFiltersAuthorization + WebFilter... webFiltersAuthorization ) { var antMatcher = new AntPathMatcher(); @@ -254,10 +251,10 @@ private ServerHttpSecurity baseConfiguration( log.debug("Unauthorized access to '{}' endpoint", requestedPath); if (Stream.of(getFullUrls( - "/application/**", - APIDOC_ROUTES, - STATIC_REFRESH_ROUTE - )).anyMatch(pattern -> antMatcher.match(pattern, requestedPath)) + "/application/**", + APIDOC_ROUTES, + STATIC_REFRESH_ROUTE + )).anyMatch(pattern -> antMatcher.match(pattern, requestedPath)) ) { exchange.getResponse().getHeaders().add(HttpHeaders.WWW_AUTHENTICATE, ApimlConstants.BASIC_AUTHENTICATION_PREFIX); } @@ -266,7 +263,7 @@ private ServerHttpSecurity baseConfiguration( }) ); - Stream.of(webFiltersAuthorization).forEach(webFilter -> http.addFilterBefore(webFilter, SecurityWebFiltersOrder.AUTHENTICATION)); + Stream.of(webFiltersAuthorization).forEach(webFilter -> http.addFilterBefore(webFilter, SecurityWebFiltersOrder.AUTHENTICATION)); return http; } diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/SecurityConfigTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/SecurityConfigTest.java new file mode 100644 index 0000000000..2b276a021c --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/functional/SecurityConfigTest.java @@ -0,0 +1,136 @@ +/* + * 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.apicatalog.functional; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Value; +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.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.apicatalog.ApiCatalogApplication; +import org.zowe.apiml.apicatalog.swagger.ApiDocService; +import org.zowe.apiml.security.client.service.GatewaySecurity; +import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.SslContextConfigurer; +import reactor.core.publisher.Mono; + +import java.util.Optional; + +import static io.restassured.RestAssured.given; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest( + classes = ApiCatalogApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT +) +public class SecurityConfigTest { + + + @BeforeAll + static void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); + SslContext.prepareSslAuthentication(configurer); + + } + + private String getUri(String hostname, int port) { + return String.format("%s://%s:%d/%s", "https", hostname, port, "apicatalog/apidoc/service1"); + } + + @Nested + @TestPropertySource( + properties = { + "apiml.security.ssl.verifySslCertificatesOfServices=false" + } + ) + @DirtiesContext + class GivenDisabledSSLVerification { + + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + + @Test + void whenClientCertificate_thenReturnUnauthorized() { + given() + .config(SslContext.clientCertApiml) + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } + } + + @Nested + @TestPropertySource( + properties = { + "apiml.security.ssl.verifySslCertificatesOfServices=true" + } + ) + @DirtiesContext + class GivenEnabledSSLVerification { + + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + + @MockitoBean + GatewaySecurity gatewaySecurity; + + @MockitoBean + ApiDocService apiDocService; + + @BeforeEach + void setup() { + when(apiDocService.retrieveDefaultApiDoc(any())).thenReturn(Mono.just("{}")); + } + + @Test + void whenNoClientCertificateButBasicAuth_thenReturnOk() { + when(gatewaySecurity.login(any(), any(), any())).thenReturn(Optional.of("token")); + given() + .header("Authorization", "Basic dXNlcjpwYXNz") + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.OK.value()); + } + + @Test + void whenClientCertificate_thenReturnOk() { + given() + .config(SslContext.clientCertApiml) + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.OK.value()); + } + + @Test + void whenNoCredentials_thenReturnUnauthorized() { + + given() + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } + } +} diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index b84dc8dd6c..02e75b669e 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -140,7 +140,6 @@ fi verify_certificates_config=$(echo "${ZWE_zowe_verifyCertificates}" | tr '[:lower:]' '[:upper:]') if [ "${verify_certificates_config}" = "DISABLED" ]; then verifySslCertificatesOfServices=false - nonStrictVerifySslCertificatesOfServices=true elif [ "${verify_certificates_config}" = "NONSTRICT" ]; then verifySslCertificatesOfServices=true nonStrictVerifySslCertificatesOfServices=true diff --git a/caching-service-package/src/main/resources/bin/start.sh b/caching-service-package/src/main/resources/bin/start.sh index 0ade013093..487ced9b46 100755 --- a/caching-service-package/src/main/resources/bin/start.sh +++ b/caching-service-package/src/main/resources/bin/start.sh @@ -77,7 +77,6 @@ fi verify_certificates_config=$(echo "${ZWE_zowe_verifyCertificates}" | tr '[:lower:]' '[:upper:]') if [ "${verify_certificates_config}" = "DISABLED" ]; then verifySslCertificatesOfServices=false - nonStrictVerifySslCertificatesOfServices=true elif [ "${verify_certificates_config}" = "NONSTRICT" ]; then verifySslCertificatesOfServices=true nonStrictVerifySslCertificatesOfServices=true diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java index 55f899f705..1a17b8a1dd 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/SpringSecurityConfig.java @@ -38,9 +38,6 @@ public class SpringSecurityConfig { @Value("${apiml.service.ssl.verifySslCertificatesOfServices:true}") private boolean verifyCertificates; - @Value("${apiml.service.ssl.nonStrictVerifySslCertificatesOfServices:false}") - private boolean nonStrictVerifyCerts; - @Value("${apiml.health.protected:true}") private boolean isHealthEndpointProtected; @@ -65,7 +62,7 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { exceptionHandlingSpec.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.FORBIDDEN)) ); - if (verifyCertificates || !nonStrictVerifyCerts) { + if (verifyCertificates) { http.authorizeExchange(exchange -> exchange .pathMatchers(antMatchersToIgnore.toArray(new String[0])).permitAll() .anyExchange().authenticated() diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java new file mode 100644 index 0000000000..edddd90170 --- /dev/null +++ b/caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java @@ -0,0 +1,115 @@ +/* + * 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.caching.config; + +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.springframework.beans.factory.annotation.Value; +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.TestPropertySource; +import org.zowe.apiml.caching.CachingServiceApplication; +import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.SslContextConfigurer; + +import static io.restassured.RestAssured.given; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SecurityConfigTest { + + @BeforeAll + static void init() throws Exception { + SslContext.reset(); + RestAssured.useRelaxedHTTPSValidation(); + SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); + SslContext.prepareSslAuthentication(configurer); + } + + private String getUri(String hostname, int port) { + return String.format("%s://%s:%d/%s", "https", hostname, port, "cachingservice/api/v1/cache"); + } + + @Nested + @TestPropertySource( + properties = { + "apiml.service.ssl.verifySslCertificatesOfServices=false" + } + ) + @DirtiesContext + @SpringBootTest( + classes = CachingServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenDisabledSSLVerification { + + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + + @Test + void thenDoNotRequireAuth() { + given() + .header(new Header("X-CS-Service-ID", "apimtst")) + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.OK.value()); + } + } + + @Nested + @TestPropertySource( + properties = { + "apiml.service.ssl.verifySslCertificatesOfServices=true" + } + ) + @DirtiesContext + @SpringBootTest( + classes = CachingServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class GivenEnabledSSLVerification { + + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + + @Test + void whenNoClientCertificate_thenReturnUnauthorized() { + given() + .header(new Header("X-CS-Service-ID", "apimtst")) + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + void whenClientCertificate_thenReturnOk() { + + given() + .config(SslContext.clientCertApiml) + .header(new Header("X-CS-Service-ID", "apimtst")) + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.OK.value()); + } + } +} diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java index b925a21e58..55409d7d6c 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java @@ -11,14 +11,8 @@ package org.zowe.apiml.caching.functional; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @@ -57,6 +51,11 @@ String getUri(String endpoint) { return String.format("https://%s:%s%s%s", hostname, port, contextPath, endpoint); } + @BeforeAll + void init() { + SslContext.reset(); + } + @BeforeEach void setup() throws Exception { SslContextConfigurer configurer = new SslContextConfigurer(password, client_cert_keystore, keystore); diff --git a/discovery-package/src/main/resources/bin/start.sh b/discovery-package/src/main/resources/bin/start.sh index c1316c9bb6..ac1b907856 100755 --- a/discovery-package/src/main/resources/bin/start.sh +++ b/discovery-package/src/main/resources/bin/start.sh @@ -97,7 +97,6 @@ fi verify_certificates_config=$(echo "${ZWE_zowe_verifyCertificates}" | tr '[:lower:]' '[:upper:]') if [ "${verify_certificates_config}" = "DISABLED" ]; then verifySslCertificatesOfServices=false - nonStrictVerifySslCertificatesOfServices=true elif [ "${verify_certificates_config}" = "NONSTRICT" ]; then verifySslCertificatesOfServices=true nonStrictVerifySslCertificatesOfServices=true diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java index 8a439ded7e..418a878d9b 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/config/HttpsWebSecurityConfig.java @@ -67,9 +67,6 @@ public class HttpsWebSecurityConfig extends AbstractWebSecurityConfigurer { @Value("${apiml.security.ssl.verifySslCertificatesOfServices:true}") private boolean verifySslCertificatesOfServices; - @Value("${apiml.security.ssl.nonStrictVerifySslCertificatesOfServices:false}") - private boolean nonStrictVerifySslCertificatesOfServices; - @Bean WebSecurityCustomizer httpsWebSecurityCustomizer() { String[] noSecurityAntMatchers = { @@ -128,7 +125,7 @@ SecurityFilterChain basicAuthOrTokenFilterChain(HttpSecurity http) throws Except @Order(2) SecurityFilterChain clientCertificateFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.securityMatcher("/eureka/**")); - if (verifySslCertificatesOfServices || !nonStrictVerifySslCertificatesOfServices) { + if (verifySslCertificatesOfServices) { http.x509(x509 -> x509.userDetailsService(x509UserDetailsService())) .authorizeHttpRequests(requests -> requests .anyRequest().authenticated() @@ -153,7 +150,7 @@ SecurityFilterChain basicAuthOrTokenOrCertFilterChain(HttpSecurity http) throws .authenticationProvider(gatewayLoginProvider) .authenticationProvider(gatewayTokenProvider) .httpBasic(basic -> basic.realmName(DISCOVERY_REALM)); - if (verifySslCertificatesOfServices || !nonStrictVerifySslCertificatesOfServices) { + if (verifySslCertificatesOfServices) { http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated()) .x509(x509 -> x509.userDetailsService(x509UserDetailsService())); if (isServerAttlsEnabled) { diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/functional/SecurityConfigTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/functional/SecurityConfigTest.java new file mode 100644 index 0000000000..9a9260011c --- /dev/null +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/functional/SecurityConfigTest.java @@ -0,0 +1,108 @@ +/* + * 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.discovery.functional; + +import io.restassured.RestAssured; +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.springframework.beans.factory.annotation.Value; +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.discovery.DiscoveryServiceApplication; +import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.SslContextConfigurer; + +import static io.restassured.RestAssured.given; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest( + classes = DiscoveryServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT +) +@ActiveProfiles({"https"}) +public class SecurityConfigTest { + + @BeforeAll + static void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); + SslContext.prepareSslAuthentication(configurer); + } + + private String getUri(String hostname, int port) { + return String.format("%s://%s:%d/%s", "https", hostname, port, "eureka/apps"); + } + + @Nested + @TestPropertySource( + properties = { + "apiml.security.ssl.verifySslCertificatesOfServices=false" + } + ) + @DirtiesContext + class GivenDisabledSSLVerification { + + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + + @Test + void thenDoNotRequireAuth() { + given() + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.OK.value()); + } + } + + @Nested + @TestPropertySource( + properties = { + "apiml.security.ssl.verifySslCertificatesOfServices=true" + } + ) + @DirtiesContext + class GivenEnabledSSLVerification { + + @Value("${apiml.service.hostname:localhost}") + String hostname; + @LocalServerPort + int port; + + @Test + void whenNoClientCertificate_thenReturnUnauthorized() { + given() + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + void whenClientCertificate_thenReturnOk() { + + given() + .config(SslContext.clientCertApiml) + .get(getUri(hostname, port)) + .then() + .log().ifValidationFails() + .statusCode(HttpStatus.OK.value()); + } + } +} diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 6fe35c851e..ddf05542e6 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -116,7 +116,6 @@ fi verify_certificates_config=$(echo "${ZWE_zowe_verifyCertificates}" | tr '[:lower:]' '[:upper:]') if [ "${verify_certificates_config}" = "DISABLED" ]; then verifySslCertificatesOfServices=false - nonStrictVerifySslCertificatesOfServices=true elif [ "${verify_certificates_config}" = "NONSTRICT" ]; then verifySslCertificatesOfServices=true nonStrictVerifySslCertificatesOfServices=true diff --git a/zaas-package/src/main/resources/bin/start.sh b/zaas-package/src/main/resources/bin/start.sh index 43ce21aa79..1da0b4fce9 100755 --- a/zaas-package/src/main/resources/bin/start.sh +++ b/zaas-package/src/main/resources/bin/start.sh @@ -126,7 +126,6 @@ fi verify_certificates_config=$(echo "${ZWE_zowe_verifyCertificates}" | tr '[:lower:]' '[:upper:]') if [ "${verify_certificates_config}" = "DISABLED" ]; then verifySslCertificatesOfServices=false - nonStrictVerifySslCertificatesOfServices=true elif [ "${verify_certificates_config}" = "NONSTRICT" ]; then verifySslCertificatesOfServices=true nonStrictVerifySslCertificatesOfServices=true From 797d32a0b31cd05a48955932c718fcec771398b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Thu, 4 Sep 2025 10:42:26 +0200 Subject: [PATCH 100/152] feat: Support configurable username field for oidc tokens (#4300) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- apiml-package/src/main/resources/bin/start.sh | 1 + zaas-package/src/main/resources/bin/start.sh | 1 + .../apiml/zaas/security/service/JwtUtils.java | 48 +++++++- .../schema/source/OIDCAuthSourceService.java | 43 +++++-- .../zaas/security/service/JwtUtilsTest.java | 55 ++++++++- .../source/OIDCAuthSourceServiceTest.java | 116 +++++++++++++----- 6 files changed, 220 insertions(+), 44 deletions(-) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 02e75b669e..160ac487ab 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -380,6 +380,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.security.oidc.jwks.refreshInternalHours=${ZWE_components_gateway_apiml_security_oidc_jwks_refreshInternalHours:-${ZWE_configs_apiml_security_oidc_jwks_refreshInternalHours:-1}} \ -Dapiml.security.oidc.jwks.uri=${ZWE_components_gateway_apiml_security_oidc_jwks_uri:-${ZWE_configs_apiml_security_oidc_jwks_uri:-}} \ -Dapiml.security.oidc.registry=${ZWE_components_gateway_apiml_security_oidc_registry:-${ZWE_configs_apiml_security_oidc_registry:-}} \ + -Dapiml.security.oidc.userIdField=${ZWE_components_gateway_apiml_security_oidc_userIdField:-${ZWE_configs_apiml_security_oidc_userIdField:-"sub"}} \ -Dapiml.security.oidc.userInfo.uri=${ZWE_components_gateway_apiml_security_oidc_userInfo_uri:-${ZWE_configs_apiml_security_oidc_userInfo_uri:-}} \ -Dapiml.security.oidc.validationType=${ZWE_components_gateway_apiml_security_oidc_validationType:-${ZWE_configs_apiml_security_oidc_validationType:-"JWK"}} \ -Dapiml.security.personalAccessToken.enabled=${ZWE_components_gateway_apiml_security_personalAccessToken_enabled:-${ZWE_configs_apiml_security_personalAccessToken_enabled:-false}} \ diff --git a/zaas-package/src/main/resources/bin/start.sh b/zaas-package/src/main/resources/bin/start.sh index 1da0b4fce9..3892b0c99c 100755 --- a/zaas-package/src/main/resources/bin/start.sh +++ b/zaas-package/src/main/resources/bin/start.sh @@ -376,6 +376,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${ZAAS_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.security.oidc.identityMapperUser=${ZWE_configs_apiml_security_oidc_identityMapperUser:-${ZWE_components_gateway_apiml_security_oidc_identityMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \ -Dapiml.security.oidc.jwks.uri=${ZWE_configs_apiml_security_oidc_jwks_uri:-${ZWE_components_gateway_apiml_security_oidc_jwks_uri:-}} \ -Dapiml.security.oidc.jwks.refreshInternalHours=${ZWE_configs_apiml_security_oidc_jwks_refreshInternalHours:-${ZWE_components_gateway_apiml_security_oidc_jwks_refreshInternalHours:-1}} \ + -Dapiml.security.oidc.userIdField=${ZWE_configs_apiml_security_oidc_userIdField:-${ZWE_components_gateway_apiml_security_oidc_userIdField:-"sub"}} \ -Dapiml.security.oidc.userInfo.uri=${ZWE_configs_apiml_security_oidc_userInfo_uri:-${ZWE_components_gateway_apiml_security_oidc_userInfo_uri:-}} \ -Dapiml.security.oidc.validationType=${ZWE_configs_apiml_security_oidc_validationType:-${ZWE_components_gateway_apiml_security_oidc_validationType:-"JWK"}} \ -Dapiml.security.allowTokenRefresh=${ZWE_configs_apiml_security_allowtokenrefresh:-${ZWE_components_gateway_apiml_security_allowtokenrefresh:-false}} \ diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java index 6e6ad31590..154619d5cf 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java @@ -10,14 +10,22 @@ package org.zowe.apiml.zaas.security.service; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.zowe.apiml.security.common.token.TokenExpireException; +import org.zowe.apiml.security.common.token.TokenFormatNotValidException; import org.zowe.apiml.security.common.token.TokenNotValidException; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.List; +import java.util.Map; @Slf4j @UtilityClass @@ -87,4 +95,42 @@ public static RuntimeException handleJwtParserException(RuntimeException excepti return new TokenNotValidException("An internal error occurred while validating the token therefore the token is no longer valid.", exception); } + /** + * Extracts value of a field from an OIDC token. The value is extracted from a custom path which supports nested objects. + * @param token to extract the field from + * @param pathToField list of strings representing path to the field + * @return userId extracted from the token + * + * @throws TokenFormatNotValidException in case of the field value cannot be extracted from the token, is null, or empty + */ + @SuppressWarnings("rawtypes") + public static String getFieldValueFromToken(String token, List pathToField) throws TokenFormatNotValidException { + if (token == null || pathToField == null || pathToField.isEmpty() || StringUtils.isBlank(pathToField.get(0))) { + throw new IllegalArgumentException("Token and field path most not be null or empty"); + } + + try { + Claims claims = getJwtClaims(token); + String fieldValue; + if (pathToField.size() == 1) { + fieldValue = claims.get(pathToField.get(0), String.class); + } else { + var iterator = pathToField.iterator(); + var key = iterator.next(); + Map val = claims.get(key, Map.class); + while (iterator.hasNext()) { + key = iterator.next(); + if (iterator.hasNext()) { + val = (Map) val.get(key); + } + } + fieldValue = (String) val.get(key); + } + if (StringUtils.isBlank(fieldValue)) throw new IllegalArgumentException(); + return fieldValue; + } catch (Exception e) { + throw new TokenFormatNotValidException(String.format("Cannot extract value from field %s. The field does not exists, is empty, or is na object.", String.join(".", pathToField))); + } + } + } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java index ab6b495a56..41521d872b 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java @@ -13,7 +13,9 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.zowe.apiml.message.core.MessageType; @@ -22,18 +24,23 @@ import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenFormatNotValidException; import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.TokenCreationService; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.function.Function; +import static org.zowe.apiml.zaas.security.service.JwtUtils.getFieldValueFromToken; + @Service @RequiredArgsConstructor @ConditionalOnProperty(value = "apiml.security.oidc.enabled", havingValue = "true") -public class OIDCAuthSourceService extends TokenAuthSourceService { +public class OIDCAuthSourceService extends TokenAuthSourceService implements InitializingBean { @InjectApimlLogger protected final ApimlLogger logger = ApimlLogger.empty(); @@ -43,6 +50,15 @@ public class OIDCAuthSourceService extends TokenAuthSourceService { private final OIDCProvider oidcProvider; private final TokenCreationService tokenService; + @Value("${apiml.security.oidc.userIdField:sub}") + protected String userIdFieldPathProperty; + private List userIdFieldPath; + + @Override + public void afterPropertiesSet() { + userIdFieldPath = Arrays.asList(userIdFieldPathProperty.trim().split("\\.")); + } + @Override protected ApimlLogger getLogger() { return logger; @@ -67,15 +83,13 @@ public Optional getToken(HttpServletRequest request) { @Override public boolean isValid(AuthSource authSource) { - if (authSource instanceof OIDCAuthSource) { - String token = ((OIDCAuthSource) authSource).getRawSource(); + if (authSource instanceof OIDCAuthSource oidcAuthSource) { + String token = oidcAuthSource.getRawSource(); if (StringUtils.isNotBlank(token)) { logger.log(MessageType.DEBUG, "Validating OIDC token."); if (oidcProvider.isValid(token)) { logger.log(MessageType.DEBUG, "OIDC token is valid, set the distributed id to the auth source."); - QueryResponse tokenClaims = authenticationService.parseJwtToken(token); - ((OIDCAuthSource) authSource).setDistributedId(tokenClaims.getUserId()); - return true; + return extractUserId(oidcAuthSource); } logger.log(MessageType.DEBUG, "OIDC token is not valid or the validation failed."); } @@ -85,11 +99,21 @@ public boolean isValid(AuthSource authSource) { return false; } + private boolean extractUserId(OIDCAuthSource authSource) { + try { + authSource.setDistributedId(getFieldValueFromToken(authSource.getRawSource(), userIdFieldPath)); + return true; + } catch (TokenFormatNotValidException e) { + logger.log(MessageType.DEBUG, String.format("Cannot extract distributed id from token. Reason: %s", e.getMessage())); + return false; + } + } + @Override public AuthSource.Parsed parse(AuthSource authSource) { - if (authSource instanceof OIDCAuthSource) { - if (isValid(authSource)) { - return parseOIDCToken((OIDCAuthSource) authSource, mapper); + if (authSource instanceof OIDCAuthSource oidcAuthSource) { + if (isValid(oidcAuthSource)) { + return parseOIDCToken( oidcAuthSource, mapper); } throw new TokenNotValidException("OIDC token is not valid."); } @@ -135,5 +159,4 @@ public String getJWT(AuthSource authSource) { AuthSource.Parsed parsed = parse(authSource); return tokenService.createJwtTokenWithoutCredentials(parsed.getUserId()); } - } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java index e7029cf694..3b70fbdd4a 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java @@ -15,16 +15,26 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.JwtException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.zowe.apiml.security.common.token.TokenExpireException; +import org.zowe.apiml.security.common.token.TokenFormatNotValidException; import org.zowe.apiml.security.common.token.TokenNotValidException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; - class JwtUtilsTest { + private static final String TOKEN_WITH_USERNAME_FIELDS = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiLAogICJlbWFpbCI6ICJ1c2VybmFtZUBvaWRjLm9yZyIsCiAgIm9yZyI6IHsKICAgICJuYW1lIjogIm9wZW5tYWluZnJhbWUiLAogICAgImRlcCI6IHsKICAgICAgIm5hbWUiOiAiem93ZSIsCiAgICAgICJ0ZWFtIjogImFwaW1sIiwKICAgICAgImNvbnRyaWJ1dG9yIjogImNvbnRyaWJ1dG9yQGFwaW1sLnpvd2UiLAogICAgICAibmlja25hbWUiOiAiIiwKICAgICAgIm51bGxWYWx1ZSI6IG51bGwKICAgIH0KICB9LAogICJtZW1iZXJPZiI6IFsKICAgICJvcGVubWFpbmZyYW1lIiwKICAgICJ6b3dlIiwKICAgICJhcGltbCIKICBdCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; + @Test void testHandleJwtParserExceptionForExpiredToken() { @@ -47,4 +57,45 @@ void testHandleJwtParserRuntimeException() { assertTrue(exception instanceof TokenNotValidException); assertEquals("An internal error occurred while validating the token therefore the token is no longer valid.", exception.getMessage()); } + + @ParameterizedTest + @CsvSource({ + "email,username@oidc.org", + "org.dep.contributor, contributor@apiml.zowe"}) + void givenValidFieldPath_thenReturnCorrectValue(String fieldPath, String expectedValue) { + var actualValue = JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, splitFieldPath(fieldPath)); + assertEquals(expectedValue, actualValue); + } + + @ParameterizedTest + @ValueSource(strings = { "nonexistent", "org.nonexistent.foo", "org.dep", "org.dep.nonexistent", "org.dep.nickname", "org.dep.nullValue"}) + void givenInvalidFieldPath_thenThrowInvalidTokenFormatException(String fieldPath) { + assertThrows(TokenFormatNotValidException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, splitFieldPath(fieldPath))); + } + + @Test + void givenNullToken_thenThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(null, List.of("foo"))); + } + + @Test + void givenNullFieldPath_thenThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, null)); + } + + @Test + void givenEmptyFieldPath_thenThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, Collections.emptyList())); + } + + @Test + void givenEmptyStringFieldPath_thenThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, List.of(" "))); + } + + + private List splitFieldPath(String fieldPath) { + return Arrays.asList(fieldPath.trim().split("\\.")); + } + } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java index 56519e6948..19157a8873 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java @@ -15,9 +15,16 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockHttpServletRequest; -import org.zowe.apiml.security.common.token.*; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; +import org.zowe.apiml.security.common.token.OIDCProvider; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenExpireException; +import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.TokenCreationService; @@ -26,8 +33,17 @@ import java.util.Date; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OIDCAuthSourceServiceTest { @@ -38,10 +54,11 @@ class OIDCAuthSourceServiceTest { private AuthenticationService authenticationService; private OIDCProvider provider; private AuthenticationMapper mapper; - private static final String TOKEN = "token"; - private static final String ISSUER = "issuer"; - private static final String DISTRIB_USER = "pc.user@acme.com"; + private static final String DUMMY_TOKEN = "token"; + private static final String TOKEN_WITH_USERNAME_FIELDS = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiLAogICJlbWFpbCI6ICJ1c2VybmFtZUBvaWRjLm9yZyIsCiAgIm9yZyI6IHsKICAgICJuYW1lIjogIm9wZW5tYWluZnJhbWUiLAogICAgImRlcCI6IHsKICAgICAgIm5hbWUiOiAiem93ZSIsCiAgICAgICJ0ZWFtIjogImFwaW1sIiwKICAgICAgImNvbnRyaWJ1dG9yIjogImNvbnRyaWJ1dG9yQGFwaW1sLnpvd2UiLAogICAgICAibmlja25hbWUiOiAiIiwKICAgICAgIm51bGxWYWx1ZSI6IG51bGwKICAgIH0KICB9LAogICJtZW1iZXJPZiI6IFsKICAgICJvcGVubWFpbmZyYW1lIiwKICAgICJ6b3dlIiwKICAgICJhcGltbCIKICBdCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; + public static final String SUB_USER = "oidc.username"; private static final String MF_USER = "MF_USER"; + private static final String DEFAULT_USERID_FIELD = "sub"; @BeforeEach void setup() { @@ -50,11 +67,13 @@ void setup() { provider = mock(OIDCProvider.class); mapper = mock(AuthenticationMapper.class); service = new OIDCAuthSourceService(mapper, authenticationService, provider, tokenCreationService); + service.userIdFieldPathProperty = DEFAULT_USERID_FIELD; + service.afterPropertiesSet(); } @Test void returnOIDCSourceMapper() { - assertTrue(service.getMapper().apply(TOKEN) instanceof OIDCAuthSource); + assertTrue(service.getMapper().apply(DUMMY_TOKEN) instanceof OIDCAuthSource); } @Test @@ -67,26 +86,29 @@ class GivenValidTokenTest { @Test void givenOidcTokenInRequestContext_thenReturnTheToken() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(TOKEN)); - when(authenticationService.getTokenOrigin(TOKEN)).thenReturn(AuthSource.Origin.OIDC); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(DUMMY_TOKEN)); + when(authenticationService.getTokenOrigin(DUMMY_TOKEN)).thenReturn(AuthSource.Origin.OIDC); Optional tokenResult = service.getToken(request); assertTrue(tokenResult.isPresent()); - assertEquals(TOKEN, tokenResult.get()); + assertEquals(DUMMY_TOKEN, tokenResult.get()); } @Test void givenPatTokenInRequestContext_thenReturnEmpty() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(TOKEN)); - when(authenticationService.getTokenOrigin(TOKEN)).thenReturn(AuthSource.Origin.ZOWE_PAT); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(DUMMY_TOKEN)); + when(authenticationService.getTokenOrigin(DUMMY_TOKEN)).thenReturn(AuthSource.Origin.ZOWE_PAT); Optional tokenResult = service.getToken(request); assertFalse(tokenResult.isPresent()); } @Test void givenTokenInAuthSource_thenReturnValid() { - OIDCAuthSource authSource = mockValidAuthSource(); + when(provider.isValid(TOKEN_WITH_USERNAME_FIELDS)).thenReturn(true); + OIDCAuthSource authSource = new OIDCAuthSource(TOKEN_WITH_USERNAME_FIELDS); + assertTrue(service.isValid(authSource)); + assertEquals(SUB_USER, authSource.getDistributedId()); } @Test @@ -96,13 +118,14 @@ void whenParse_thenCorrect() { AuthSource.Parsed parsedSource = service.parse(authSource); verify(mapper, times(1)).mapToMainframeUserId(authSource); + assertEquals(SUB_USER, authSource.getDistributedId()); assertEquals(MF_USER, parsedSource.getUserId()); } @Test void givenNoMapping_whenParse_thenThrowException() { - OIDCAuthSource authSource = mockValidAuthSource(); - when(mapper.mapToMainframeUserId(authSource)).thenReturn(null); + when(provider.isValid(TOKEN_WITH_USERNAME_FIELDS)).thenReturn(true); + OIDCAuthSource authSource = new OIDCAuthSource(TOKEN_WITH_USERNAME_FIELDS); assertThrows(NoMainframeIdentityException.class, () -> { service.parse(authSource); @@ -121,6 +144,7 @@ void givenValidAuthSource_thenReturnLTPAToken() { String ltpaResult = service.getLtpaToken(authSource); assertEquals(expectedToken, ltpaResult); + assertEquals(SUB_USER, authSource.getDistributedId()); } @Test @@ -131,6 +155,35 @@ void givenValidAuthSource_thenReturnJWT() { when(tokenCreationService.createJwtTokenWithoutCredentials(MF_USER)).thenReturn(expectedToken); String jwtResult = service.getJWT(authSource); assertEquals(expectedToken, jwtResult); + assertEquals(SUB_USER, authSource.getDistributedId()); + } + + @ParameterizedTest + @CsvSource({ + "email,username@oidc.org", + "org.dep.contributor, contributor@apiml.zowe"}) + void givenUserIdFieldProperty_thenReturnCorrectUsername(String preferredUsernameField, String username) { + when(provider.isValid(TOKEN_WITH_USERNAME_FIELDS)).thenReturn(true); + OIDCAuthSource authSource = new OIDCAuthSource(TOKEN_WITH_USERNAME_FIELDS); + + service.userIdFieldPathProperty = preferredUsernameField; + service.afterPropertiesSet(); + + assertTrue(service.isValid(authSource)); + assertEquals(username, authSource.getDistributedId()); + } + + @ParameterizedTest + @ValueSource(strings = { "nonexistent", "org.nonexistent.foo", "org.dep", "org.dep.nonexistent", "org.dep.nickname", "org.dep.nullValue"}) + void givenInvalidUserIdFieldProperty_thenThrowException(String preferredUsernameField) { + when(provider.isValid(TOKEN_WITH_USERNAME_FIELDS)).thenReturn(true); + OIDCAuthSource authSource = new OIDCAuthSource(TOKEN_WITH_USERNAME_FIELDS); + + service.userIdFieldPathProperty = preferredUsernameField; + service.afterPropertiesSet(); + + assertFalse(service.isValid(authSource)); + assertNull(authSource.getDistributedId()); } } @@ -139,14 +192,14 @@ class GivenDifferentAuthSourcesTest { @Test void givenJWTAuthSourceWhenValidating_thenReturnFalse() { - JwtAuthSource authSource = new JwtAuthSource(TOKEN); + JwtAuthSource authSource = new JwtAuthSource(DUMMY_TOKEN); boolean isValid = service.isValid(authSource); assertFalse(isValid); } @Test void givenJWTAuthSource_thenReturnNull() { - JwtAuthSource authSource = new JwtAuthSource(TOKEN); + JwtAuthSource authSource = new JwtAuthSource(DUMMY_TOKEN); AuthSource.Parsed parsedSource = service.parse(authSource); assertNull(parsedSource); } @@ -168,15 +221,15 @@ void whenTokenIsEmpty_thenReturnTokenInvalid() { @Test void whenIsInvalid_thenReturnTokenInvalid() { - OIDCAuthSource authSource = new OIDCAuthSource(TOKEN); - when(provider.isValid(TOKEN)).thenReturn(false); + OIDCAuthSource authSource = new OIDCAuthSource(DUMMY_TOKEN); + when(provider.isValid(DUMMY_TOKEN)).thenReturn(false); assertFalse(service.isValid(authSource)); } @Test void whenParse_thenReturnNull() { - OIDCAuthSource authSource = new OIDCAuthSource(TOKEN); - when(provider.isValid(TOKEN)).thenReturn(false); + OIDCAuthSource authSource = new OIDCAuthSource(DUMMY_TOKEN); + when(provider.isValid(DUMMY_TOKEN)).thenReturn(false); assertThrows(TokenNotValidException.class, () -> service.parse(authSource)); verify(mapper, times(0)).mapToMainframeUserId(authSource); @@ -185,28 +238,29 @@ void whenParse_thenReturnNull() { @Test void whenTokenIsExpired_thenThrow() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(TOKEN)); - when(authenticationService.getTokenOrigin(TOKEN)).thenThrow(new TokenExpireException("token expired")); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(DUMMY_TOKEN)); + when(authenticationService.getTokenOrigin(DUMMY_TOKEN)).thenThrow(new TokenExpireException("token expired")); assertThrows(TokenExpireException.class, () -> service.getToken(request)); - verify(authenticationService, times(1)).getTokenOrigin(TOKEN); + verify(authenticationService, times(1)).getTokenOrigin(DUMMY_TOKEN); } @Test void whenTokenIsNotValid_thenThrow() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(TOKEN)); - when(authenticationService.getTokenOrigin(TOKEN)).thenThrow(new TokenNotValidException("token not valid")); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(DUMMY_TOKEN)); + when(authenticationService.getTokenOrigin(DUMMY_TOKEN)).thenThrow(new TokenNotValidException("token not valid")); assertThrows(TokenNotValidException.class, () -> service.getToken(request)); - verify(authenticationService, times(1)).getTokenOrigin(TOKEN); + verify(authenticationService, times(1)).getTokenOrigin(DUMMY_TOKEN); } } private OIDCAuthSource mockValidAuthSource() { - QueryResponse tokenResponse = new QueryResponse("domain", DISTRIB_USER, new Date(), new Date(), ISSUER, Collections.emptyList(), QueryResponse.Source.OIDC); - when(authenticationService.parseJwtToken(TOKEN)).thenReturn(tokenResponse); - when(provider.isValid(TOKEN)).thenReturn(true); - return new OIDCAuthSource(TOKEN); + //No QueryResponse field is validated, so it can have dummy values to simplify mocking. + QueryResponse tokenResponse = new QueryResponse("domain", "user", new Date(), new Date(), "issuer", Collections.emptyList(), QueryResponse.Source.OIDC); + when(authenticationService.parseJwtToken(TOKEN_WITH_USERNAME_FIELDS)).thenReturn(tokenResponse); + when(provider.isValid(TOKEN_WITH_USERNAME_FIELDS)).thenReturn(true); + return new OIDCAuthSource(TOKEN_WITH_USERNAME_FIELDS); } } From 51827487c9e6a652617f9ca80cd6aa4174b2eabc Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:58:47 +0200 Subject: [PATCH 101/152] chore: enable gateway actuator endpoints in debug mode (#4305) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- apiml/src/main/resources/application.yml | 12 +++++++++--- gateway-service/src/main/resources/application.yml | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 0e0555b656..4de23e1bc9 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -314,8 +314,6 @@ logging: management: endpoint: - gateway: - access: none health: showDetails: always health: @@ -325,7 +323,7 @@ management: web: base-path: /application exposure: - include: health,info,gateway + include: health,info --- spring.config.activate.on-profile: debug @@ -357,6 +355,14 @@ logging: reactor.netty.http.client.HttpClient: DEBUG reactor.netty.http.client.HttpClientConnect: DEBUG +management: + endpoint: + gateway: + access: unrestricted + endpoints: + web: + exposure: + include: health,info,gateway --- spring.config.activate.on-profile: wiretap diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index de292d25e2..ad569e21db 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -160,8 +160,6 @@ logging: management: endpoint: - gateway: - access: none health: showDetails: always health: @@ -171,7 +169,7 @@ management: web: base-path: /application exposure: - include: health,info,gateway + include: health,info --- spring.config.activate.on-profile: debug @@ -192,6 +190,14 @@ logging: org.zowe.apiml: DEBUG reactor.netty: TRACE +management: + endpoint: + gateway: + access: unrestricted + endpoints: + web: + exposure: + include: health,info,gateway --- spring.config.activate.on-profile: wiretap From 0a02ecbb0b1cc9731662c3c9e581bb4c05e8184f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Thu, 4 Sep 2025 15:55:53 +0200 Subject: [PATCH 102/152] chore: improve debug log for oidc token user mapping (#4306) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- .../service/schema/source/OIDCAuthSourceService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java index 41521d872b..9432985e2e 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java @@ -101,10 +101,12 @@ public boolean isValid(AuthSource authSource) { private boolean extractUserId(OIDCAuthSource authSource) { try { - authSource.setDistributedId(getFieldValueFromToken(authSource.getRawSource(), userIdFieldPath)); + var userId = getFieldValueFromToken(authSource.getRawSource(), userIdFieldPath); + logger.log(MessageType.DEBUG, "UserId {} extracted from OIDC token field {}.", userId, String.join(".", userIdFieldPath)); + authSource.setDistributedId(userId); return true; } catch (TokenFormatNotValidException e) { - logger.log(MessageType.DEBUG, String.format("Cannot extract distributed id from token. Reason: %s", e.getMessage())); + logger.log(MessageType.DEBUG, "Cannot extract distributed id from OIDC token. Reason: {}", e.getMessage()); return false; } } From a68784795afed610094189dcb8cbec24b318ad60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Thu, 4 Sep 2025 18:01:01 +0200 Subject: [PATCH 103/152] fix: cherry pick apiml.gateway.servicesToDisableRetry to modulith (#4307) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- apiml-package/src/main/resources/bin/start.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 160ac487ab..e1a7959e6c 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -356,6 +356,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.gateway.registry.enabled=${ZWE_components_gateway_apiml_gateway_registry_enabled:-${ZWE_configs_apiml_gateway_registry_enabled:-false}} \ -Dapiml.gateway.registry.metadata-key-allow-list=${ZWE_components_gateway_gateway_registry_metadataKeyAllowList:-${ZWE_configs_gateway_registry_metadataKeyAllowList:-}} \ -Dapiml.gateway.servicesToLimitRequestRate=${ZWE_components_gateway_apiml_gateway_servicesToLimitRequestRate:-${ZWE_configs_apiml_gateway_servicesToLimitRequestRate:-}} \ + -Dapiml.gateway.servicesToDisableRetry=${ZWE_components_gateway_apiml_gateway_servicesToDisableRetry:-${ZWE_configs_apiml_gateway_servicesToDisableRetry:-}} \ -Dapiml.health.protected=${ZWE_components_gateway_apiml_health_protected:-${ZWE_configs_apiml_health_protected:-true}} \ -Dapiml.httpclient.ssl.enabled-protocols=${client_enabled_protocols} \ -Dapiml.internal-discovery.port=${ZWE_components_discovery_port:-${ZWE_configs_internal_discovery_port:-7553}} \ From 148ddd53780d1d697066efee61a5f33c320a467d Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 5 Sep 2025 00:43:30 +0000 Subject: [PATCH 104/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.8'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fab8568f8b..9937d4b614 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.8-SNAPSHOT +version=3.3.8 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 80b53bb9a30098936adddd9febc00224f3a3dca4 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 5 Sep 2025 00:43:31 +0000 Subject: [PATCH 105/152] [Gradle Release plugin] Create new version: 'v3.3.9-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9937d4b614..40e3fc79c9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.8 +version=3.3.9-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From a97e1f9135c0914a0c7a892d4c301952f1be9c87 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 5 Sep 2025 00:43:32 +0000 Subject: [PATCH 106/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 8a6d082236..1ba893f552 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.8-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.9-SNAPSHOT From 387c41e380231269675ad883356009c8621d7e57 Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Fri, 5 Sep 2025 04:19:13 -0400 Subject: [PATCH 107/152] chore: Update all non-major dependencies (v3.x.x) (#4297) Signed-off-by: Renovate Bot Signed-off-by: ac892247 Co-authored-by: Renovate Bot Co-authored-by: ac892247 Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- .../swagger/ApiDocRetrievalServiceLocal.java | 3 +- api-catalog-ui/frontend/package-lock.json | 41 ++-- api-catalog-ui/frontend/package.json | 12 +- gradle/versions.gradle | 22 +-- .../package-lock.json | 8 +- .../package.json | 2 +- onboarding-enabler-nodejs/package-lock.json | 2 +- onboarding-enabler-nodejs/package.json | 2 +- .../package-lock.json | 184 +++++++++--------- zowe-cli-id-federation-plugin/package.json | 10 +- 10 files changed, 144 insertions(+), 142 deletions(-) diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java index 3a22b7cb13..d7e4f7e312 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ApiDocRetrievalServiceLocal.java @@ -72,7 +72,8 @@ public ApiDocRetrievalServiceLocal( operationParser, springDocConfigProperties, springDocProviders, new SpringDocCustomizers(Optional.of(openApiCustomizers), Optional.of(groupedOpenApi.getOperationCustomizers()), - Optional.of(groupedOpenApi.getRouterOperationCustomizers()), Optional.of(groupedOpenApi.getOpenApiMethodFilters())) + Optional.of(groupedOpenApi.getRouterOperationCustomizers()), Optional.of(groupedOpenApi.getOpenApiMethodFilters()), + Optional.empty(), Optional.empty()) ) { @Override protected String getServerUrl(ServerHttpRequest serverHttpRequest, String apiDocsUrl) { diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index cce3c37b26..afa4e741ed 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -53,9 +53,9 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.91.0", + "sass": "1.92.0", "stream": "0.0.3", - "swagger-ui-react": "5.27.1", + "swagger-ui-react": "5.28.1", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -69,7 +69,7 @@ "@cfaester/enzyme-adapter-react-18": "0.8.0", "@eslint/compat": "1.3.2", "@eslint/js": "9.34.0", - "@reduxjs/toolkit": "2.8.2", + "@reduxjs/toolkit": "2.9.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.8.0", "@testing-library/react": "16.3.0", @@ -77,7 +77,7 @@ "ajv": "8.17.1", "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001737", + "caniuse-lite": "1.0.30001739", "concurrently": "9.2.1", "cors": "2.8.5", "cross-env": "7.0.3", @@ -124,7 +124,7 @@ "yaml": "2.8.1" }, "engines": { - "node": "=20.19.4", + "node": "=20.19.5", "npm": "=10.9.3" } }, @@ -5648,9 +5648,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.8.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", - "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "version": "2.9.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", "dev": true, "license": "MIT", "dependencies": { @@ -9536,9 +9536,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001737", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", - "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "version": "1.0.30001739", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "funding": [ { "type": "opencollective", @@ -27532,9 +27532,9 @@ "license": "CC0-1.0" }, "node_modules/sass": { - "version": "1.91.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.91.0.tgz", - "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", + "version": "1.92.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/sass/-/sass-1.92.0.tgz", + "integrity": "sha512-KDNI0BxgIRDAfJgzNm5wuy+4yOCIZyrUbjSpiU/JItfih+KGXAVefKL53MTml054MmBA3DDKIBMSI/7XLxZJ3A==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -29744,14 +29744,15 @@ } }, "node_modules/swagger-ui-react": { - "version": "5.27.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz", - "integrity": "sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==", + "version": "5.28.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/swagger-ui-react/-/swagger-ui-react-5.28.1.tgz", + "integrity": "sha512-JfLZvbCpsjwUnYKx+Q1YfkrgM7gD6Zb7HnO8/DwFNo5xppwC3MbL27/5MhPkmDyMZ669r6CNiJ0SALiivs7iQg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.27.1", "@scarf/scarf": "=1.4.0", "base64-js": "^1.5.1", + "buffer": "^6.0.3", "classnames": "^2.5.1", "css.escape": "1.5.1", "deep-extend": "0.6.0", @@ -29776,7 +29777,7 @@ "remarkable": "^2.0.1", "reselect": "^5.1.1", "serialize-error": "^8.1.0", - "sha.js": "^2.4.11", + "sha.js": "^2.4.12", "swagger-client": "^3.35.5", "url-parse": "^1.5.10", "xml": "=1.0.1", @@ -29784,8 +29785,8 @@ "zenscroll": "^4.0.2" }, "peerDependencies": { - "react": ">=16.8.0 <19", - "react-dom": ">=16.8.0 <19" + "react": ">=16.8.0 <20", + "react-dom": ">=16.8.0 <20" } }, "node_modules/swagger-ui-react/node_modules/immutable": { diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 8dd0cdad96..55f7df24e0 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -49,9 +49,9 @@ "redux-persist-transform-filter": "0.0.22", "redux-thunk": "3.1.0", "rxjs": "7.8.2", - "sass": "1.91.0", + "sass": "1.92.0", "stream": "0.0.3", - "swagger-ui-react": "5.27.1", + "swagger-ui-react": "5.28.1", "url": "0.11.4", "util": "0.12.5", "uuid": "10.0.0" @@ -94,11 +94,11 @@ "@testing-library/jest-dom": "6.8.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", - "@reduxjs/toolkit": "2.8.2", + "@reduxjs/toolkit": "2.9.0", "ajv": "8.17.1", "ansi-regex": "6.2.0", "body-parser": "1.20.3", - "caniuse-lite": "1.0.30001737", + "caniuse-lite": "1.0.30001739", "concurrently": "9.2.1", "cors": "2.8.5", "cross-env": "7.0.3", @@ -147,7 +147,7 @@ "overrides": { "nth-check": "2.1.1", "jsdom": "16.7.0", - "got": "14.4.7", + "got": "14.4.8", "react-error-overlay": "6.1.0", "tough-cookie": "5.1.2", "@braintree/sanitize-url": "7.1.1", @@ -167,7 +167,7 @@ }, "engines": { "npm": "=10.9.3", - "node": "=20.19.4" + "node": "=20.19.5" }, "browserslist": [ ">0.2%", diff --git a/gradle/versions.gradle b/gradle/versions.gradle index bb7e172d52..3b23a677cf 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -15,20 +15,20 @@ dependencyResolutionManagement { version('springRetry', '2.0.12') version('modulith', '1.4.3') - version('jmolecules', '2023.3.2') + version('jmolecules', '2023.3.3') version('glassfishHk2', '3.1.1') version('zosUtils', '2.0.7') - version('aws', '1.12.788') + version('aws', '1.12.789') version('awaitility', '4.3.0') version('bouncyCastle', '1.81') version('caffeine', '3.2.2') - version('checkerQual', '3.49.5') + version('checkerQual', '3.50.0') version('commonsLang3', '3.18.0') version('commonsLogging', '1.3.5') version('commonsText', '1.14.0') version('commonsIo', '2.20.0') - version('ehCache', '3.11.0') + version('ehCache', '3.11.1') version('eureka', '2.0.5') version('netflixServo', '0.13.2') version('googleErrorprone', '2.41.0') @@ -38,7 +38,7 @@ dependencyResolutionManagement { version('hamcrest', '3.0') version('httpClient4', '4.5.14') version('httpClient5', '5.5') - version('infinispan', '15.2.5.Final') + version('infinispan', '15.2.6.Final') version('jacksonCore', '2.19.2') version('jacksonDatabind', '2.19.2') version('jacksonDataformatYaml', '2.19.2') @@ -69,25 +69,25 @@ dependencyResolutionManagement { version('junitJupiter', '5.13.4') version('junitPlatform', '1.13.4') version('jxpath', '1.4.0') - version('lettuce', '6.8.0.RELEASE') + version('lettuce', '6.8.1.RELEASE') // force version in build.gradle file - compatibility with Slf4j version('log4j', '2.25.1') version('lombok', '1.18.38') - version('netty', '4.2.4.Final') + version('netty', '4.2.5.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 version('nettyReactor', '1.2.9') version('nimbusJoseJwt', '10.4.2') - version('openApiDiff', '2.1.2') + version('openApiDiff', '2.1.3') version('picocli', '4.7.7') version('reactor', '3.7.9') version('restAssured', '5.5.6') version('rhino', '1.8.0') - version('springDoc', '2.8.11') + version('springDoc', '2.8.12') version('swaggerCore', '2.2.36') - version('swaggerInflector', '2.0.13') + version('swaggerInflector', '2.0.14') version('swagger2Parser', '1.0.75') - version('swagger3Parser', '2.1.32') + version('swagger3Parser', '2.1.33') version('thymeleaf', '3.1.3.RELEASE') version('velocity', '2.4.1') version('woodstoxCore', '7.1.1') diff --git a/onboarding-enabler-nodejs-sample-app/package-lock.json b/onboarding-enabler-nodejs-sample-app/package-lock.json index 2221bce94d..d096049c3a 100644 --- a/onboarding-enabler-nodejs-sample-app/package-lock.json +++ b/onboarding-enabler-nodejs-sample-app/package-lock.json @@ -13,7 +13,7 @@ "express": "4.21.2" }, "engines": { - "node": "=20.19.4", + "node": "=20.19.5", "npm": "=10.9.3" } }, @@ -30,7 +30,7 @@ "babel-core": "6.26.3", "babel-istanbul": "0.12.2", "babel-preset-env": "1.7.0", - "chai": "5.2.1", + "chai": "5.3.3", "coveralls": "3.1.1", "eslint": "2.13.1", "eslint-config-airbnb-base": "3.0.1", @@ -43,10 +43,10 @@ "gulp-mocha": "10.0.1", "nock": "13.5.6", "sinon": "19.0.5", - "sinon-chai": "4.0.0" + "sinon-chai": "4.0.1" }, "engines": { - "node": "=20.19.4", + "node": "=20.19.5", "npm": "=10.9.3" } }, diff --git a/onboarding-enabler-nodejs-sample-app/package.json b/onboarding-enabler-nodejs-sample-app/package.json index 7a9f54a8e1..44340c98f9 100755 --- a/onboarding-enabler-nodejs-sample-app/package.json +++ b/onboarding-enabler-nodejs-sample-app/package.json @@ -23,6 +23,6 @@ }, "engines": { "npm": "=10.9.3", - "node": "=20.19.4" + "node": "=20.19.5" } } diff --git a/onboarding-enabler-nodejs/package-lock.json b/onboarding-enabler-nodejs/package-lock.json index 48da0f7c40..6ad88484a7 100644 --- a/onboarding-enabler-nodejs/package-lock.json +++ b/onboarding-enabler-nodejs/package-lock.json @@ -33,7 +33,7 @@ "sinon-chai": "4.0.1" }, "engines": { - "node": "=20.19.4", + "node": "=20.19.5", "npm": "=10.9.3" } }, diff --git a/onboarding-enabler-nodejs/package.json b/onboarding-enabler-nodejs/package.json index 3fc0e21b55..c67d7b4f39 100644 --- a/onboarding-enabler-nodejs/package.json +++ b/onboarding-enabler-nodejs/package.json @@ -53,6 +53,6 @@ }, "engines": { "npm": "=10.9.3", - "node": "=20.19.4" + "node": "=20.19.5" } } diff --git a/zowe-cli-id-federation-plugin/package-lock.json b/zowe-cli-id-federation-plugin/package-lock.json index 123f19cc73..305b244112 100644 --- a/zowe-cli-id-federation-plugin/package-lock.json +++ b/zowe-cli-id-federation-plugin/package-lock.json @@ -14,9 +14,9 @@ "devDependencies": { "@eslint/js": "9.34.0", "@types/jest": "29.5.14", - "@types/node": "20.19.11", - "@typescript-eslint/eslint-plugin": "8.41.0", - "@typescript-eslint/parser": "8.41.0", + "@types/node": "20.19.12", + "@typescript-eslint/eslint-plugin": "8.42.0", + "@typescript-eslint/parser": "8.42.0", "@zowe/cli": "8.26.2", "@zowe/cli-test-utils": "8.26.2", "@zowe/imperative": "8.26.2", @@ -38,11 +38,11 @@ "madge": "8.0.0", "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.11", + "typedoc": "0.28.12", "typescript": "5.9.2" }, "engines": { - "node": "=20.19.4", + "node": "=20.19.5", "npm": "=10.9.3" }, "peerDependencies": { @@ -777,16 +777,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.9.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", - "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", + "version": "3.12.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@gerrit0/mini-shiki/-/mini-shiki-3.12.2.tgz", + "integrity": "sha512-HKZPmO8OSSAAo20H2B3xgJdxZaLTwtlMwxg0967scnrDlPwe6j5+ULGHyIqwgTbFCn9yv/ff8CmfWZLE9YKBzA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.9.2", - "@shikijs/langs": "^3.9.2", - "@shikijs/themes": "^3.9.2", - "@shikijs/types": "^3.9.2", + "@shikijs/engine-oniguruma": "^3.12.2", + "@shikijs/langs": "^3.12.2", + "@shikijs/themes": "^3.12.2", + "@shikijs/types": "^3.12.2", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -1642,40 +1642,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.9.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", - "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", + "version": "3.12.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.2.tgz", + "integrity": "sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.9.2", + "@shikijs/types": "3.12.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.9.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/langs/-/langs-3.9.2.tgz", - "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", + "version": "3.12.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/langs/-/langs-3.12.2.tgz", + "integrity": "sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.9.2" + "@shikijs/types": "3.12.2" } }, "node_modules/@shikijs/themes": { - "version": "3.9.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/themes/-/themes-3.9.2.tgz", - "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", + "version": "3.12.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/themes/-/themes-3.12.2.tgz", + "integrity": "sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.9.2" + "@shikijs/types": "3.12.2" } }, "node_modules/@shikijs/types": { - "version": "3.9.2", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/types/-/types-3.9.2.tgz", - "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", + "version": "3.12.2", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@shikijs/types/-/types-3.12.2.tgz", + "integrity": "sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2060,9 +2060,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.19.11", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.11.tgz", - "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "version": "20.19.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@types/node/-/node-20.19.12.tgz", + "integrity": "sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -2105,17 +2105,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", - "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz", + "integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/type-utils": "8.41.0", - "@typescript-eslint/utils": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/type-utils": "8.42.0", + "@typescript-eslint/utils": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2129,7 +2129,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.41.0", + "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2158,16 +2158,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.41.0.tgz", - "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/parser/-/parser-8.42.0.tgz", + "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4" }, "engines": { @@ -2183,14 +2183,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", - "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz", + "integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.41.0", - "@typescript-eslint/types": "^8.41.0", + "@typescript-eslint/tsconfig-utils": "^8.42.0", + "@typescript-eslint/types": "^8.42.0", "debug": "^4.3.4" }, "engines": { @@ -2205,14 +2205,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", - "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz", + "integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0" + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,9 +2223,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", - "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz", + "integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==", "dev": true, "license": "MIT", "engines": { @@ -2240,15 +2240,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", - "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz", + "integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0", + "@typescript-eslint/utils": "8.42.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2278,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.41.0.tgz", - "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/types/-/types-8.42.0.tgz", + "integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==", "dev": true, "license": "MIT", "engines": { @@ -2292,16 +2292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", - "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz", + "integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.41.0", - "@typescript-eslint/tsconfig-utils": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", + "@typescript-eslint/project-service": "8.42.0", + "@typescript-eslint/tsconfig-utils": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2360,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.41.0.tgz", - "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/utils/-/utils-8.42.0.tgz", + "integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0" + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2384,13 +2384,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.41.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", - "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", + "version": "8.42.0", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz", + "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/types": "8.42.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -12546,17 +12546,17 @@ } }, "node_modules/typedoc": { - "version": "0.28.11", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.11.tgz", - "integrity": "sha512-1FqgrrUYGNuE3kImAiEDgAVVVacxdO4ZVTKbiOVDGkoeSB4sNwQaDpa8mta+Lw5TEzBFmGXzsg0I1NLRIoaSFw==", + "version": "0.28.12", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/typedoc/-/typedoc-0.28.12.tgz", + "integrity": "sha512-H5ODu4f7N+myG4MfuSp2Vh6wV+WLoZaEYxKPt2y8hmmqNEMVrH69DAjjdmYivF4tP/C2jrIZCZhPalZlTU/ipA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.9.0", + "@gerrit0/mini-shiki": "^3.12.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.8.0" + "yaml": "^2.8.1" }, "bin": { "typedoc": "bin/typedoc" @@ -13002,9 +13002,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { diff --git a/zowe-cli-id-federation-plugin/package.json b/zowe-cli-id-federation-plugin/package.json index 811d3e00d0..9d989ad6b1 100644 --- a/zowe-cli-id-federation-plugin/package.json +++ b/zowe-cli-id-federation-plugin/package.json @@ -51,9 +51,9 @@ "devDependencies": { "@eslint/js": "9.34.0", "@types/jest": "29.5.14", - "@types/node": "20.19.11", - "@typescript-eslint/eslint-plugin": "8.41.0", - "@typescript-eslint/parser": "8.41.0", + "@types/node": "20.19.12", + "@typescript-eslint/eslint-plugin": "8.42.0", + "@typescript-eslint/parser": "8.42.0", "@zowe/cli": "8.26.2", "@zowe/cli-test-utils": "8.26.2", "@zowe/imperative": "8.26.2", @@ -75,7 +75,7 @@ "madge": "8.0.0", "ts-jest": "29.4.1", "ts-node": "10.9.2", - "typedoc": "0.28.11", + "typedoc": "0.28.12", "typescript": "5.9.2" }, "overrides": { @@ -86,7 +86,7 @@ }, "engines": { "npm": "=10.9.3", - "node": "=20.19.4" + "node": "=20.19.5" }, "jest": { "modulePathIgnorePatterns": [ From 44cc7883f743f0a4181add532bae49c14ec09713 Mon Sep 17 00:00:00 2001 From: Hrishikesh Nalawade <167364362+hrishikesh-nalawade@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:41:34 +0530 Subject: [PATCH 108/152] fix: Custom Disk Health Configuration (#4269) Signed-off-by: hrishikesh-nalawade Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../CustomDiskSpaceHealthIndicator.java | 49 ++++++++++++++ .../DiskHealthConfiguration.java | 35 ++++++++++ .../CustomDiskSpaceHealthIndicatorTest.java | 66 +++++++++++++++++++ .../DiskHealthConfigurationTest.java | 43 ++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 apiml-common/src/main/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicator.java create mode 100644 apiml-common/src/main/java/org/zowe/apiml/product/compatibility/DiskHealthConfiguration.java create mode 100644 apiml-common/src/test/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicatorTest.java create mode 100644 apiml-common/src/test/java/org/zowe/apiml/product/compatibility/DiskHealthConfigurationTest.java diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicator.java b/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicator.java new file mode 100644 index 0000000000..1a1d5bd2b5 --- /dev/null +++ b/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicator.java @@ -0,0 +1,49 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.product.compatibility; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator; +import org.springframework.util.unit.DataSize; + +import java.io.File; + +/** + * Custom implementation that extends DiskSpaceHealthIndicator to prevent + * misleading disk space logs for z/OS + */ +public class CustomDiskSpaceHealthIndicator extends DiskSpaceHealthIndicator { + + private final File path; + private final DataSize threshold; + + /** + * Create a custom DiskSpaceHealthIndicator that overrides the default behavior + * to prevent misleading logs for z/OS + */ + public CustomDiskSpaceHealthIndicator(File path, DataSize threshold) { + super(path, threshold); + this.path = path; + this.threshold = threshold; + } + + @Override + protected void doHealthCheck(Health.Builder builder) { + // Always reporting UP status without checking disk space on z/OS + builder.up() + .withDetail("total", "not monitored") + .withDetail("free", "not monitored") + .withDetail("threshold", this.threshold.toBytes()) + .withDetail("path", this.path.getAbsolutePath()) + .withDetail("exists", this.path.exists()) + .withDetail("note", "Disk space monitoring disabled"); + } +} diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/DiskHealthConfiguration.java b/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/DiskHealthConfiguration.java new file mode 100644 index 0000000000..dd13feb663 --- /dev/null +++ b/apiml-common/src/main/java/org/zowe/apiml/product/compatibility/DiskHealthConfiguration.java @@ -0,0 +1,35 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.product.compatibility; + +import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties; +import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Configuration class that replaces the default DiskSpaceHealthIndicator + */ +@Configuration +@AutoConfigureBefore(DiskSpaceHealthContributorAutoConfiguration.class) +@EnableConfigurationProperties(DiskSpaceHealthIndicatorProperties.class) +public class DiskHealthConfiguration { + + @Bean + @Primary + public DiskSpaceHealthIndicator diskSpaceHealthIndicator(DiskSpaceHealthIndicatorProperties properties) { + return new CustomDiskSpaceHealthIndicator(properties.getPath(), properties.getThreshold()); + } +} diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicatorTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicatorTest.java new file mode 100644 index 0000000000..9574d7ab57 --- /dev/null +++ b/apiml-common/src/test/java/org/zowe/apiml/product/compatibility/CustomDiskSpaceHealthIndicatorTest.java @@ -0,0 +1,66 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.product.compatibility; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.util.unit.DataSize; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CustomDiskSpaceHealthIndicatorTest { + + private File testPath; + private DataSize testThreshold; + private CustomDiskSpaceHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + testPath = new File(System.getProperty("java.io.tmpdir")); + testThreshold = DataSize.ofMegabytes(10); + healthIndicator = new CustomDiskSpaceHealthIndicator(testPath, testThreshold); + } + + @Nested + class WhenCheckingHealth { + @Test + void shouldAlwaysReportUpStatus() { + + Health.Builder builder = new Health.Builder(); + // When + healthIndicator.doHealthCheck(builder); + Health health = builder.build(); + // Then + assertEquals(Status.UP, health.getStatus()); + } + + @Test + void shouldIncludeExpectedDetails() { + + Health.Builder builder = new Health.Builder(); + // When + healthIndicator.doHealthCheck(builder); + Health health = builder.build(); + // Then + assertEquals("not monitored", health.getDetails().get("total")); + assertEquals("not monitored", health.getDetails().get("free")); + assertEquals(testThreshold.toBytes(), health.getDetails().get("threshold")); + assertEquals(testPath.getAbsolutePath(), health.getDetails().get("path")); + assertEquals(testPath.exists(), health.getDetails().get("exists")); + assertEquals("Disk space monitoring disabled", health.getDetails().get("note")); + } + } +} diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/compatibility/DiskHealthConfigurationTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/compatibility/DiskHealthConfigurationTest.java new file mode 100644 index 0000000000..02ffc3aa35 --- /dev/null +++ b/apiml-common/src/test/java/org/zowe/apiml/product/compatibility/DiskHealthConfigurationTest.java @@ -0,0 +1,43 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.product.compatibility; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties; +import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator; +import org.springframework.util.unit.DataSize; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class DiskHealthConfigurationTest { + + @Nested + class WhenCreatingDiskSpaceHealthIndicator { + @Test + void shouldReturnCustomImplementation() { + + DiskHealthConfiguration configuration = new DiskHealthConfiguration(); + DiskSpaceHealthIndicatorProperties properties = new DiskSpaceHealthIndicatorProperties(); + properties.setPath(new File(System.getProperty("java.io.tmpdir"))); + properties.setThreshold(DataSize.ofMegabytes(100)); + + // When + DiskSpaceHealthIndicator indicator = configuration.diskSpaceHealthIndicator(properties); + + // Then + assertNotNull(indicator); + assertTrue(indicator instanceof CustomDiskSpaceHealthIndicator); + } + } +} From 2e08dbfdd42b7272e2878ff3caa16ffacbab4601 Mon Sep 17 00:00:00 2001 From: Hrishikesh Nalawade <167364362+hrishikesh-nalawade@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:53:46 +0530 Subject: [PATCH 109/152] fix: API ML services log cleanup (#4284) Signed-off-by: hrishikesh-nalawade Signed-off-by: Gowtham Selvaraj --- .../apicatalog/config/ApiLayerServices.java | 50 +++++++++++++++ .../apicatalog/swagger/ContainerService.java | 9 ++- .../resources/apicatalog-log-messages.yml | 4 +- .../apicatalog/util/ApiLayerServicesTest.java | 61 +++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/ApiLayerServices.java create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApiLayerServicesTest.java diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/ApiLayerServices.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/ApiLayerServices.java new file mode 100644 index 0000000000..c4c898435f --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/ApiLayerServices.java @@ -0,0 +1,50 @@ +/* + * 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.apicatalog.config; + + +/** + * Enum representing internal API Layer services + * Used to identify services that are part of API Layer itself + */ +public enum ApiLayerServices { + DISCOVERY("discovery"), + GATEWAY("gateway"), + APIML("apiml"), + ZAAS("zaas"), + API_CATALOG("apicatalog"), + CACHING_SERVICE("cachingservice"), + IBMZOSMF("ibmzosmf"), + ZSS("zss"); + + private final String serviceId; + + ApiLayerServices(String serviceId) { + this.serviceId = serviceId; + } + + public String getServiceId() { + return serviceId; + } + + public static boolean isApiLayerService(String serviceId) { + if (serviceId == null || serviceId.trim().isEmpty()) { + return false; + } + String normalizedServiceId = serviceId.toLowerCase().trim(); + for (ApiLayerServices service : ApiLayerServices.values()) { + if (service.serviceId.equals(normalizedServiceId)) { + return true; + } + } + return false; + } +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java index d5448ce8dc..6f578cb7a5 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/swagger/ContainerService.java @@ -20,6 +20,7 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; import org.springframework.stereotype.Service; +import org.zowe.apiml.apicatalog.config.ApiLayerServices; import org.zowe.apiml.apicatalog.model.APIContainer; import org.zowe.apiml.apicatalog.model.APIService; import org.zowe.apiml.apicatalog.model.CustomStyleConfig; @@ -143,7 +144,9 @@ private String getInstanceHomePageUrl(ServiceInstance serviceInstance) { routes, isClientAttlsEnabled); } catch (URLTransformationException | IllegalArgumentException e) { - apimlLog.log("org.zowe.apiml.apicatalog.homePageTransformFailed", serviceId, e.getMessage()); + if (!ApiLayerServices.isApiLayerService(serviceId)) { + apimlLog.log("org.zowe.apiml.apicatalog.homePageTransformFailed", serviceId, e.getMessage()); + } } } @@ -171,7 +174,9 @@ private String getApiBasePath(ServiceInstance serviceInstance) { getHomePageUrl(serviceInstance), routes); } catch (URLTransformationException e) { - apimlLog.log("org.zowe.apiml.apicatalog.getApiBasePathFailed", serviceInstance.getServiceId(), e.getMessage()); + if (!ApiLayerServices.isApiLayerService(serviceInstance.getServiceId())) { + apimlLog.log("org.zowe.apiml.apicatalog.getApiBasePathFailed", serviceInstance.getServiceId(), e.getMessage()); + } } } return ""; diff --git a/api-catalog-services/src/main/resources/apicatalog-log-messages.yml b/api-catalog-services/src/main/resources/apicatalog-log-messages.yml index 43271763b5..77ec33b6e2 100644 --- a/api-catalog-services/src/main/resources/apicatalog-log-messages.yml +++ b/api-catalog-services/src/main/resources/apicatalog-log-messages.yml @@ -100,7 +100,7 @@ messages: - key: org.zowe.apiml.apicatalog.homePageTransformFailed number: ZWEAC705 - type: WARNING + type: DEBUG text: "The home page url for service %s was not transformed. %s" reason: "The home page url for service was not transformed. The original url will be used." action: "Refer to the specific printed message. Possible causes include:\n @@ -125,7 +125,7 @@ messages: - key: org.zowe.apiml.apicatalog.getApiBasePathFailed number: ZWEAC708 - type: ERROR + type: DEBUG text: "The API base path for service %s was not retrieved. %s" reason: "The API base path for service was not retrieved. An empty path will be used." action: "Refer to the specific printed message. Possible causes include:\n diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApiLayerServicesTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApiLayerServicesTest.java new file mode 100644 index 0000000000..1bf3abe987 --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ApiLayerServicesTest.java @@ -0,0 +1,61 @@ +/* + * 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.apicatalog.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.zowe.apiml.apicatalog.config.ApiLayerServices; +import static org.junit.jupiter.api.Assertions.*; + +class ApiLayerServicesTest { + + @Test + void testEnumConstruction_shouldHaveCorrectServiceIds() { + assertEquals("discovery", ApiLayerServices.DISCOVERY.getServiceId()); + assertEquals("gateway", ApiLayerServices.GATEWAY.getServiceId()); + assertEquals("apiml", ApiLayerServices.APIML.getServiceId()); + assertEquals("zaas", ApiLayerServices.ZAAS.getServiceId()); + assertEquals("apicatalog", ApiLayerServices.API_CATALOG.getServiceId()); + assertEquals("cachingservice", ApiLayerServices.CACHING_SERVICE.getServiceId()); + assertEquals("ibmzosmf", ApiLayerServices.IBMZOSMF.getServiceId()); + assertEquals("zss", ApiLayerServices.ZSS.getServiceId()); + } + + @Test + void testToString_shouldReturnEnumConstantName() { + assertEquals("DISCOVERY", ApiLayerServices.DISCOVERY.toString()); + assertEquals("GATEWAY", ApiLayerServices.GATEWAY.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"discovery", "DISCOVERY", "Discovery", " discovery ", "DiScOvErY"}) + void testIsApiLayerService_withDiscoveryVariants_shouldReturnTrue(String input) { + boolean result = ApiLayerServices.isApiLayerService(input); + assertTrue(result, "Should recognize '" + input + "' as an API Layer service"); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", " "}) + void testIsApiLayerService_withNullOrEmptyOrBlank_shouldReturnFalse(String input) { + boolean result = ApiLayerServices.isApiLayerService(input); + assertFalse(result, "Should not recognize null or empty string as an API Layer service"); + } + + @ParameterizedTest + @ValueSource(strings = {"unknown", "discoveryservice", "gateways", "api", "catalog", "caching", "notaservice", "external-service"}) + void testIsApiLayerService_withNonApiLayerServices_shouldReturnFalse(String input) { + boolean result = ApiLayerServices.isApiLayerService(input); + assertFalse(result, "Should not recognize '" + input + "' as an API Layer service"); + } +} From 742ba4ece9f5fa331981e02e66bcf95224e0f2dd Mon Sep 17 00:00:00 2001 From: zowe-robot <42546701+zowe-robot@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:53:18 -0400 Subject: [PATCH 110/152] Automatic update for the Changelog for v3.3.0 release (#4230) Signed-off-by: Zowe Robot Signed-off-by: Richard Salac Co-authored-by: Zowe Robot Co-authored-by: Richard Salac Co-authored-by: Jakub Balhar Signed-off-by: Gowtham Selvaraj --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6964645cc..1737202e07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to the Zowe API Mediation Layer package will be documented in this file. +## `3.3.0 (2025-08-18)` + +* Feature: New configuration property **`apiml.security.forwardHeader.trustedProxies`** added to specify the regular expression pattern used to identify trusted proxies from which `X-Forwarded-*` headers are accepted and forwarded. Mitigates CVE-2025-41235. (#4171) ([ff8c81d](https://github.com/zowe/api-layer/commit/ff8c81d)), closes [#4171](https://github.com/zowe/api-layer/pull/4171) +* Feature: Support independent response time route setting (#3981) ([aba1b0f](https://github.com/zowe/api-layer/commit/aba1b0f)), closes [#3981](https://github.com/zowe/api-layer/issues/3981) +* Feature: Apiml Spring-Modulith based module with ZAAS service (#4108) ([738915e](https://github.com/zowe/api-layer/commit/738915e)), closes [#4108](https://github.com/zowe/api-layer/issues/4108) +* Feature: Add check of certificate signing algorithm in Certificate Analyzer tool (#4121) ([39274e7](https://github.com/zowe/api-layer/commit/39274e7)), closes [#4121](https://github.com/zowe/api-layer/issues/4121) +* Feature: Apiml Spring-Modulith based module with Gateway and Discovery services (#4051) ([47c3e60](https://github.com/zowe/api-layer/commit/47c3e60)), closes [#4051](https://github.com/zowe/api-layer/issues/4051) +* Feature: Certificate validation improvements (#4017) ([b45747f](https://github.com/zowe/api-layer/commit/b45747f)), closes [#4017](https://github.com/zowe/api-layer/issues/4017) +* Feature: Onboarding Python Enabler (#4068) ([3f966f3](https://github.com/zowe/api-layer/commit/3f966f3)), closes [#4068](https://github.com/zowe/api-layer/issues/4068) +* Feature: Eureka client connection timeout (#4045) ([0e3c116](https://github.com/zowe/api-layer/commit/0e3c116)), closes [#4045](https://github.com/zowe/api-layer/issues/4045) + + +* Bugfix: Gateway returns empty auth keys from z/OSMF when **`apiml.security.auth.zosmf.jwtAutoconfiguration`** is set to **`jwt`**. (#4108) ([738915e](https://github.com/zowe/api-layer/commit/738915e)), closes [#4092](https://github.com/zowe/api-layer/issues/4092) +* Bugfix: Update start.sh settings for caching service (#4226) ([328a4c6](https://github.com/zowe/api-layer/commit/328a4c6)), closes [#4226](https://github.com/zowe/api-layer/issues/4226) +* Bugfix: API ML startup message in modulith mode (#4216) ([fbd3356](https://github.com/zowe/api-layer/commit/fbd3356)), closes [#4216](https://github.com/zowe/api-layer/issues/4216) +* Bugfix: Fix SAF auth check in non-modulith (#4212) ([b2ddf07](https://github.com/zowe/api-layer/commit/b2ddf07)), closes [#4212](https://github.com/zowe/api-layer/issues/4212) +* Bugfix: Unresponsive eureka (#4223) ([4e28a83](https://github.com/zowe/api-layer/commit/4e28a83)), closes [#4223](https://github.com/zowe/api-layer/issues/4223) +* Bugfix: Modulith mode does not distribute logout (#4191) ([82b96f5](https://github.com/zowe/api-layer/commit/82b96f5)), closes [#4191](https://github.com/zowe/api-layer/issues/4191) +* Bugfix: Disable infinispan diagnostics by default (#4157) ([d1b6972](https://github.com/zowe/api-layer/commit/d1b6972)), closes [#4157](https://github.com/zowe/api-layer/issues/4157) +* Bugfix: Fix obtaining public keys if there is unsupported type of key (#4154) ([a7d3700](https://github.com/zowe/api-layer/commit/a7d3700)), closes [#4154](https://github.com/zowe/api-layer/issues/4154) +* Bugfix: Generate git properties file before release build (#4173) ([2ce6e5b](https://github.com/zowe/api-layer/commit/2ce6e5b)), closes [#4173](https://github.com/zowe/api-layer/issues/4173) +* Bugfix: Release build without cache (#4179) ([5898329](https://github.com/zowe/api-layer/commit/5898329)), closes [#4179](https://github.com/zowe/api-layer/issues/4179) +* Bugfix: Remove duplicate log messages (#4147) ([d57f9c0](https://github.com/zowe/api-layer/commit/d57f9c0)), closes [#4147](https://github.com/zowe/api-layer/issues/4147) +* Bugfix: Fix detection of connection issue (#4142) ([e33d27a](https://github.com/zowe/api-layer/commit/e33d27a)), closes [#4142](https://github.com/zowe/api-layer/issues/4142) +* Bugfix: Set memory limit for javap (#4141) ([fcb021f](https://github.com/zowe/api-layer/commit/fcb021f)), closes [#4141](https://github.com/zowe/api-layer/issues/4141) +* Bugfix: Config change for Gateway Endlessly Spamming Issue (#4095) ([08bd675](https://github.com/zowe/api-layer/commit/08bd675)), closes [#4095](https://github.com/zowe/api-layer/issues/4095) +* Bugfix: Adding HSTS header when AT-TLS enabled V3 (#4052) ([143d73f](https://github.com/zowe/api-layer/commit/143d73f)), closes [#4052](https://github.com/zowe/api-layer/issues/4052) +* Bugfix: Non-strict hostname verification in Jetty client for WebSockets (#4073) ([a4768e2](https://github.com/zowe/api-layer/commit/a4768e2)), closes [#4073](https://github.com/zowe/api-layer/issues/4073) +* Bugfix: Fix SSO issue in the API Catalog (#4070) ([fb52fa6](https://github.com/zowe/api-layer/commit/fb52fa6)), closes [#4070](https://github.com/zowe/api-layer/issues/4070) + ## `APIML 3.2.3 / Zowe 3.2.0 (2025-04-16)` * Feature: v3 with Java 21 (#4028) ([59ea8ee](https://github.com/zowe/api-layer/commit/59ea8ee)), closes [#4028](https://github.com/zowe/api-layer/issues/4028) From 0c35c6f929cd00ff77ed12ff671428febdc0d1f8 Mon Sep 17 00:00:00 2001 From: pketki Date: Wed, 10 Sep 2025 14:31:24 -0400 Subject: [PATCH 111/152] feat: (enabler-nodejs) Expose Eureka class directly to pass config (#4311) Signed-off-by: pketki Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- onboarding-enabler-nodejs/README.md | 45 +++++++++++++++++++ onboarding-enabler-nodejs/package-lock.json | 4 +- onboarding-enabler-nodejs/package.json | 4 +- onboarding-enabler-nodejs/src/EurekaClient.js | 19 +++++--- onboarding-enabler-nodejs/src/index.js | 2 + .../test/integration.test.js | 2 +- 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/onboarding-enabler-nodejs/README.md b/onboarding-enabler-nodejs/README.md index c0b5637263..189aafd819 100644 --- a/onboarding-enabler-nodejs/README.md +++ b/onboarding-enabler-nodejs/README.md @@ -108,5 +108,50 @@ Below is an example of the configuration. keyPassword: password ``` +Alternatively, you can also pass the config as a json to the client: + ```js + import { EurekaClient as Eureka } from '@zowe/apiml-onboarding-enabler-nodejs' + const client = new Eureka({ + eureka: { + ssl: true, + host: localhost, + port: 10011, + servicePath: '/eureka/apps/', + maxRetries: 2, + registryFetchInterval: 30000, + fetchRegistry: false, + heartbeatInterval: 60000 + }, + instance: { + app: hwexpress, + instanceId: localhost:hwexpress:10020, + hostName: 'localhost', + ipAddr: '127.0.0.1', + homePageUrl: https://localhost:10020/, + secureVipAddress: hwexpress, + port: { + $: 10020, + '@enabled': false + }, + securePort: { + $: 10020, + '@enabled': true + }, + dataCenterInfo: { + '@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo', + name: 'MyOwn' + }, + metadata: { + 'apiml.routes.ui-v1.gatewayUrl': 'ui', + 'apiml.routes.ui-v1.serviceUrl': '/', + 'apiml.routes.ws-v1.gatewayUrl': 'ws/ui', + 'apiml.routes.ws-v1.serviceUrl': '/' + } + }, + requestMiddleware: (requestOpts, done) => { + done(Object.assign(requestOpts, tlsOptions)); + } + }); + ``` 4. Start your Node.js service and verify that it registers to the Zowe API Mediation Layer. diff --git a/onboarding-enabler-nodejs/package-lock.json b/onboarding-enabler-nodejs/package-lock.json index 6ad88484a7..8a82817c18 100644 --- a/onboarding-enabler-nodejs/package-lock.json +++ b/onboarding-enabler-nodejs/package-lock.json @@ -33,8 +33,8 @@ "sinon-chai": "4.0.1" }, "engines": { - "node": "=20.19.5", - "npm": "=10.9.3" + "node": ">=20.19.2", + "npm": ">=10.9.2" } }, "node_modules/@babel/code-frame": { diff --git a/onboarding-enabler-nodejs/package.json b/onboarding-enabler-nodejs/package.json index c67d7b4f39..680ab2dd88 100644 --- a/onboarding-enabler-nodejs/package.json +++ b/onboarding-enabler-nodejs/package.json @@ -52,7 +52,7 @@ ] }, "engines": { - "npm": "=10.9.3", - "node": "=20.19.5" + "npm": ">=10.9.2", + "node": ">=20.19.2" } } diff --git a/onboarding-enabler-nodejs/src/EurekaClient.js b/onboarding-enabler-nodejs/src/EurekaClient.js index d12068290e..53079f2f95 100644 --- a/onboarding-enabler-nodejs/src/EurekaClient.js +++ b/onboarding-enabler-nodejs/src/EurekaClient.js @@ -90,14 +90,21 @@ export default class Eureka extends EventEmitter { const cwd = config.cwd || process.cwd(); const env = process.env.EUREKA_ENV || process.env.NODE_ENV || 'development'; - const filename = config.filename || 'eureka-client'; + // Config can either be passed via config file or as json + // Check if config file and cwd was provided + if (cwd) { + const filename = config.filename || 'eureka-client'; - // Load in the configuration files: - const defaultYml = getYaml(path.join(cwd, `${filename}.yml`)); - const envYml = getYaml(path.join(cwd, `${filename}-${env}.yml`)); + // Load in the configuration files: + const defaultYml = getYaml(path.join(cwd, `${filename}.yml`)); + const envYml = getYaml(path.join(cwd, `${filename}-${env}.yml`)); - // apply config overrides in appropriate order - this.config = merge({}, defaultConfig, defaultYml, envYml, config); + // apply config overrides in appropriate order + this.config = merge({}, defaultConfig, defaultYml, envYml, config); + } else { + // config was provided as JSON + this.config = config; + } // Validate the provided the values we need: this.validateConfig(this.config); diff --git a/onboarding-enabler-nodejs/src/index.js b/onboarding-enabler-nodejs/src/index.js index a1a12f9d01..1c56cca8f1 100644 --- a/onboarding-enabler-nodejs/src/index.js +++ b/onboarding-enabler-nodejs/src/index.js @@ -66,3 +66,5 @@ export function unregisterFromEureka() { console.log('\nUnregistering the service from Eureka...'); client.stop(); } + +export const EurekaClient = Eureka; diff --git a/onboarding-enabler-nodejs/test/integration.test.js b/onboarding-enabler-nodejs/test/integration.test.js index 325f784f5e..8a0d55f343 100644 --- a/onboarding-enabler-nodejs/test/integration.test.js +++ b/onboarding-enabler-nodejs/test/integration.test.js @@ -32,7 +32,7 @@ * SOFTWARE. */ -import Eureka from '../src/EurekaClient.js'; +import { EurekaClient as Eureka } from '../src'; import { expect } from 'chai'; import fs from 'fs'; From ea4d13869f1d4ec65e3129bf416506bd4dc777ee Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 12 Sep 2025 00:43:36 +0000 Subject: [PATCH 112/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.9'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 40e3fc79c9..2380d14bfe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.9-SNAPSHOT +version=3.3.9 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 130c6c439d545c082c7f403dedf0199644c69b65 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 12 Sep 2025 00:43:38 +0000 Subject: [PATCH 113/152] [Gradle Release plugin] Create new version: 'v3.3.10-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2380d14bfe..f2b537b978 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.9 +version=3.3.10-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From f7d2c87cc7a3389bff513400d65a18d01fe4f095 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 12 Sep 2025 00:43:39 +0000 Subject: [PATCH 114/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 1ba893f552..4d70c5153b 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.9-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.10-SNAPSHOT From 5ce00f354261f03d448faad665143fed9a97aa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Tue, 16 Sep 2025 10:00:39 +0200 Subject: [PATCH 115/152] Support multi-value OIDC claims for userId mapping (#4308) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- .../zowe/apiml/util/HttpClientMockHelper.java | 9 +- .../security/mapping/OIDCExternalMapper.java | 77 +++------ .../security/mapping/OIDCMapperHelper.java | 92 +++++++++++ .../security/mapping/OIDCNativeMapper.java | 54 +------ .../apiml/zaas/security/service/JwtUtils.java | 66 +++++--- .../service/schema/source/OIDCAuthSource.java | 4 +- .../schema/source/OIDCAuthSourceService.java | 8 +- .../mapping/OIDCExternalMapperTest.java | 148 ++++++++++++++---- .../mapping/OIDCNativeMapperTest.java | 100 ++++++++++-- .../zaas/security/service/JwtUtilsTest.java | 33 ++-- .../source/OIDCAuthSourceServiceTest.java | 17 +- .../service/token/OIDCTokenProviderTest.java | 44 ++---- .../org/zowe/apiml/zaas/utils/JWTUtils.java | 69 +++++++- 13 files changed, 489 insertions(+), 232 deletions(-) create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCMapperHelper.java diff --git a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java index 491333c969..223c09eae7 100644 --- a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java +++ b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/HttpClientMockHelper.java @@ -35,9 +35,12 @@ public static OngoingStubbing whenExecuteThenThrow(CloseableHttpClient httpCl } @SneakyThrows - public static OngoingStubbing mockExecuteWithResponse(CloseableHttpClient httpClientMock, ClassicHttpResponse responseMock) { - return Mockito.when(httpClientMock.execute(ArgumentMatchers.any(ClassicHttpRequest.class), ArgumentMatchers.any(HttpClientResponseHandler.class))) - .thenAnswer((InvocationOnMock invocation) -> invokeResponseHandler(invocation, responseMock)); + public static OngoingStubbing mockExecuteWithResponse(CloseableHttpClient httpClientMock, ClassicHttpResponse... responseMocks) { + var stubbing = Mockito.when(httpClientMock.execute(ArgumentMatchers.any(ClassicHttpRequest.class), ArgumentMatchers.any(HttpClientResponseHandler.class))); + for (ClassicHttpResponse responseMock : responseMocks) { + stubbing = stubbing.thenAnswer((InvocationOnMock invocation) -> invokeResponseHandler(invocation, responseMock)); + } + return stubbing; } @SneakyThrows diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapper.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapper.java index ec0a716c01..32f05a6e14 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapper.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapper.java @@ -11,7 +11,6 @@ package org.zowe.apiml.zaas.security.mapping; import com.fasterxml.jackson.core.JsonProcessingException; -import jakarta.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.core5.http.io.entity.StringEntity; @@ -19,81 +18,45 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.zowe.apiml.message.core.MessageType; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.zaas.security.mapping.model.MapperResponse; import org.zowe.apiml.zaas.security.mapping.model.OIDCRequest; import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; -import org.zowe.apiml.zaas.security.service.schema.source.OIDCAuthSource; - -import static org.zowe.apiml.zaas.security.mapping.model.MapperResponse.OIDC_FAILED_MESSAGE_KEY; @Component("oidcMapper") @ConditionalOnExpression("'${apiml.security.oidc.enabled:false}' == 'true' && '${apiml.security.useInternalMapper:false}' == 'false'") public class OIDCExternalMapper extends ExternalMapper implements AuthenticationMapper { - @Value("${apiml.security.oidc.registry:}") - protected String registry; - - @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); - - protected boolean isConfigError = false; - - @PostConstruct - private void postConstruct() { - if (StringUtils.isEmpty(registry)) { - isConfigError = true; - apimlLog.log("org.zowe.apiml.security.common.OIDCConfigError"); - } - } + OIDCMapperHelper mapperHelper; public OIDCExternalMapper(@Value("${apiml.security.oidc.identityMapperUrl:}") String mapperUrl, @Value("${apiml.security.oidc.identityMapperUser:}") String mapperUser, @Qualifier("secureHttpClientWithoutKeystore") CloseableHttpClient secureHttpClientWithoutKeystore, TokenCreationService tokenCreationService, - AuthConfigurationProperties authConfigurationProperties) { + AuthConfigurationProperties authConfigurationProperties, + OIDCMapperHelper mapperHelper) { super(mapperUrl, mapperUser, secureHttpClientWithoutKeystore, tokenCreationService, authConfigurationProperties); + this.mapperHelper = mapperHelper; } public String mapToMainframeUserId(AuthSource authSource) { - if (isConfigError) { - apimlLog.log("org.zowe.apiml.security.common.OIDCConfigError"); - return null; - } - - if (!(authSource instanceof OIDCAuthSource)) { - apimlLog.log(MessageType.DEBUG,"The used authentication source type is {} and not OIDC", authSource.getType()); - return null; - } - - final String distributedId = ((OIDCAuthSource) authSource).getDistributedId(); - if (StringUtils.isEmpty(distributedId)) { - apimlLog.log(OIDC_FAILED_MESSAGE_KEY, - "OIDC token is missing the distributed ID. Make sure your distributed identity provider is" + - " properly configured."); - return null; - } - OIDCRequest oidcRequest = new OIDCRequest(distributedId, registry); - try { - StringEntity payload = new StringEntity(objectMapper.writeValueAsString(oidcRequest)); - MapperResponse mapperResponse = callExternalMapper(payload); - - if (mapperResponse != null && mapperResponse.isOIDCResultValid()) { - String userId = mapperResponse.getUserId().trim(); - return StringUtils.isNotEmpty(userId) ? userId : null; + return mapperHelper.mapToMainframeUserId(authSource, distributedId -> { + OIDCRequest oidcRequest = new OIDCRequest(distributedId, mapperHelper.registry); + try { + StringEntity payload = new StringEntity(objectMapper.writeValueAsString(oidcRequest)); + MapperResponse mapperResponse = callExternalMapper(payload); + + if (mapperResponse != null && mapperResponse.isOIDCResultValid()) { + String userId = mapperResponse.getUserId().trim(); + return StringUtils.isNotEmpty(userId) ? userId : null; + } + } catch (JsonProcessingException e) { + apimlLog.log("org.zowe.apiml.security.common.OIDCMappingError", + "Unable to generate JSON payload for identity mapping request", + e.getMessage()); } - - } catch (JsonProcessingException e) { - apimlLog.log("org.zowe.apiml.security.common.OIDCMappingError", - "Unable to generate JSON payload for identity mapping request", - e.getMessage()); - } - - return null; + return null; + }); } - } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCMapperHelper.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCMapperHelper.java new file mode 100644 index 0000000000..dcbaea278f --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCMapperHelper.java @@ -0,0 +1,92 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas.security.mapping; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Component; +import org.zowe.apiml.message.core.MessageType; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; +import org.zowe.apiml.zaas.security.service.schema.source.OIDCAuthSource; + +import java.util.function.UnaryOperator; + +import static org.zowe.apiml.zaas.security.mapping.model.MapperResponse.OIDC_FAILED_MESSAGE_KEY; + +@Component +@ConditionalOnBean(name = "oidcMapper") +public class OIDCMapperHelper implements InitializingBean { + + @Value("${apiml.security.oidc.registry:}") + protected String registry; + + @InjectApimlLogger + private final ApimlLogger apimlLog = ApimlLogger.empty(); + + protected boolean isConfigError = false; + + @Override + public void afterPropertiesSet() { + if (StringUtils.isBlank(registry)) { + isConfigError = true; + apimlLog.log("org.zowe.apiml.security.common.OIDCConfigError"); + } + } + + /** + * Maps the authSource distribution id to a mainframe user. The method validates OIDC mapping configuration and authSource and if these are valid, invokes the mapper. + * @param authSource OidcAuthSource with the distributed id to map + * @param mapper the mapper function with the actual mapping logic, accepts the authSource distributed id and returns a mainframe user id on success or null otherwise + * @return returns result of the mapper or null on validation failure + */ + + public String mapToMainframeUserId(AuthSource authSource, UnaryOperator mapper) { + + if (isConfigError) { + apimlLog.log("org.zowe.apiml.security.common.OIDCConfigError"); + return null; + } + + if (mapper == null) { + apimlLog.log(MessageType.ERROR, "OIDC token mapping invoked but no mapper provided"); + return null; + } + + if (!(authSource instanceof OIDCAuthSource)) { + apimlLog.log(MessageType.DEBUG, "The used authentication source type is {} and not OIDC", authSource.getType()); + return null; + } + + var distributedIds = ((OIDCAuthSource) authSource).getDistributedId(); + if (distributedIds == null || distributedIds.isEmpty()) { + apimlLog.log(OIDC_FAILED_MESSAGE_KEY, + "OIDC token is missing the distributed ID. Make sure your distributed identity provider is" + + " properly configured."); + return null; + } + + for (String distributedId : distributedIds) { + if (StringUtils.isNotBlank(distributedId)) { + var mainframeUserId = mapper.apply(distributedId); + if (StringUtils.isNotBlank(mainframeUserId)) { + return mainframeUserId; + } + } + } + + apimlLog.log(MessageType.DEBUG, "No mainframe user mapping found for distributed ids {}", distributedIds); + return null; + } +} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapper.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapper.java index 553c484d26..8e8b185150 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapper.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapper.java @@ -12,20 +12,11 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; -import org.zowe.apiml.zaas.security.service.schema.source.OIDCAuthSource; -import org.zowe.apiml.message.core.MessageType; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.commons.usermap.MapperResponse; -import jakarta.annotation.PostConstruct; - -import static org.zowe.apiml.zaas.security.mapping.model.MapperResponse.OIDC_FAILED_MESSAGE_KEY; - @RequiredArgsConstructor @Component("oidcMapper") @ConditionalOnExpression("'${apiml.security.oidc.enabled:false}' == 'true' && '${apiml.security.useInternalMapper:false}' == 'true'") @@ -33,47 +24,16 @@ public class OIDCNativeMapper implements AuthenticationMapper { private final NativeMapperWrapper nativeMapper; - @Value("${apiml.security.oidc.registry:}") - protected String registry; - - @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); - - protected boolean isConfigError = false; - - @PostConstruct - private void postConstruct() { - if (StringUtils.isEmpty(registry)) { - isConfigError = true; - apimlLog.log("org.zowe.apiml.security.common.OIDCConfigError"); - } - } + private final OIDCMapperHelper mapperHelper; @Override public String mapToMainframeUserId(AuthSource authSource) { - if (isConfigError) { - apimlLog.log("org.zowe.apiml.security.common.OIDCConfigError"); + return mapperHelper.mapToMainframeUserId(authSource, distributedId -> { + MapperResponse response = nativeMapper.getUserIDForDN(distributedId, mapperHelper.registry); + if (response.getRc() == 0 && StringUtils.isNotBlank(response.getUserId())) { + return response.getUserId(); + } return null; - } - - if (!(authSource instanceof OIDCAuthSource)) { - apimlLog.log(MessageType.DEBUG, "The used authentication source type is {} and not OIDC", authSource.getType()); - return null; - } - - final String distributedId = ((OIDCAuthSource) authSource).getDistributedId(); - if (StringUtils.isEmpty(distributedId)) { - apimlLog.log(OIDC_FAILED_MESSAGE_KEY, - "OIDC token is missing the distributed ID. Make sure your distributed identity provider is" + - " properly configured."); - return null; - } - - MapperResponse response = nativeMapper.getUserIDForDN(distributedId, registry); - if (response.getRc() == 0 && StringUtils.isNotEmpty(response.getUserId())) { - return response.getUserId(); - } - - return null; + }); } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java index 154619d5cf..35e4908b38 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java @@ -40,7 +40,7 @@ public class JwtUtils { * This method reads the claims without validating the token signature. It should be used only if the validity was checked in the calling code. * * @param jwt token to be parsed - * @return parsed claims or empty object if the jwt is null + * @return parsed claims * @throws TokenNotValidException in case of invalid input, or TokenExpireException if JWT is expired */ public static Claims getJwtClaims(String jwt) { @@ -99,37 +99,63 @@ public static RuntimeException handleJwtParserException(RuntimeException excepti * Extracts value of a field from an OIDC token. The value is extracted from a custom path which supports nested objects. * @param token to extract the field from * @param pathToField list of strings representing path to the field - * @return userId extracted from the token + * @return list of values extracted from the token field * * @throws TokenFormatNotValidException in case of the field value cannot be extracted from the token, is null, or empty */ - @SuppressWarnings("rawtypes") - public static String getFieldValueFromToken(String token, List pathToField) throws TokenFormatNotValidException { + public static List getFieldValuesFromToken(String token, List pathToField) throws TokenFormatNotValidException { if (token == null || pathToField == null || pathToField.isEmpty() || StringUtils.isBlank(pathToField.get(0))) { - throw new IllegalArgumentException("Token and field path most not be null or empty"); + throw new IllegalArgumentException("Token and field path must not be null or empty"); } try { Claims claims = getJwtClaims(token); - String fieldValue; + List fieldValues; if (pathToField.size() == 1) { - fieldValue = claims.get(pathToField.get(0), String.class); + fieldValues = extractHighLevelField(claims, pathToField); } else { - var iterator = pathToField.iterator(); - var key = iterator.next(); - Map val = claims.get(key, Map.class); - while (iterator.hasNext()) { - key = iterator.next(); - if (iterator.hasNext()) { - val = (Map) val.get(key); - } - } - fieldValue = (String) val.get(key); + fieldValues = extractNestedFields(claims, pathToField); + } + + fieldValues = fieldValues.stream().filter(StringUtils::isNotBlank).toList(); + if (fieldValues.isEmpty()) { + throw new IllegalArgumentException(); + } else { + return fieldValues; } - if (StringUtils.isBlank(fieldValue)) throw new IllegalArgumentException(); - return fieldValue; } catch (Exception e) { - throw new TokenFormatNotValidException(String.format("Cannot extract value from field %s. The field does not exists, is empty, or is na object.", String.join(".", pathToField))); + throw new TokenFormatNotValidException( + String.format("Cannot extract value from field %s. The field does not exist, is empty, or is an object.", String.join(".", pathToField))); + } + } + + private List extractHighLevelField(Claims claims, List pathToField) { + return extractValueAsList(claims.get(pathToField.get(0))); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private List extractNestedFields(Claims claims, List pathToField) { + var iterator = pathToField.iterator(); + var key = iterator.next(); + Map val = claims.get(key, Map.class); + while (iterator.hasNext()) { + key = iterator.next(); + if (iterator.hasNext()) { + val = (Map) val.get(key); + } + } + + return extractValueAsList(val.get(key)); + } + + @SuppressWarnings("unchecked") + private List extractValueAsList(Object rawValue) { + if (rawValue instanceof String value) { + return List.of(value); + } else if (rawValue instanceof List values) { + return values; + } else { + throw new IllegalArgumentException("Field value is neither String nor List of Strings"); } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSource.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSource.java index 375f7af361..8edc8fb496 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSource.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSource.java @@ -15,6 +15,8 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; +import java.util.List; + /** * Implementation of OIDC token source of authentication. */ @@ -38,5 +40,5 @@ public AuthSource.AuthSourceType getType() { } @Setter - private String distributedId; + private List distributedId; } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java index 9432985e2e..2fe0839d01 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java @@ -35,7 +35,7 @@ import java.util.Optional; import java.util.function.Function; -import static org.zowe.apiml.zaas.security.service.JwtUtils.getFieldValueFromToken; +import static org.zowe.apiml.zaas.security.service.JwtUtils.getFieldValuesFromToken; @Service @RequiredArgsConstructor @@ -101,9 +101,9 @@ public boolean isValid(AuthSource authSource) { private boolean extractUserId(OIDCAuthSource authSource) { try { - var userId = getFieldValueFromToken(authSource.getRawSource(), userIdFieldPath); - logger.log(MessageType.DEBUG, "UserId {} extracted from OIDC token field {}.", userId, String.join(".", userIdFieldPath)); - authSource.setDistributedId(userId); + var userIds = getFieldValuesFromToken(authSource.getRawSource(), userIdFieldPath); + logger.log(MessageType.DEBUG, "UserId values {} extracted from OIDC token field {}.", userIds, String.join(".", userIdFieldPath)); + authSource.setDistributedId(userIds); return true; } catch (TokenFormatNotValidException e) { logger.log(MessageType.DEBUG, "Cannot extract distributed id from OIDC token. Reason: {}", e.getMessage()); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java index 6079bc51f7..a4ab25f70f 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCExternalMapperTest.java @@ -12,10 +12,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.entity.BasicHttpEntity; import org.apache.http.HttpStatus; import org.junit.jupiter.api.AfterEach; @@ -23,6 +26,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.commons.util.StringUtils; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -34,11 +39,18 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OIDCExternalMapperTest { @@ -52,13 +64,18 @@ class OIDCExternalMapperTest { private final AuthConfigurationProperties authConfigurationProperties = new AuthConfigurationProperties(); private OIDCAuthSource authSource; + private OIDCMapperHelper mapperHelper; private OIDCExternalMapper oidcExternalMapper; - private BasicHttpEntity responseEntity; - + private static final String DISTRIBUTED_ID_1 = "openmainframe"; + private static final String DISTRIBUTED_ID_2 = "zowe"; + private static final String DISTRIBUTED_ID_3 = "apiml"; + private static final List DISTRIBUTED_IDS = List.of(DISTRIBUTED_ID_1, DISTRIBUTED_ID_2, DISTRIBUTED_ID_3); private static final String ZOSUSER = "ZOSUSER"; - private static final String SUCCESS_MAPPER_RESPONSE = "{" + - "\"userid\": \"" + ZOSUSER + "\", " + + private static final String ZOSUSER_2 = "ZOSUSER2"; + + private static final String SUCCESS_MAPPER_RESPONSE_TEMPLATE = "{" + + "\"userid\": \"%s\", " + "\"returnCode\": 0, " + "\"safReturnCode\": 0, " + "\"racfReturnCode\": 0, " + @@ -72,52 +89,67 @@ class OIDCExternalMapperTest { "\"racfReturnCode\": 8, " + "\"racfReasonCode\": 48 " + "}"; + @BeforeEach void setup() { authSource = new OIDCAuthSource("OIDC_access_token"); - authSource.setDistributedId("distributed_ID"); - oidcExternalMapper = new OIDCExternalMapper("https://domain.com/mapper", "mapper_user", httpClient, tokenCreationService, authConfigurationProperties); - oidcExternalMapper.registry = "test_registry"; - - responseEntity = new BasicHttpEntity(IOUtils.toInputStream(SUCCESS_MAPPER_RESPONSE, StandardCharsets.UTF_8), ContentType.APPLICATION_JSON); + authSource.setDistributedId(List.of("distributed_ID")); + mapperHelper = new OIDCMapperHelper(); + mapperHelper.registry = "test_registry"; + oidcExternalMapper = new OIDCExternalMapper("https://domain.com/mapper", "mapper_user", httpClient, tokenCreationService, authConfigurationProperties, mapperHelper); } @Nested class GivenIdentityMappingExists { - @BeforeEach - void setup() { - CloseableHttpResponse response = mock(CloseableHttpResponse.class); - when(response.getCode()).thenReturn(HttpStatus.SC_OK); - when(response.getEntity()).thenReturn(responseEntity); - HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); + @Test + void thenZosUserIsReturned() { + var responses = mockMapperResponse(httpClient, ZOSUSER); + + String userId = oidcExternalMapper.mapToMainframeUserId(authSource); + assertEquals(ZOSUSER, userId); + verifyCallForResponse(responses); } @Test - void thenZosUserIsReturned() { + void givenMultipleDistributedIds_firstResponseMapped_thenZosUserIsReturned() { + var responses = mockMapperResponse(httpClient, ZOSUSER); + authSource.setDistributedId(DISTRIBUTED_IDS); String userId = oidcExternalMapper.mapToMainframeUserId(authSource); assertEquals(ZOSUSER, userId); + + verifyCallForResponse(responses); } + @Test + void givenMultipleDistributedIds_secondResponseMapped_thenZosUserIsReturned() { + var responses = mockMapperResponse(httpClient, null, ZOSUSER_2); + authSource.setDistributedId(DISTRIBUTED_IDS); + String userId = oidcExternalMapper.mapToMainframeUserId(authSource); + assertEquals(ZOSUSER_2, userId); + + verifyCallForResponse(responses); + } } @Nested class GivenNoIdentityMappingExists { - @BeforeEach - void setup() throws IOException { - responseEntity = new BasicHttpEntity(IOUtils.toInputStream(FAILURE_MAPPER_RESPONSE, StandardCharsets.UTF_8), ContentType.APPLICATION_JSON); - - CloseableHttpResponse response = mock(CloseableHttpResponse.class); - when(response.getCode()).thenReturn(HttpStatus.SC_OK); - when(response.getEntity()).thenReturn(responseEntity); - HttpClientMockHelper.mockExecuteWithResponse(httpClient, response); + @Test + void thenNullIsReturned() { + mockMapperResponse(httpClient, (String) null); + String userId = oidcExternalMapper.mapToMainframeUserId(authSource); + assertNull(userId); } @Test - void thenNullIsReturned() { + void givenMultipleDistributedIds_thenZosUserIsReturned() { + var responses = mockMapperResponse(httpClient, null, null, null); + authSource.setDistributedId(DISTRIBUTED_IDS); String userId = oidcExternalMapper.mapToMainframeUserId(authSource); assertNull(userId); + + verifyCallForResponse(responses); } } @@ -129,23 +161,43 @@ void whenAnotherAuthSourceUsed_thenNullIsReturned() throws IOException { JwtAuthSource jwtAuthSource = new JwtAuthSource("source"); String userId = oidcExternalMapper.mapToMainframeUserId(jwtAuthSource); assertNull(userId); - verify(httpClient, times(0)).execute(any()); + verify(httpClient, times(0)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); } @Test void whenRegistryIsNotProvided_thenNullIsReturned() throws IOException { - oidcExternalMapper.isConfigError = true; + mapperHelper.isConfigError = true; + String userId = oidcExternalMapper.mapToMainframeUserId(authSource); + assertNull(userId); + verify(httpClient, times(0)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); + } + } + + @Nested + class GivenInvalidDistributedIds { + + @Test + void whenEmptyListDistributedIdProvided_thenNullIsReturned() throws IOException { + authSource.setDistributedId(Collections.emptyList()); + String userId = oidcExternalMapper.mapToMainframeUserId(authSource); + assertNull(userId); + verify(httpClient, times(0)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); + } + + @Test + void whenNullDistributedIdProvided_thenNullIsReturned() throws IOException { + authSource.setDistributedId(null); String userId = oidcExternalMapper.mapToMainframeUserId(authSource); assertNull(userId); - verify(httpClient, times(0)).execute(any()); + verify(httpClient, times(0)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); } @Test - void whenNoDistributedIdProvided_thenNullIsReturned() throws IOException { - authSource.setDistributedId(""); + void whenBlankDistributedIdProvided_thenNullIsReturned() throws IOException { + authSource.setDistributedId(List.of(" ")); String userId = oidcExternalMapper.mapToMainframeUserId(authSource); assertNull(userId); - verify(httpClient, times(0)).execute(any()); + verify(httpClient, times(0)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); } } @@ -170,8 +222,38 @@ void whenJsonProcessingException_thenNullIsReturned() throws IOException { doThrow(JsonProcessingException.class).when(mockedMapper).writeValueAsString(any()); String userId = oidcExternalMapper.mapToMainframeUserId(authSource); assertNull(userId); - verify(httpClient, times(0)).execute(any()); + verify(httpClient, times(0)).execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)); } } + private List mockMapperResponse(CloseableHttpClient httpClient, String... zosUsers) { + List responses = new ArrayList<>(); + for (String zosUser : zosUsers) { + BasicHttpEntity responseEntity; + if (StringUtils.isBlank(zosUser)) { + responseEntity = new BasicHttpEntity(IOUtils.toInputStream( + FAILURE_MAPPER_RESPONSE, StandardCharsets.UTF_8), ContentType.APPLICATION_JSON); + } else { + responseEntity = new BasicHttpEntity(IOUtils.toInputStream( + String.format(SUCCESS_MAPPER_RESPONSE_TEMPLATE, zosUser), StandardCharsets.UTF_8), ContentType.APPLICATION_JSON); + } + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getCode()).thenReturn(HttpStatus.SC_OK); + when(response.getEntity()).thenReturn(responseEntity); + responses.add(response); + } + + HttpClientMockHelper.mockExecuteWithResponse(httpClient, responses.toArray(new CloseableHttpResponse[0])); + return responses; + } + + @SneakyThrows + private void verifyCallForResponse(List responses) { + responses.forEach(response -> { + verify(response, times(1)).getCode(); + verify(response, times(2)).getEntity(); + }); + verify(httpClient, times(responses.size())).execute(ArgumentMatchers.any(ClassicHttpRequest.class), ArgumentMatchers.any(HttpClientResponseHandler.class)); + } + } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapperTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapperTest.java index 270d93e902..0906770688 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapperTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/mapping/OIDCNativeMapperTest.java @@ -17,16 +17,31 @@ import org.zowe.apiml.zaas.security.service.schema.source.OIDCAuthSource; import org.zowe.commons.usermap.MapperResponse; +import java.util.Collections; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; class OIDCNativeMapperTest { private static final String DISTRIBUTED_ID = "distributed_id"; + private static final String DISTRIBUTED_ID_1 = "openmainframe"; + private static final String DISTRIBUTED_ID_2 = "zowe"; + private static final String DISTRIBUTED_ID_3 = "apiml"; + private static final List DISTRIBUTED_IDS = List.of(DISTRIBUTED_ID_1, DISTRIBUTED_ID_2, DISTRIBUTED_ID_3); private static final String MF_ID = "mf_user"; + private static final String MF_ID_2 = "mf_user2"; + private static final String MF_ID_3 = "mf_user3"; private static final String REGISTRY = "test_registry"; private OIDCAuthSource authSource; + private OIDCMapperHelper mapperHelper; private OIDCNativeMapper oidcNativeMapper; private NativeMapperWrapper mockMapper; @@ -34,39 +49,73 @@ class OIDCNativeMapperTest { @BeforeEach void setUp() { authSource = new OIDCAuthSource("OIDC_access_token"); - authSource.setDistributedId(DISTRIBUTED_ID); + authSource.setDistributedId(List.of(DISTRIBUTED_ID)); mockMapper = mock(NativeMapperWrapper.class); - oidcNativeMapper = new OIDCNativeMapper(mockMapper); - oidcNativeMapper.registry = REGISTRY; + mapperHelper = new OIDCMapperHelper(); + mapperHelper.registry = REGISTRY; + oidcNativeMapper = new OIDCNativeMapper(mockMapper, mapperHelper); } @Nested class GivenIdentityMappingExists { - @BeforeEach - void setup() { - when(mockMapper.getUserIDForDN(DISTRIBUTED_ID, REGISTRY)).thenReturn(new MapperResponse(MF_ID, 0, 0, 0, 0)); - } @Test void thenZosUserIsReturned() { + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID, REGISTRY)).thenReturn(new MapperResponse(MF_ID, 0, 0, 0, 0)); String userId = oidcNativeMapper.mapToMainframeUserId(authSource); assertEquals(MF_ID, userId); } + + @Test + void givenMultipleDistributedIds_onlyOneMapped_thenZosUserIsReturned() { + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_1, REGISTRY)).thenReturn(new MapperResponse(null, 8, 8, 8, 48)); + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_2, REGISTRY)).thenReturn(new MapperResponse(MF_ID_2, 0, 0, 0, 0)); + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_3, REGISTRY)).thenReturn(new MapperResponse(null, 8, 8, 8, 48)); + authSource.setDistributedId(DISTRIBUTED_IDS); + String userId = oidcNativeMapper.mapToMainframeUserId(authSource); + assertEquals(MF_ID_2, userId); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID_1, REGISTRY); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID_2, REGISTRY); + verify(mockMapper, never()).getUserIDForDN(DISTRIBUTED_ID_3, REGISTRY); + } + + @Test + void givenMultipleDistributedIds_multipleOneMapped_thenZosUserIsReturned() { + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_1, REGISTRY)).thenReturn(new MapperResponse("", 8, 8, 8, 48)); + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_2, REGISTRY)).thenReturn(new MapperResponse(MF_ID_2, 0, 0, 0, 0)); + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_3, REGISTRY)).thenReturn(new MapperResponse(MF_ID_3, 0, 0, 0, 0)); + authSource.setDistributedId(DISTRIBUTED_IDS); + String userId = oidcNativeMapper.mapToMainframeUserId(authSource); + assertEquals(MF_ID_2, userId); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID_1, REGISTRY); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID_2, REGISTRY); + verify(mockMapper, never()).getUserIDForDN(DISTRIBUTED_ID_3, REGISTRY); + } + } @Nested class GivenNoIdentityMappingExists { - @BeforeEach - void setup() { + @Test + void thenNullIsReturned() { when(mockMapper.getUserIDForDN(DISTRIBUTED_ID, REGISTRY)).thenReturn(new MapperResponse("", 8, 8, 8, 48)); + String userId = oidcNativeMapper.mapToMainframeUserId(authSource); + assertNull(userId); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID, REGISTRY); } @Test - void thenNullIsReturned() { + void givenMultipleDistributedIds_thenNullIsReturned() { + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_1, REGISTRY)).thenReturn(new MapperResponse(null, 8, 8, 8, 48)); + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_2, REGISTRY)).thenReturn(new MapperResponse(null, 8, 8, 8, 48)); + when(mockMapper.getUserIDForDN(DISTRIBUTED_ID_3, REGISTRY)).thenReturn(new MapperResponse(null, 8, 8, 8, 48)); + authSource.setDistributedId(DISTRIBUTED_IDS); String userId = oidcNativeMapper.mapToMainframeUserId(authSource); assertNull(userId); - verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID, REGISTRY); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID_1, REGISTRY); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID_2, REGISTRY); + verify(mockMapper, times(1)).getUserIDForDN(DISTRIBUTED_ID_3, REGISTRY); } } @@ -99,15 +148,36 @@ void whenAnotherAuthSourceUsed_thenNullIsReturned() { @Test void whenRegistryIsNotProvided_thenNullIsReturned() { - oidcNativeMapper.isConfigError = true; + mapperHelper.isConfigError = true; + String userId = oidcNativeMapper.mapToMainframeUserId(authSource); + assertNull(userId); + verifyNoInteractions(mockMapper); + } + + } + + @Nested + class GivenInvalidDistributedIds { + + @Test + void whenEmptyListDistributedIdProvided_thenNullIsReturned() { + authSource.setDistributedId(Collections.emptyList()); + String userId = oidcNativeMapper.mapToMainframeUserId(authSource); + assertNull(userId); + verifyNoInteractions(mockMapper); + } + + @Test + void whenBlankValueDistributedIdProvided_thenNullIsReturned() { + authSource.setDistributedId(List.of(" ")); String userId = oidcNativeMapper.mapToMainframeUserId(authSource); assertNull(userId); verifyNoInteractions(mockMapper); } @Test - void whenNoDistributedIdProvided_thenNullIsReturned() { - authSource.setDistributedId(""); + void whenNullValueDistributedIdProvided_thenNullIsReturned() { + authSource.setDistributedId(null); String userId = oidcNativeMapper.mapToMainframeUserId(authSource); assertNull(userId); verifyNoInteractions(mockMapper); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java index 3b70fbdd4a..925916bdc8 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java @@ -14,6 +14,7 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; import io.jsonwebtoken.JwtException; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -26,14 +27,16 @@ import java.util.Collections; import java.util.List; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.zowe.apiml.zaas.utils.JWTUtils.createTokenWithUserFields; class JwtUtilsTest { - private static final String TOKEN_WITH_USERNAME_FIELDS = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiLAogICJlbWFpbCI6ICJ1c2VybmFtZUBvaWRjLm9yZyIsCiAgIm9yZyI6IHsKICAgICJuYW1lIjogIm9wZW5tYWluZnJhbWUiLAogICAgImRlcCI6IHsKICAgICAgIm5hbWUiOiAiem93ZSIsCiAgICAgICJ0ZWFtIjogImFwaW1sIiwKICAgICAgImNvbnRyaWJ1dG9yIjogImNvbnRyaWJ1dG9yQGFwaW1sLnpvd2UiLAogICAgICAibmlja25hbWUiOiAiIiwKICAgICAgIm51bGxWYWx1ZSI6IG51bGwKICAgIH0KICB9LAogICJtZW1iZXJPZiI6IFsKICAgICJvcGVubWFpbmZyYW1lIiwKICAgICJ6b3dlIiwKICAgICJhcGltbCIKICBdCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; + private static final String TOKEN_WITH_USERNAME_FIELDS = createTokenWithUserFields(); @Test void testHandleJwtParserExceptionForExpiredToken() { @@ -58,39 +61,51 @@ void testHandleJwtParserRuntimeException() { assertEquals("An internal error occurred while validating the token therefore the token is no longer valid.", exception.getMessage()); } + @Test + void givenJwtNullToken_thenThrowTokenNotValidException() { + assertThrows(TokenNotValidException.class, () -> JwtUtils.getJwtClaims(null)); + } + @ParameterizedTest @CsvSource({ "email,username@oidc.org", "org.dep.contributor, contributor@apiml.zowe"}) void givenValidFieldPath_thenReturnCorrectValue(String fieldPath, String expectedValue) { - var actualValue = JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, splitFieldPath(fieldPath)); - assertEquals(expectedValue, actualValue); + var actualValue = JwtUtils.getFieldValuesFromToken(TOKEN_WITH_USERNAME_FIELDS, splitFieldPath(fieldPath)); + assertEquals(List.of(expectedValue), actualValue); + } + + @ParameterizedTest + @ValueSource(strings = { "memberOf", "groups.memberOf"}) + void givenValidFieldPath_thenReturnCorrectValuesFromArray(String fieldPath) { + var actualValue = JwtUtils.getFieldValuesFromToken(TOKEN_WITH_USERNAME_FIELDS, splitFieldPath(fieldPath)); + assertThat(List.of("openmainframe", "zowe", "apiml"), Matchers.containsInAnyOrder(actualValue.toArray())); } @ParameterizedTest - @ValueSource(strings = { "nonexistent", "org.nonexistent.foo", "org.dep", "org.dep.nonexistent", "org.dep.nickname", "org.dep.nullValue"}) + @ValueSource(strings = { "nonexistent", "nullValue", "org.nonexistent.foo", "org.dep", "org.dep.nonexistent", "org.dep.nickname"}) void givenInvalidFieldPath_thenThrowInvalidTokenFormatException(String fieldPath) { - assertThrows(TokenFormatNotValidException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, splitFieldPath(fieldPath))); + assertThrows(TokenFormatNotValidException.class, () -> JwtUtils.getFieldValuesFromToken(TOKEN_WITH_USERNAME_FIELDS, splitFieldPath(fieldPath))); } @Test void givenNullToken_thenThrowIllegalArgumentException() { - assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(null, List.of("foo"))); + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValuesFromToken(null, List.of("foo"))); } @Test void givenNullFieldPath_thenThrowIllegalArgumentException() { - assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, null)); + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValuesFromToken(TOKEN_WITH_USERNAME_FIELDS, null)); } @Test void givenEmptyFieldPath_thenThrowIllegalArgumentException() { - assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, Collections.emptyList())); + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValuesFromToken(TOKEN_WITH_USERNAME_FIELDS, Collections.emptyList())); } @Test void givenEmptyStringFieldPath_thenThrowIllegalArgumentException() { - assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValueFromToken(TOKEN_WITH_USERNAME_FIELDS, List.of(" "))); + assertThrows(IllegalArgumentException.class, () -> JwtUtils.getFieldValuesFromToken(TOKEN_WITH_USERNAME_FIELDS, List.of(" "))); } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java index 19157a8873..41d9b0faee 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java @@ -44,6 +44,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.zowe.apiml.zaas.utils.JWTUtils.createTokenWithUserFields; @ExtendWith(MockitoExtension.class) class OIDCAuthSourceServiceTest { @@ -55,7 +56,7 @@ class OIDCAuthSourceServiceTest { private OIDCProvider provider; private AuthenticationMapper mapper; private static final String DUMMY_TOKEN = "token"; - private static final String TOKEN_WITH_USERNAME_FIELDS = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiLAogICJlbWFpbCI6ICJ1c2VybmFtZUBvaWRjLm9yZyIsCiAgIm9yZyI6IHsKICAgICJuYW1lIjogIm9wZW5tYWluZnJhbWUiLAogICAgImRlcCI6IHsKICAgICAgIm5hbWUiOiAiem93ZSIsCiAgICAgICJ0ZWFtIjogImFwaW1sIiwKICAgICAgImNvbnRyaWJ1dG9yIjogImNvbnRyaWJ1dG9yQGFwaW1sLnpvd2UiLAogICAgICAibmlja25hbWUiOiAiIiwKICAgICAgIm51bGxWYWx1ZSI6IG51bGwKICAgIH0KICB9LAogICJtZW1iZXJPZiI6IFsKICAgICJvcGVubWFpbmZyYW1lIiwKICAgICJ6b3dlIiwKICAgICJhcGltbCIKICBdCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; + private static final String TOKEN_WITH_USERNAME_FIELDS = createTokenWithUserFields(); public static final String SUB_USER = "oidc.username"; private static final String MF_USER = "MF_USER"; private static final String DEFAULT_USERID_FIELD = "sub"; @@ -108,7 +109,7 @@ void givenTokenInAuthSource_thenReturnValid() { OIDCAuthSource authSource = new OIDCAuthSource(TOKEN_WITH_USERNAME_FIELDS); assertTrue(service.isValid(authSource)); - assertEquals(SUB_USER, authSource.getDistributedId()); + assertEquals(SUB_USER, authSource.getDistributedId().get(0)); } @Test @@ -118,7 +119,7 @@ void whenParse_thenCorrect() { AuthSource.Parsed parsedSource = service.parse(authSource); verify(mapper, times(1)).mapToMainframeUserId(authSource); - assertEquals(SUB_USER, authSource.getDistributedId()); + assertEquals(SUB_USER, authSource.getDistributedId().get(0)); assertEquals(MF_USER, parsedSource.getUserId()); } @@ -127,9 +128,7 @@ void givenNoMapping_whenParse_thenThrowException() { when(provider.isValid(TOKEN_WITH_USERNAME_FIELDS)).thenReturn(true); OIDCAuthSource authSource = new OIDCAuthSource(TOKEN_WITH_USERNAME_FIELDS); - assertThrows(NoMainframeIdentityException.class, () -> { - service.parse(authSource); - }); + assertThrows(NoMainframeIdentityException.class, () -> service.parse(authSource)); } @Test @@ -144,7 +143,7 @@ void givenValidAuthSource_thenReturnLTPAToken() { String ltpaResult = service.getLtpaToken(authSource); assertEquals(expectedToken, ltpaResult); - assertEquals(SUB_USER, authSource.getDistributedId()); + assertEquals(SUB_USER, authSource.getDistributedId().get(0)); } @Test @@ -155,7 +154,7 @@ void givenValidAuthSource_thenReturnJWT() { when(tokenCreationService.createJwtTokenWithoutCredentials(MF_USER)).thenReturn(expectedToken); String jwtResult = service.getJWT(authSource); assertEquals(expectedToken, jwtResult); - assertEquals(SUB_USER, authSource.getDistributedId()); + assertEquals(SUB_USER, authSource.getDistributedId().get(0)); } @ParameterizedTest @@ -170,7 +169,7 @@ void givenUserIdFieldProperty_thenReturnCorrectUsername(String preferredUsername service.afterPropertiesSet(); assertTrue(service.isValid(authSource)); - assertEquals(username, authSource.getDistributedId()); + assertEquals(username, authSource.getDistributedId().get(0)); } @ParameterizedTest diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java index 438e585f97..6d00752d91 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java @@ -12,7 +12,6 @@ import com.google.common.io.Resources; import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.util.DefaultResourceRetriever; import com.nimbusds.jose.util.Resource; import io.jsonwebtoken.Jwts; @@ -35,21 +34,27 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.zaas.cache.CachingServiceClientException; -import java.io.FileInputStream; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.security.Key; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.interfaces.RSAPublicKey; import java.time.Instant; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; 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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.zowe.apiml.zaas.utils.JWTUtils.loadPrivateKey; @ExtendWith(MockitoExtension.class) class OIDCTokenProviderTest { @@ -79,7 +84,8 @@ static Stream invalidTokens() { @BeforeAll static void init() throws Exception { var now = Instant.now(); - var pKey = loadPrivateKey("../keystore/localhost/localhost.keystore.p12", "localhost", "password"); + var jwkAndSet = loadPrivateKey("../keystore/localhost/localhost.keystore.p12", "localhost", "password"); + localJwkSet = jwkAndSet.jwkSet(); VALID_TOKEN = Jwts.builder() .header().keyId("0987").and() .subject("user") @@ -87,23 +93,7 @@ static void init() throws Exception { .expiration(Date.from(now.plusSeconds(1200))) .issuer("API ML") .id(UUID.randomUUID().toString()) - .signWith(pKey, Jwts.SIG.RS256).compact(); - } - - static PrivateKey loadPrivateKey(String path, String alias, String password) throws Exception { - KeyStore ks = KeyStore.getInstance("PKCS12"); - try (FileInputStream fis = new FileInputStream(path)) { - ks.load(fis, password.toCharArray()); - } - Key key = ks.getKey(alias, password.toCharArray()); - var cert = ks.getCertificate(alias); - var pubKey = cert.getPublicKey(); - if (pubKey instanceof RSAPublicKey rsaPublicKey) { - var k = new RSAKey.Builder(rsaPublicKey).keyID("0987").build().toPublicJWK(); - localJwkSet = new JWKSet(k); - } - - return (PrivateKey) key; + .signWith(jwkAndSet.privateKey()).compact(); } @BeforeEach diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java index e9e6eeef70..e0114f3a55 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java @@ -10,13 +10,22 @@ package org.zowe.apiml.zaas.utils; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import lombok.SneakyThrows; import org.zowe.apiml.security.HttpsConfig; import org.zowe.apiml.security.SecurityUtils; +import java.io.FileInputStream; import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; import java.util.Date; +import java.util.List; +import java.util.Map; import java.util.UUID; public class JWTUtils { @@ -34,14 +43,60 @@ public static String createToken(String username, String domain, String ltpaToke long expiration = now + 100_000L; Key jwtSecret = SecurityUtils.loadKey(config); return Jwts.builder() - .setSubject(username) + .subject(username) .claim("dom", domain) .claim("ltpa", ltpaToken) - .setIssuedAt(new Date(now)) - .setExpiration(new Date(expiration)) - .setIssuer(issuer) - .setId(UUID.randomUUID().toString()) - .signWith(jwtSecret, SignatureAlgorithm.RS256) + .issuedAt(new Date(now)) + .expiration(new Date(expiration)) + .issuer(issuer) + .id(UUID.randomUUID().toString()) + .signWith(jwtSecret) .compact(); } + + @SneakyThrows + public static String createTokenWithUserFields() { + var now = Instant.now(); + var jwkAndSet = loadPrivateKey("../keystore/localhost/localhost.keystore.p12", "localhost", "password"); + return Jwts.builder() + .header().keyId("0987").and() + .subject("oidc.username") + .claim("email", "username@oidc.org") + .claim("nullValue", null) + .claim("org", Map.of( + "name", "openmainframe", + "dep", Map.of( + "name", "zowe", + "team", "apiml", + "contributor", "contributor@apiml.zowe", + "nickname", "" + ))) + .claim("memberOf", List.of("openmainframe", "zowe", "apiml")) + .claim("groups", Map.of("memberOf", List.of("openmainframe", "zowe", "apiml"))) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(1200))) + .issuer("API ML") + .id(UUID.randomUUID().toString()) + .signWith(jwkAndSet.privateKey()) + .compact(); + } + + public static JwkAndSet loadPrivateKey(String path, String alias, String password) throws Exception { + KeyStore ks = KeyStore.getInstance("PKCS12"); + try (FileInputStream fis = new FileInputStream(path)) { + ks.load(fis, password.toCharArray()); + } + Key key = ks.getKey(alias, password.toCharArray()); + var cert = ks.getCertificate(alias); + var pubKey = cert.getPublicKey(); + if (pubKey instanceof RSAPublicKey rsaPublicKey) { + var k = new RSAKey.Builder(rsaPublicKey).keyID("0987").build().toPublicJWK(); + return new JwkAndSet((PrivateKey) key, new JWKSet(k)); + } + + return new JwkAndSet((PrivateKey) key, null); + } + + public record JwkAndSet(PrivateKey privateKey, JWKSet jwkSet) { + } } From 1137b5f66c850b777b892c04b4f3db0611060b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= <58428711+pj892031@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:17:13 +0200 Subject: [PATCH 116/152] chore: Upgrade transitive dependency fileupload to 1.6.0 (#4317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Gowtham Selvaraj --- gradle/versions.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 3b23a677cf..fc9b9ff39b 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -30,6 +30,7 @@ dependencyResolutionManagement { version('commonsIo', '2.20.0') version('ehCache', '3.11.1') version('eureka', '2.0.5') + version('fileupload', '1.6.0') version('netflixServo', '0.13.2') version('googleErrorprone', '2.41.0') version('gradleGitProperties', '2.5.2') // Used in classpath dependencies @@ -167,6 +168,7 @@ dependencyResolutionManagement { library('eh_cache', 'org.ehcache', 'ehcache').versionRef('ehCache') library('eureka_jersey_client', 'com.netflix.eureka', 'eureka-client-jersey3').versionRef('eureka') library('eureka_core', 'com.netflix.eureka', 'eureka-core').versionRef('eureka') + library('fileupload', 'commons-fileupload', 'commons-fileupload').versionRef('fileupload') library('google_errorprone', 'com.google.errorprone', 'error_prone_annotations').versionRef('googleErrorprone') // to define minimum version and avoid duplicity libraries in the classpath library('google_gson', 'com.google.code.gson', 'gson').versionRef('googleGson') library('guava', 'com.google.guava', 'guava').versionRef('guava') From 8e474995acae8038950a74007c404abe340164a9 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 19 Sep 2025 00:44:20 +0000 Subject: [PATCH 117/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.10'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f2b537b978..fcfad29348 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.10-SNAPSHOT +version=3.3.10 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From d68ac08f1c868542a04c5a3b5263e64b7a2633f6 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 19 Sep 2025 00:44:22 +0000 Subject: [PATCH 118/152] [Gradle Release plugin] Create new version: 'v3.3.11-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fcfad29348..0406f21ff1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.10 +version=3.3.11-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 5a1d8edd9b3a44efd3851af2a0fe6f573ed11a73 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 19 Sep 2025 00:44:23 +0000 Subject: [PATCH 119/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 4d70c5153b..022f6bfae1 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.10-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.11-SNAPSHOT From 3052b6f9868fbd528e2c831d8fc802ecf19effa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Mon, 22 Sep 2025 15:13:02 +0200 Subject: [PATCH 120/152] feat: Support Keycloak as OIDC provider in integration tests (#4321) Signed-off-by: ac892247 Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- .github/workflows/integration-tests.yml | 4 +- config/local/zaas-service.yml | 4 +- integration-tests/build.gradle | 24 +-- ...ktaOauth2Test.java => OidcOauth2Test.java} | 108 ++++--------- .../schemes/GatewayAuthTest.java | 11 +- .../services/ZaasClientIntegrationTest.java | 14 +- .../integration/zaas/PassTicketTest.java | 21 +-- .../integration/zaas/SafIdTokensTest.java | 11 +- .../integration/zaas/ZaasNegativeTest.java | 11 +- .../integration/zaas/ZosmfTokensTest.java | 6 +- .../integration/zaas/ZoweTokensTest.java | 7 +- .../org/zowe/apiml/util/SecurityUtils.java | 144 +++++++++++++----- .../zowe/apiml/util/config/ConfigReader.java | 27 ++-- .../util/config/EnvironmentConfiguration.java | 3 +- .../apiml/util/config/IDPConfiguration.java | 26 ---- .../apiml/util/config/OidcConfiguration.java | 9 +- ...nment-configuration-docker-modulith-ha.yml | 6 +- ...ironment-configuration-docker-modulith.yml | 6 +- .../environment-configuration-docker.yml | 6 +- .../environment-configuration-ha.yml | 6 +- .../resources/environment-configuration.yml | 10 +- .../zaas/security/mapping/NativeMapper.java | 2 +- 22 files changed, 215 insertions(+), 251 deletions(-) rename integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/{OktaOauth2Test.java => OidcOauth2Test.java} (88%) delete mode 100644 integration-tests/src/test/java/org/zowe/apiml/util/config/IDPConfiguration.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3bc993e5cb..e96daa3a8e 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -867,9 +867,9 @@ jobs: run: > ./gradlew runStartUpCheck :integration-tests:runZaasTest --info -Denvironment.config=-docker -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} - -Dokta.client.id=${{ secrets.OKTA_CLIENT_ID }} -Doidc.test.user=${{ secrets.OIDC_TEST_USER }} + -Doidc.client.id=${{ secrets.OKTA_CLIENT_ID }} -Doidc.test.user=${{ secrets.OIDC_TEST_USER }} -Doidc.test.pass=${{ secrets.OIDC_TEST_PASS }} -Doidc.test.alt_user=${{ secrets.OKTA_WINNIE_USER }} - -Doidc.test.alt_pass=${{ secrets.OKTA_WINNIE_PASS }} -DidpConfiguration.host=${{secrets.OKTA_HOST}} + -Doidc.test.alt_pass=${{ secrets.OKTA_WINNIE_PASS }} -Doidc.host=${{secrets.OKTA_HOST}} -Ddiscoverableclient.instances=1 - name: Dump DC jacoco data diff --git a/config/local/zaas-service.yml b/config/local/zaas-service.yml index 88afe8f745..c9b75c501c 100644 --- a/config/local/zaas-service.yml +++ b/config/local/zaas-service.yml @@ -13,14 +13,14 @@ apiml: personalAccessToken: enabled: true oidc: - enabled: false + enabled: true clientId: clientSecret: registry: zowe.okta.com identityMapperUrl: https://localhost:10010/zss/api/v1/certificate/dn identityMapperUser: APIMTST jwks: - uri: + uri: https://dev-15878923.okta.com/oauth2/default/v1/keys auth: jwt: customAuthHeader: diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 751c37d6a3..29f10bf7c3 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -123,7 +123,7 @@ task runLocalIntegrationTests(type: Test) { 'AdditionalLocalTest', 'HATest', 'ChaoticHATest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest' ) } @@ -145,7 +145,7 @@ task runAllIntegrationTestsNormal(type: Test) { 'HATest', 'ChaoticHATest', 'NotAttlsTest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest' ) @@ -193,7 +193,7 @@ task runAllIntegrationTestsForZoweNonHaTestingOnZos(type: Test) { 'DiscoverableClientDependentTest', 'HATest', 'ChaoticHATest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest', 'NotForMainframeTest', 'ApiCatalogStandaloneTest', @@ -235,7 +235,7 @@ task runAllIntegrationTestsForZoweHaTestingOnZos(type: Test) { 'AdditionalLocalTest', 'TestsNotMeantForZowe', 'DiscoverableClientDependentTest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest', 'NotForMainframeTest', 'ApiCatalogStandaloneTest', @@ -283,7 +283,7 @@ task runAllIntegrationTestsForZoweModulithNonHaTestingOnZos(type: Test) { 'DiscoverableClientDependentTest', 'HATest', 'ChaoticHATest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest', 'NotForMainframeTest', 'ApiCatalogStandaloneTest', @@ -327,7 +327,7 @@ task runAllIntegrationTestsForZoweModulithHaTestingOnZos(type: Test) { 'AdditionalLocalTest', 'TestsNotMeantForZowe', 'DiscoverableClientDependentTest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest', 'NotForMainframeTest', 'ApiCatalogStandaloneTest', @@ -366,7 +366,7 @@ task runAllIntegrationTestsForZoweTesting(type: Test) { 'DiscoverableClientDependentTest', 'HATest', 'ChaoticHATest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest', 'NotForMainframeTest' ) @@ -396,7 +396,7 @@ task runCITests(type: Test) { 'DeterministicLbHaTest', 'StickySessionLbHaTest', 'ChaoticHATest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'MultipleRegistrationsTest' ) } @@ -425,7 +425,7 @@ task runContainerModulithTests(type: Test) { 'HATest', 'ChaoticHATest', 'InfinispanStorageTest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'GatewayProxyTest', 'GatewayServiceRouting', 'GatewayCentralRegistry', @@ -460,7 +460,7 @@ task runContainerTests(type: Test) { 'HATest', 'ChaoticHATest', 'InfinispanStorageTest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'GatewayProxyTest', 'GatewayServiceRouting', 'GatewayCentralRegistry', @@ -510,7 +510,7 @@ task runBaseTests(type: Test) { 'HATest', 'ChaoticHATest', 'InfinispanStorageTest', - 'OktaOauth2Test', + 'OidcOauth2Test', 'GatewayProxyTest', 'GatewayServiceRouting', 'GatewayCentralRegistry', @@ -604,7 +604,7 @@ task runZaasTest(type: Test) { useJUnitPlatform { includeTags( 'ZaasTest', - 'OktaOauth2Test' + 'OidcOauth2Test' ) } } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OidcOauth2Test.java similarity index 88% rename from integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java rename to integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OidcOauth2Test.java index 0caf6bda5c..eabd77c18d 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OidcOauth2Test.java @@ -12,23 +12,18 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWKSet; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.LocatorAdapter; -import io.jsonwebtoken.ProtectedHeader; +import io.jsonwebtoken.*; import io.jsonwebtoken.impl.DefaultClock; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; -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.*; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.integration.authentication.oauth2.model.ZssResponse; import org.zowe.apiml.integration.authentication.pat.ValidateRequestModel; @@ -51,20 +46,17 @@ import static org.hamcrest.Matchers.hasKey; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.zowe.apiml.util.SecurityUtils.GATEWAY_TOKEN_COOKIE_NAME; -import static org.zowe.apiml.util.requests.Endpoints.JWK_ALL; -import static org.zowe.apiml.util.requests.Endpoints.REQUEST_INFO_ENDPOINT; -import static org.zowe.apiml.util.requests.Endpoints.SAF_IDT_REQUEST; -import static org.zowe.apiml.util.requests.Endpoints.ZOSMF_REQUEST; -import static org.zowe.apiml.util.requests.Endpoints.ZOWE_JWT_REQUEST; +import static org.zowe.apiml.util.requests.Endpoints.*; -@Tag("OktaOauth2Test") -public class OktaOauth2Test { +@Tag("OidcOauth2Test") +public class OidcOauth2Test { public static final URI VALIDATE_ENDPOINT = HttpRequestUtils.getUriFromGateway(Endpoints.VALIDATE_OIDC_TOKEN); - public static final URI JWK_ENDPOINT = HttpRequestUtils.getUriFromGateway(JWK_ALL); - private static final String VALID_TOKEN_WITH_MAPPING = SecurityUtils.validOktaAccessToken(true); - private static final String VALID_TOKEN_NO_MAPPING = SecurityUtils.validOktaAccessToken(false); - private static final String EXPIRED_TOKEN = SecurityUtils.expiredOktaAccessToken(); + public static URI JWK_ENDPOINT = HttpRequestUtils.getUriFromGateway(JWK_ALL); + + private static final String VALID_TOKEN_WITH_MAPPING = SecurityUtils.validOidcAccessToken(true); + private static final String VALID_TOKEN_NO_MAPPING = SecurityUtils.validOidcAccessToken(false); + private static final String EXPIRED_TOKEN = SecurityUtils.expiredOidcAccessToken(); static Stream validTokens() { return Stream.of( @@ -86,10 +78,10 @@ static void init() { } @Nested - class GivenValidOktaToken { + class GivenValidOidcToken { @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#validTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#validTokens") void thenValidateUsingJWKLocally(String token) throws ParseException, IOException, JOSEException { HttpsURLConnection.setDefaultSSLSocketFactory(SecurityUtils.getSslContext().getSocketFactory()); JWKSet jwkSet = JWKSet.load(new URL(JWK_ENDPOINT.toString())); @@ -112,7 +104,7 @@ protected Key locate(ProtectedHeader header) { } @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#validTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#validTokens") void thenValidateReturns200(String validToken) { ValidateRequestModel requestBody = new ValidateRequestModel(); requestBody.setToken(validToken); @@ -379,10 +371,10 @@ void whenUserNoHasMapping_thenZoweAuthFailure() { } @Nested - class GivenInvalidOktaToken { + class GivenInvalidOidcToken { @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void thenValidateReturns401(String invalidToken) { ValidateRequestModel requestBody = new ValidateRequestModel(); requestBody.setToken(invalidToken); @@ -399,7 +391,7 @@ class WhenTestingZoweJwtScheme { private final URI DC_url = HttpRequestUtils.getUriFromGateway(ZOWE_JWT_REQUEST); @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -413,7 +405,7 @@ void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { } @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInCookie_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -432,7 +424,7 @@ class WhenTestingZosmfScheme { private final URI DC_url = HttpRequestUtils.getUriFromGateway(ZOSMF_REQUEST); @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -446,7 +438,7 @@ void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { } @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInCookie_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -465,7 +457,7 @@ class WhenTestingSafIdtScheme { private final URI DC_url = HttpRequestUtils.getUriFromGateway(SAF_IDT_REQUEST); @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -479,7 +471,7 @@ void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { } @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInCookie_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -497,7 +489,7 @@ class WhenTestingPassticketScheme { private final URI DC_url = HttpRequestUtils.getUriFromGateway(REQUEST_INFO_ENDPOINT); @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -512,7 +504,7 @@ void whenTokenInHeader_thenZoweAuthFailure(String invalidToken) { } @ParameterizedTest - @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#invalidTokens") + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OidcOauth2Test#invalidTokens") void whenTokenInCookie_thenZoweAuthFailure(String invalidToken) { given() .contentType(ContentType.JSON) @@ -566,54 +558,10 @@ void testUserIsNotAuthorizedToQueryMapping() { .body("headers", not(hasKey("cookie"))); } - @Test - void testOtherMappingError() { - setZssResponse(200, ZssResponse.ZssError.MAPPING_OTHER); - - given() - .contentType(ContentType.JSON) - .header(HttpHeaders.AUTHORIZATION, - ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN_WITH_MAPPING) - .when() - .get(DC_url) - .then().statusCode(200) - .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", not(hasKey("cookie"))); - } - - @Test - void testZssReturns401() { - setZssResponse(401, ZssResponse.ZssError.MAPPING_OTHER); - - given() - .contentType(ContentType.JSON) - .header(HttpHeaders.AUTHORIZATION, - ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN_WITH_MAPPING) - .when() - .get(DC_url) - .then().statusCode(200) - .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", not(hasKey("cookie"))); - } - - @Test - void testZssReturns404() { - setZssResponse(404, ZssResponse.ZssError.MAPPING_OTHER); - - given() - .contentType(ContentType.JSON) - .header(HttpHeaders.AUTHORIZATION, - ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN_WITH_MAPPING) - .when() - .get(DC_url) - .then().statusCode(200) - .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", not(hasKey("cookie"))); - } - - @Test - void testZssReturns500() { - setZssResponse(500, ZssResponse.ZssError.MAPPING_OTHER); + @ParameterizedTest + @ValueSource(ints = {200, 401, 404, 500}) + void testOtherZaasResponse(int responseStatusCode) { + setZssResponse(responseStatusCode, ZssResponse.ZssError.MAPPING_OTHER); given() .contentType(ContentType.JSON) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java index b87fad4256..4b8d701a21 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java @@ -25,10 +25,7 @@ import org.zowe.apiml.util.config.*; import org.zowe.apiml.util.http.HttpRequestUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Stream; @@ -36,7 +33,9 @@ import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; import static org.junit.jupiter.api.Assertions.*; -import static org.zowe.apiml.util.SecurityUtils.*; +import static org.zowe.apiml.util.SecurityUtils.generateJwtWithRandomSignature; +import static org.zowe.apiml.util.SecurityUtils.personalAccessToken; +import static org.zowe.apiml.util.SecurityUtils.validOidcAccessToken; import static org.zowe.apiml.util.requests.Endpoints.*; @ZaasTest @@ -153,7 +152,7 @@ void givenValidRequest_thenClientCertIsTransformed(String title, String basePath @ParameterizedTest(name = "givenValidRequest_thenOidcIsTransformed {0} [{index}]") @MethodSource("org.zowe.apiml.integration.authentication.schemes.GatewayAuthTest#validToBeTransformed") void givenValidRequest_thenOidcIsTransformed(String title, String basePath, Consumer assertions) { - String oAuthToken = validOktaAccessToken(true); + String oAuthToken = validOidcAccessToken(true); Response response = given() .header(HttpHeaders.AUTHORIZATION, "Bearer " + oAuthToken) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ZaasClientIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ZaasClientIntegrationTest.java index f5d60d899b..95bf672204 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ZaasClientIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ZaasClientIntegrationTest.java @@ -14,10 +14,7 @@ import io.jsonwebtoken.SignatureAlgorithm; import org.apache.commons.codec.binary.Base64; import org.hamcrest.core.Is; -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.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -36,10 +33,7 @@ import org.zowe.apiml.zaasclient.service.ZaasToken; import org.zowe.apiml.zaasclient.service.internal.ZaasClientImpl; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.CertificateException; @@ -264,10 +258,10 @@ void givenEmptyToken() { } @Nested - @Tag("OktaOauth2Test") + @Tag("OidcOauth2Test") class WhenOidcQuery { - private static final String VALID_TOKEN_NO_MAPPING = SecurityUtils.validOktaAccessToken(false); + private static final String VALID_TOKEN_NO_MAPPING = SecurityUtils.validOidcAccessToken(false); @Test void givenValidOidcToken_thenValidDetailsAreProvided() throws ZaasClientException { diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java index 720fb9fd9f..3f68c5987d 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/PassTicketTest.java @@ -11,10 +11,7 @@ package org.zowe.apiml.integration.zaas; import io.restassured.RestAssured; -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.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.zowe.apiml.passticket.PassTicketService; @@ -32,11 +29,7 @@ import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.TEXT; -import static org.apache.http.HttpStatus.SC_BAD_REQUEST; -import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; -import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED; -import static org.apache.http.HttpStatus.SC_OK; -import static org.apache.http.HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE; +import static org.apache.http.HttpStatus.*; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.isEmptyOrNullString; @@ -45,13 +38,7 @@ import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase; import static org.zowe.apiml.integration.zaas.ZaasTestUtil.COOKIE; import static org.zowe.apiml.integration.zaas.ZaasTestUtil.ZAAS_TICKET_URI; -import static org.zowe.apiml.util.SecurityUtils.USERNAME; -import static org.zowe.apiml.util.SecurityUtils.generateZoweJwtWithLtpa; -import static org.zowe.apiml.util.SecurityUtils.getConfiguredSslConfig; -import static org.zowe.apiml.util.SecurityUtils.getZosmfJwtTokenFromGw; -import static org.zowe.apiml.util.SecurityUtils.getZosmfLtpaToken; -import static org.zowe.apiml.util.SecurityUtils.personalAccessToken; -import static org.zowe.apiml.util.SecurityUtils.validOktaAccessToken; +import static org.zowe.apiml.util.SecurityUtils.*; /** * Verify integration of the API ML PassTicket support with the zOS provider of the PassTicket. @@ -151,7 +138,7 @@ void givenX509Certificate(String certificate, String description) { @Test void givenValidOAuthToken() { - String oAuthToken = validOktaAccessToken(true); + String oAuthToken = validOidcAccessToken(true); //@formatter:off given() diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java index 6d0c80b581..259144c1eb 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/SafIdTokensTest.java @@ -11,10 +11,7 @@ package org.zowe.apiml.integration.zaas; import io.restassured.RestAssured; -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.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.zowe.apiml.passticket.PassTicketService; @@ -37,7 +34,9 @@ import static jakarta.servlet.http.HttpServletResponse.*; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.*; -import static org.zowe.apiml.integration.zaas.ZaasTestUtil.*; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.COOKIE; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.LTPA_COOKIE; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.ZAAS_SAFIDT_URI; import static org.zowe.apiml.util.SecurityUtils.*; @ZaasTest @@ -132,7 +131,7 @@ void givenX509Certificate(String certificate, String description) { @Test void givenValidOAuthToken() { - String oAuthToken = validOktaAccessToken(true); + String oAuthToken = validOidcAccessToken(true); //@formatter:off given() diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZaasNegativeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZaasNegativeTest.java index d1962a0848..dcc8aca608 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZaasNegativeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZaasNegativeTest.java @@ -26,10 +26,7 @@ import org.zowe.apiml.util.config.*; import java.net.URI; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Stream; import static io.restassured.RestAssured.given; @@ -141,11 +138,11 @@ void givenInvalidToken(URI uri, RequestSpecification requestSpecification, Strin @ParameterizedTest @MethodSource("org.zowe.apiml.integration.zaas.ZaasNegativeTest#provideZaasEndpoints") - void givenOKTATokenWithNoMapping(URI uri, RequestSpecification requestSpecification) { - String oktaTokenNoMapping = SecurityUtils.validOktaAccessToken(false); + void givenOidcTokenWithNoMapping(URI uri, RequestSpecification requestSpecification) { + String oidcTokenNoMapping = SecurityUtils.validOidcAccessToken(false); //@formatter:off requestSpecification - .header("Authorization", "Bearer " + oktaTokenNoMapping) + .header("Authorization", "Bearer " + oidcTokenNoMapping) .when() .post(uri) .then() diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZosmfTokensTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZosmfTokensTest.java index ea82be736a..c999b909a2 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZosmfTokensTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZosmfTokensTest.java @@ -31,7 +31,9 @@ import static org.hamcrest.Matchers.isEmptyOrNullString; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; -import static org.zowe.apiml.integration.zaas.ZaasTestUtil.*; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.COOKIE; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.LTPA_COOKIE; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.ZAAS_ZOSMF_URI; import static org.zowe.apiml.util.SecurityUtils.*; @ZaasTest @@ -115,7 +117,7 @@ void givenX509Certificate(String certificate, String description) { @Test void givenValidOAuthToken() { - String oAuthToken = validOktaAccessToken(true); + String oAuthToken = validOidcAccessToken(true); //@formatter:off given() diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZoweTokensTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZoweTokensTest.java index be71839d34..fef40dfa82 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZoweTokensTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZoweTokensTest.java @@ -31,7 +31,8 @@ import static org.hamcrest.Matchers.isEmptyOrNullString; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; -import static org.zowe.apiml.integration.zaas.ZaasTestUtil.*; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.COOKIE; +import static org.zowe.apiml.integration.zaas.ZaasTestUtil.ZAAS_ZOWE_URI; import static org.zowe.apiml.util.SecurityUtils.*; @ZaasTest @@ -113,14 +114,16 @@ void givenX509Certificate(String certificate, String description) { @Test void givenValidOAuthToken() { - String oAuthToken = validOktaAccessToken(true); + String oAuthToken = validOidcAccessToken(true); //@formatter:off given() + .log().all() .cookie(COOKIE, oAuthToken) .when() .post(ZAAS_ZOWE_URI) .then() + .log().all() .statusCode(SC_OK) .body("cookieName", is(COOKIE)) .body("token", not(isEmptyOrNullString())); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java index ae40478d39..5b422ea4db 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java @@ -10,6 +10,7 @@ package org.zowe.apiml.util; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.util.Base64; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -24,10 +25,7 @@ import org.apache.http.HttpHeaders; import org.apache.http.ParseException; import org.apache.http.client.CookieStore; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.*; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.utils.URIBuilder; import org.apache.http.conn.ssl.SSLSocketFactory; @@ -60,32 +58,16 @@ import org.zowe.apiml.util.http.HttpRequestUtils; import javax.net.ssl.SSLContext; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; -import java.security.Key; -import java.security.KeyManagementException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Security; -import java.security.UnrecoverableKeyException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.*; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.Calendar; -import java.util.Date; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; @@ -97,10 +79,7 @@ import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.zowe.apiml.util.requests.Endpoints.GENERATE_ACCESS_TOKEN; -import static org.zowe.apiml.util.requests.Endpoints.ROUTED_LOGIN; -import static org.zowe.apiml.util.requests.Endpoints.ROUTED_QUERY; -import static org.zowe.apiml.util.requests.Endpoints.ZOSMF_AUTH_ENDPOINT; +import static org.zowe.apiml.util.requests.Endpoints.*; public class SecurityUtils { public static final String GATEWAY_TOKEN_COOKIE_NAME = "apimlAuthenticationToken"; @@ -119,16 +98,24 @@ public class SecurityUtils { public static final String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); public static final String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); - public static final String OKTA_HOSTNAME = ConfigReader.environmentConfiguration().getIdpConfiguration().getHost(); - public static final String OKTA_CLIENT_ID = ConfigReader.environmentConfiguration().getOidcConfiguration().getClientId(); - public static final String OKTA_USER = ConfigReader.environmentConfiguration().getIdpConfiguration().getUser(); - public static final String OKTA_PASSWORD = ConfigReader.environmentConfiguration().getIdpConfiguration().getPassword(); - public static final String OKTA_ALT_USER = ConfigReader.environmentConfiguration().getIdpConfiguration().getAlternateUser(); - public static final String OKTA_ALT_PASSWORD = ConfigReader.environmentConfiguration().getIdpConfiguration().getAlternatePassword(); + public static final String OIDC_HOSTNAME = ConfigReader.environmentConfiguration().getOidcConfiguration().getHost(); + public static final String OIDC_CLIENT_ID = ConfigReader.environmentConfiguration().getOidcConfiguration().getClientId(); + public static final String OIDC_CLIENT_PASSWORD = ConfigReader.environmentConfiguration().getOidcConfiguration().getClientSecret(); + public static final String OIDC_USER = ConfigReader.environmentConfiguration().getOidcConfiguration().getUser(); + public static final String OIDC_PASSWORD = ConfigReader.environmentConfiguration().getOidcConfiguration().getPassword(); + public static final String OIDC_ALT_USER = ConfigReader.environmentConfiguration().getOidcConfiguration().getAlternateUser(); + public static final String OIDC_ALT_PASSWORD = ConfigReader.environmentConfiguration().getOidcConfiguration().getAlternatePassword(); + public static final String OIDC_PROVIDER_NAME = ConfigReader.environmentConfiguration().getOidcConfiguration().getProviderName(); + + public static final String OKTA_AUTHENTICATE_SESSION_URL = "/api/v1/authn"; + public static final String OKTA_GENERATE_TOKEN_URL = "/oauth2/v1/authorize"; + public static final String KEYCLOAK_GENERATE_TOKEN_URL = "/realms/apiml/protocol/openid-connect/token"; public static final String COOKIE_NAME = "apimlAuthenticationToken"; public static final String PAT_COOKIE_AUTH_NAME = "personalAccessToken"; + public static final ObjectMapper MAPPER = new ObjectMapper(); + protected static String getUsername() { return USERNAME; } @@ -432,23 +419,33 @@ public static String personalAccessTokenWithClientCert(RestAssuredConfig sslConf } } + public static String validOidcAccessToken(boolean userHasMappingDefined) { + if (isKeycloakProvider()) { + return validKeycloakToken(userHasMappingDefined); + } else if (isOktaProvider()) { + return validOktaAccessToken(userHasMappingDefined); + } + throw new IllegalArgumentException(String.format("Unsupported OIDC provider: %s", OIDC_PROVIDER_NAME)); + } + public static String validOktaAccessToken(boolean userHasMappingDefined) { - assertNotNull(OKTA_HOSTNAME, "OKTA host name is not set."); - assertNotNull(OKTA_CLIENT_ID, "OKTA client id is not set."); + + assertNotNull(OIDC_HOSTNAME, "OIDC host name is not set."); + assertNotNull(OIDC_CLIENT_ID, "OIDC client id is not set."); String sessionToken; if (userHasMappingDefined) { - sessionToken = getOktaSession(OKTA_USER, OKTA_PASSWORD); + sessionToken = getOktaSession(OIDC_USER, OIDC_PASSWORD); } else { - sessionToken = getOktaSession(OKTA_ALT_USER, OKTA_ALT_PASSWORD); + sessionToken = getOktaSession(OIDC_ALT_USER, OIDC_ALT_PASSWORD); } assertNotNull(sessionToken, "Failed to get session token from Okta authentication."); // retrieve the access token from Okta using session token try (CloseableHttpClient httpClient = HttpClientBuilder.create().setSSLContext(getRelaxedSslContext()).build()) { - var uriBuilder = new URIBuilder(OKTA_HOSTNAME + "/oauth2/v1/authorize"); - uriBuilder.setParameter("client_id", OKTA_CLIENT_ID) + var uriBuilder = new URIBuilder(OIDC_HOSTNAME + OKTA_GENERATE_TOKEN_URL); + uriBuilder.setParameter("client_id", OIDC_CLIENT_ID) .setParameter("redirect_uri", "https://localhost:10010/login/oauth2/code/okta") .setParameter("response_type", "token") .setParameter("response_mode", "form_post") @@ -474,6 +471,56 @@ public static String validOktaAccessToken(boolean userHasMappingDefined) { } } + public static boolean isKeycloakProvider() { + return "keycloak".equalsIgnoreCase(OIDC_PROVIDER_NAME); + } + + public static boolean isOktaProvider() { + return "okta".equalsIgnoreCase(OIDC_PROVIDER_NAME); + } + + public static String validKeycloakToken(boolean userHasMappingDefined) { + assertNotNull(OIDC_HOSTNAME, "Keycloak host name is not set."); + assertNotNull(OIDC_CLIENT_ID, "Keycloak client id is not set."); + + try (CloseableHttpClient httpClient = HttpClientBuilder.create().setSSLContext(getRelaxedSslContext()).build()) { + var uri = new URI(OIDC_HOSTNAME + KEYCLOAK_GENERATE_TOKEN_URL); + var request = new HttpPost(uri); + var form = new StringBuilder(); + form.append("grant_type=password"); + form.append("&scope=discoverableclient"); + form.append("&client_id=").append(URLEncoder.encode(OIDC_CLIENT_ID, StandardCharsets.UTF_8)); + form.append("&client_secret=").append(URLEncoder.encode(OIDC_CLIENT_PASSWORD, StandardCharsets.UTF_8)); + if (userHasMappingDefined) { + form.append("&username=").append(URLEncoder.encode(OIDC_USER, StandardCharsets.UTF_8)); + form.append("&password=").append(URLEncoder.encode(OIDC_PASSWORD, StandardCharsets.UTF_8)); + } else { + form.append("&username=").append(URLEncoder.encode(OIDC_ALT_USER, StandardCharsets.UTF_8)); + form.append("&password=").append(URLEncoder.encode(OIDC_ALT_PASSWORD, StandardCharsets.UTF_8)); + } + var entity = new StringEntity( + form.toString(), + ContentType.APPLICATION_FORM_URLENCODED + ); + request.setEntity(entity); + request.addHeader("content-type", "application/x-www-form-urlencoded"); + + var response = httpClient.execute(request); + + if (response.getStatusLine().getStatusCode() == 200) { + // The response is HTML form where access token is hidden input field (this is controlled by response_mode = form_post) + var body = EntityUtils.toString(response.getEntity()); + var accessToken = MAPPER.readValue(body,HashMap.class).get("access_token"); + assertNotNull(accessToken, "Failed to locate access token in the Keycloak /authorize response."); + return (String)accessToken; + } else { + throw new RuntimeException("Failed obtaining Keycloak access token: " + response.getStatusLine().getStatusCode() + ": " + EntityUtils.toString(response.getEntity())); + } + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + } + private static String getOktaSession(String username, String password) { assertNotNull(username, "OKTA username is not set."); assertNotNull(password, "OKTA password is not set."); @@ -486,7 +533,7 @@ private static String getOktaSession(String username, String password) { } try (CloseableHttpClient httpClient = HttpClientBuilder.create().setSSLContext(getRelaxedSslContext()).build()) { - var uriBuilder = new URIBuilder(OKTA_HOSTNAME + "/api/v1/authn"); + var uriBuilder = new URIBuilder(OIDC_HOSTNAME + OKTA_AUTHENTICATE_SESSION_URL); var request = new HttpPost(uriBuilder.build()); request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json"); @@ -507,10 +554,23 @@ private static String getOktaSession(String username, String password) { } } + public static String expiredOidcAccessToken() { + if (isOktaProvider()) { + return expiredOktaAccessToken(); + } else if (isKeycloakProvider()) { + return expiredKeycloakAccessToken(); + } + throw new IllegalArgumentException(String.format("Unsupported OIDC provider: %s", OIDC_PROVIDER_NAME)); + } + public static String expiredOktaAccessToken() { return "eyJraWQiOiJGTUM5UndncFVJMUt0V25QWkdmVmFKYzZUZGlTTElZU29jeWs4aHlEbE44IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULkVzZ051RGxkcm5FN0VDYlhnNUhEdUY4MW9BV3k1UDF4WUZLT1psTmVJcmMiLCJpc3MiOiJodHRwczovL2Rldi05NTcyNzY4Ni5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2Njc5MTc5MjcsImV4cCI6MTY2NzkyMTUyNywiY2lkIjoiMG9hNmE0OG1uaVhBcUVNcng1ZDciLCJ1aWQiOiIwMHU3NmVvZjB6bnNNYkY3NDVkNyIsInNjcCI6WyJvcGVuaWQiXSwiYXV0aF90aW1lIjoxNjY3OTE3ODg2LCJzdWIiOiJpdF90ZXN0QGFjbWUuY29tIiwiZ3JvdXBzIjpbIkV2ZXJ5b25lIl19.KiPa0c1U5IClozwZI5aDRSwjoi-hYtIkQZWpizGF8PPsgzvfMaivUzMoPi5GfEUZF6Bjlg_fQFUK7kJQ8NWjL6gY_5QQMfONw0U9dzQy2HLHb5gU55IKt6mBIutBSPk2FmCTd4SaPmllMb6nAyhIZf0DI7xuAXqRgt5JnasnmCKSIM3HJMlTeXDzHQ5BvMr7tVHWmwQ-8W3nef5nsKi2Sw05rds9RgkcckGUzhA2tMeF_rVTitufeG7h2oXYICtv60wfK6YSnmE78aoHf5NQD5517gnGrRxGMM6UAn3SV4GKOll6OlGDzpz87mq-AR2tigkDfVcOtJA9mkxFFv7HSg"; } + public static String expiredKeycloakAccessToken() { + return "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJZzc0dlkyMEZ5bXNxSmN5ZDc1MjVGbGloWElPdk13cFZMVVc5TkN2VVFrIn0.eyJleHAiOjE3NTgxOTgyMzUsImlhdCI6MTc1ODE5NzkzNSwianRpIjoib25ydHJvOjBiYjQzNTE4LTQ0MTQtODg1Zi01YThlLWQ1YmExYWMzOGI2NCIsImlzcyI6Imh0dHA6Ly8xMC4yNTIuMTIxLjE5MDo4MDgwL3JlYWxtcy9hcGltbCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJmYzA4NWE1NC01N2E5LTRhMTEtOGQzMy1hMGU5Njc1YmMwZTkiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjbGllbnRpZGFwaW1sIiwic2lkIjoiOGYzYzlkZmItYzEwNS00NzIyLWE5NTYtODhmZTc1ZjcwMTcwIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1hcGltbCIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZGlzY292ZXJhYmxlY2xpZW50IGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiQVBJTUwgTWluaXBsZXggSW50ZWdyYXRpb24gVGVzdHMgQXV0aG9yaXplZCBVc2VyIiwiZ3JvdXBzIjp7Im1lbWViZXJPZiI6WyIvYXBpbWwiLCIva2V5Y2xvYWtncm91cCIsIi9yZWFkZXJzIiwiL3dhdGNoVG93ZXJVc2VyIl19LCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ6b3dlc29uQHpvd2UuY29tIiwiZ2l2ZW5fbmFtZSI6IkFQSU1MIE1pbmlwbGV4IEludGVncmF0aW9uIFRlc3RzIiwiZmFtaWx5X25hbWUiOiJBdXRob3JpemVkIFVzZXIiLCJ1c2VybmFtZSI6eyJmdWxsTmFtZSI6Inpvd2Vzb25Aem93ZS5jb20ifX0.Yut1S6tIoBdX-h782Zde4g2xvkMu28HduOf4DwwpCUzWSJ-KxPbk5JZl7x_8ga81EuR8myv9o4bowOUA3nLZwOd9iC4_gcj_ZnS3dHIAlPA1PC7cUmppDRl_3GGYdEfAk7a_uLbyc9S1RyrD4FuvJASv4HPoB8pcrF7MJAFuQnWSVOhvDzKq-aRhuAHDXNBBb8SzwfKOuQ5cCeq1AjDrGPUt3XGWySeGxOFzQHPRdqPYTbgmJ7nOqzKJdJ-5hd6tvQRoGmGsq8nU1RCcrayc-27flnR1M3JJEVB3CcAp1DmofijHHO-CSgwbycUSz5zdx1c_tY_iUseQeSWsvshPxA"; + } + public static void logoutOnGateway(String url, String jwtToken) { given() .cookie(GATEWAY_TOKEN_COOKIE_NAME, jwtToken) 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 7d3ddaeccc..913a548707 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 @@ -83,9 +83,8 @@ public static EnvironmentConfiguration environmentConfiguration() { AuxiliaryUserList auxiliaryUserList = new AuxiliaryUserList("user,password"); ZosmfServiceConfiguration zosmfServiceConfiguration = new ZosmfServiceConfiguration("https", "zosmf.acme.com", 1443, "ibmzosmf", ""); - IDPConfiguration idpConfiguration = new IDPConfiguration("https://okta-dev.com", "user", "user", "alt_user", "alt_user"); + OidcConfiguration oidcConfiguration = new OidcConfiguration("okta","https://okta-dev.com", "","", "user", "user", "alt_user", "alt_user"); SafIdtConfiguration safIdtConfiguration = new SafIdtConfiguration(true); - OidcConfiguration oidcConfiguration = new OidcConfiguration(""); configuration = new EnvironmentConfiguration( credentials, @@ -101,9 +100,8 @@ public static EnvironmentConfiguration environmentConfiguration() { zosmfServiceConfiguration, auxiliaryUserList, null, - idpConfiguration, - safIdtConfiguration, - oidcConfiguration + oidcConfiguration, + safIdtConfiguration ); } @@ -157,16 +155,21 @@ public static EnvironmentConfiguration environmentConfiguration() { configuration.getCachingServiceConfiguration().setUrl(System.getProperty("caching.url", configuration.getCachingServiceConfiguration().getUrl())); - configuration.getIdpConfiguration().setUser(System.getProperty("oidc.test.user", configuration.getIdpConfiguration().getUser())); - configuration.getIdpConfiguration().setPassword(System.getProperty("oidc.test.pass", configuration.getIdpConfiguration().getPassword())); - configuration.getIdpConfiguration().setAlternateUser(System.getProperty("oidc.test.alt_user", configuration.getIdpConfiguration().getAlternateUser())); - configuration.getIdpConfiguration().setAlternatePassword(System.getProperty("oidc.test.alt_pass", configuration.getIdpConfiguration().getAlternatePassword())); - configuration.getIdpConfiguration().setHost(System.getProperty("idpConfiguration.host", configuration.getIdpConfiguration().getHost())); + configuration.getOidcConfiguration().setProviderName(System.getProperty("oidc.providerName", String.valueOf(configuration.getOidcConfiguration().getProviderName()))); + configuration.getOidcConfiguration().setUser(System.getProperty("oidc.test.user", configuration.getOidcConfiguration().getUser())); + configuration.getOidcConfiguration().setPassword(System.getProperty("oidc.test.pass", configuration.getOidcConfiguration().getPassword())); + configuration.getOidcConfiguration().setAlternateUser(System.getProperty("oidc.test.alt_user", configuration.getOidcConfiguration().getAlternateUser())); + configuration.getOidcConfiguration().setAlternatePassword(System.getProperty("oidc.test.alt_pass", configuration.getOidcConfiguration().getAlternatePassword())); + configuration.getOidcConfiguration().setHost(System.getProperty("oidc.host", configuration.getOidcConfiguration().getHost())); + configuration.getOidcConfiguration().setClientId(System.getProperty("oidc.client.id", String.valueOf(configuration.getOidcConfiguration().getClientId()))); + configuration.getOidcConfiguration().setClientSecret(System.getProperty("oidc.client.secret", String.valueOf(configuration.getOidcConfiguration().getClientSecret()))); + var oidcProviderName = configuration.getOidcConfiguration().getProviderName(); + if (!("keycloak".equalsIgnoreCase(oidcProviderName) || "okta".equalsIgnoreCase(oidcProviderName))) { + throw new IllegalArgumentException(String.format("Unsupported OIDC provider: %s", oidcProviderName)); + } configuration.getSafIdtConfiguration().setEnabled(Boolean.parseBoolean(System.getProperty("safidt.enabled", String.valueOf(configuration.getSafIdtConfiguration().isEnabled())))); - configuration.getOidcConfiguration().setClientId(System.getProperty("okta.client.id", String.valueOf(configuration.getOidcConfiguration().getClientId()))); - setZosmfConfigurationFromSystemProperties(configuration); setTlsConfigurationFromSystemProperties(configuration); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/EnvironmentConfiguration.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/EnvironmentConfiguration.java index 21ab637f16..35c6e72ead 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/EnvironmentConfiguration.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/config/EnvironmentConfiguration.java @@ -34,7 +34,6 @@ public class EnvironmentConfiguration { private ZosmfServiceConfiguration zosmfServiceConfiguration; private AuxiliaryUserList auxiliaryUserList; private Map instanceEnv; - private IDPConfiguration idpConfiguration; - private SafIdtConfiguration safIdtConfiguration; private OidcConfiguration oidcConfiguration; + private SafIdtConfiguration safIdtConfiguration; } diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/IDPConfiguration.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/IDPConfiguration.java deleted file mode 100644 index d3b9fabd16..0000000000 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/IDPConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package org.zowe.apiml.util.config; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class IDPConfiguration { - private String host; - private String user; - private String password; - private String alternateUser; - private String alternatePassword; -} diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/OidcConfiguration.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/OidcConfiguration.java index 2fe135d22c..b764995aa7 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/OidcConfiguration.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/config/OidcConfiguration.java @@ -18,7 +18,12 @@ @AllArgsConstructor @NoArgsConstructor public class OidcConfiguration { - + private String providerName; + private String host; private String clientId; - + private String clientSecret; + private String user; + private String password; + private String alternateUser; + private String alternatePassword; } diff --git a/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml b/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml index ffe7ed30b0..e8a015570b 100644 --- a/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml +++ b/integration-tests/src/test/resources/environment-configuration-docker-modulith-ha.yml @@ -57,9 +57,7 @@ zosmfServiceConfiguration: serviceId: mockzosmf auxiliaryUserList: value: 'unauthorized,USER1,validPassword;servicesinfo-authorized,USER,validPassword;servicesinfo-unauthorized,USER1,validPassword' -idpConfiguration: - host: https://dev-95727686.okta.com +oidcConfiguration: + providerName: okta safIdtConfiguration: enabled: true -oidcConfiguration: - clientId: diff --git a/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml b/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml index 6e75bc3ad7..066badf781 100644 --- a/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml +++ b/integration-tests/src/test/resources/environment-configuration-docker-modulith.yml @@ -57,9 +57,7 @@ zosmfServiceConfiguration: serviceId: mockzosmf auxiliaryUserList: value: 'unauthorized,USER1,validPassword;servicesinfo-authorized,USER,validPassword;servicesinfo-unauthorized,USER1,validPassword' -idpConfiguration: - host: https://dev-95727686.okta.com +oidcConfiguration: + providerName: okta safIdtConfiguration: enabled: true -oidcConfiguration: - clientId: diff --git a/integration-tests/src/test/resources/environment-configuration-docker.yml b/integration-tests/src/test/resources/environment-configuration-docker.yml index 6f40352496..0cf93c7f34 100644 --- a/integration-tests/src/test/resources/environment-configuration-docker.yml +++ b/integration-tests/src/test/resources/environment-configuration-docker.yml @@ -61,9 +61,7 @@ zosmfServiceConfiguration: serviceId: mockzosmf auxiliaryUserList: value: 'unauthorized,USER1,validPassword;servicesinfo-authorized,USER,validPassword;servicesinfo-unauthorized,USER1,validPassword' -idpConfiguration: - host: https://dev-95727686.okta.com +oidcConfiguration: + providerName: okta safIdtConfiguration: enabled: true -oidcConfiguration: - clientId: diff --git a/integration-tests/src/test/resources/environment-configuration-ha.yml b/integration-tests/src/test/resources/environment-configuration-ha.yml index 5d8b25a433..c9be90c682 100644 --- a/integration-tests/src/test/resources/environment-configuration-ha.yml +++ b/integration-tests/src/test/resources/environment-configuration-ha.yml @@ -56,9 +56,7 @@ zosmfServiceConfiguration: serviceId: mockzosmf auxiliaryUserList: value: 'servicesinfo-authorized,USER,validPassword;servicesinfo-unauthorized,USER1,validPassword' -idpConfiguration: - host: https://dev-95727686.okta.com +oidcConfiguration: + providerName: okta safIdtConfiguration: enabled: true -oidcConfiguration: - clientId: diff --git a/integration-tests/src/test/resources/environment-configuration.yml b/integration-tests/src/test/resources/environment-configuration.yml index 5c069ea7e9..bb291c02ab 100644 --- a/integration-tests/src/test/resources/environment-configuration.yml +++ b/integration-tests/src/test/resources/environment-configuration.yml @@ -61,12 +61,14 @@ zosmfServiceConfiguration: serviceId: mockzosmf auxiliaryUserList: value: 'servicesinfo-authorized,USER,validPassword;servicesinfo-unauthorized,USER1,validPassword' -idpConfiguration: - host: https://dev-95727686.okta.com +oidcConfiguration: + providerName: okta + host: https://dev-15878923.okta.com + clientId: 0oalf0sc35aNKevXs5d7 + user: zoweson@zowe.com + password: hKE6tisHdujbmSg safIdtConfiguration: enabled: true -oidcConfiguration: - clientId: instanceEnv: # Try to avoid adding quotes to the values of these env variables CMMN_LB: build/libs/api-layer-lite-lib-all.jar diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/NativeMapper.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/NativeMapper.java index 28e1c7d778..533df9ff27 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/NativeMapper.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/NativeMapper.java @@ -40,7 +40,7 @@ public CertificateResponse getUserIDForCertificate(byte[] cert) { @Override public MapperResponse getUserIDForDN(String dn, String registry) { MapperResponse response = userMapper.getUserIDForDN(dn, registry); - log.debug("{}", response); + log.debug("Mapping {} from registry {}: {}",dn, registry, response); return response; } } From 4e2e096fbb9cc6746c25e1ceca1c9db30480bfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sala=C4=8D?= Date: Tue, 23 Sep 2025 10:16:58 +0200 Subject: [PATCH 121/152] chore: Upgrade java dependencies (#4326) Signed-off-by: Richard Salac Signed-off-by: Gowtham Selvaraj --- gradle/versions.gradle | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/gradle/versions.gradle b/gradle/versions.gradle index fc9b9ff39b..efaa8bdce6 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -10,8 +10,8 @@ dependencyResolutionManagement { version('springCloudNetflix', '4.3.0') version('springCloudCommons', '4.3.0') version('springCloudCB', '3.3.0') - version('springCloudGateway', '4.3.0') - version('springFramework', '6.2.10') + version('springCloudGateway', '4.3.1') + version('springFramework', '6.2.11') version('springRetry', '2.0.12') version('modulith', '1.4.3') @@ -19,11 +19,11 @@ dependencyResolutionManagement { version('glassfishHk2', '3.1.1') version('zosUtils', '2.0.7') - version('aws', '1.12.789') + version('aws', '1.12.791') version('awaitility', '4.3.0') version('bouncyCastle', '1.81') version('caffeine', '3.2.2') - version('checkerQual', '3.50.0') + version('checkerQual', '3.51.0') version('commonsLang3', '3.18.0') version('commonsLogging', '1.3.5') version('commonsText', '1.14.0') @@ -34,7 +34,7 @@ dependencyResolutionManagement { version('netflixServo', '0.13.2') version('googleErrorprone', '2.41.0') version('gradleGitProperties', '2.5.2') // Used in classpath dependencies - version('googleGson', '2.13.1') + version('googleGson', '2.13.2') version('guava', '33.4.8-jre') version('hamcrest', '3.0') version('httpClient4', '4.5.14') @@ -60,7 +60,7 @@ dependencyResolutionManagement { } version('jbossLogging', '3.6.1.Final') version('jerseySun', '1.19.4') - version('jettyWebSocketClient', '12.1.0') + version('jettyWebSocketClient', '12.1.1') version('jettison', '1.5.4') //0.12.x version contains breaking changes version('jjwt', '0.13.0') @@ -73,22 +73,22 @@ dependencyResolutionManagement { version('lettuce', '6.8.1.RELEASE') // force version in build.gradle file - compatibility with Slf4j version('log4j', '2.25.1') - version('lombok', '1.18.38') - version('netty', '4.2.5.Final') + version('lombok', '1.18.40') + version('netty', '4.2.6.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 - version('nettyReactor', '1.2.9') - version('nimbusJoseJwt', '10.4.2') + version('nettyReactor', '1.2.10') + version('nimbusJoseJwt', '10.5') version('openApiDiff', '2.1.3') version('picocli', '4.7.7') - version('reactor', '3.7.9') + version('reactor', '3.7.11') version('restAssured', '5.5.6') version('rhino', '1.8.0') - version('springDoc', '2.8.12') - version('swaggerCore', '2.2.36') + version('springDoc', '2.8.13') + version('swaggerCore', '2.2.37') version('swaggerInflector', '2.0.14') version('swagger2Parser', '1.0.75') - version('swagger3Parser', '2.1.33') + version('swagger3Parser', '2.1.34') version('thymeleaf', '3.1.3.RELEASE') version('velocity', '2.4.1') version('woodstoxCore', '7.1.1') @@ -102,7 +102,7 @@ dependencyResolutionManagement { version('gradleTestLogger', '4.0.0') version('testLogger', '4.0.0') version('micronautPlatform', '4.9.0') - version('micronaut', '4.9.10') + version('micronaut', '4.9.11') version('micronautPlugin', '4.5.4') version('shadow', '8.1.1') version('checkstyle', '10.17.0') From 8ebfcd949922cd113dc7deee9a55f7951fa60c04 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 26 Sep 2025 00:43:17 +0000 Subject: [PATCH 122/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.11'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0406f21ff1..d4721a0f9c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.11-SNAPSHOT +version=3.3.11 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From c283afafaa32d48405c4a5dd86b1484d2bbdd238 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 26 Sep 2025 00:43:19 +0000 Subject: [PATCH 123/152] [Gradle Release plugin] Create new version: 'v3.3.12-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d4721a0f9c..3dd1ec9453 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.11 +version=3.3.12-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 6a5365194a6b5e2a95016b3bac9b0ab2bfa908da Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 26 Sep 2025 00:43:21 +0000 Subject: [PATCH 124/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 022f6bfae1..d5c3c40f7e 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.11-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.12-SNAPSHOT From b52247e372e4513a4f2a6c826833dfeb0ab486bc Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 26 Sep 2025 12:26:33 +0200 Subject: [PATCH 125/152] fix: z/OSMF static definition for AT-TLS (#4327) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Co-authored-by: Nafi Xhafa <164854562+nxhafa@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- .../src/main/resources/zosmf-static-definition.yaml.template | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discovery-package/src/main/resources/zosmf-static-definition.yaml.template b/discovery-package/src/main/resources/zosmf-static-definition.yaml.template index d16f64446a..ef9425c24c 100644 --- a/discovery-package/src/main/resources/zosmf-static-definition.yaml.template +++ b/discovery-package/src/main/resources/zosmf-static-definition.yaml.template @@ -25,6 +25,10 @@ services: apiml: enableUrlEncodedCharacters: true headersToIgnore: Origin + service: + scheme: ${ZOSMF_SCHEME} + nonSecurePortEnabled: ${ZOSMF_NON_SECURE_PORT_ENABLED:-false} + securePortEnabled: ${ZOSMF_SECURE_PORT_ENABLED:-true} catalogUiTiles: zosmf: title: z/OSMF services From 89869606cc2534110e163869dc68db30310008b4 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 29 Sep 2025 09:41:30 +0200 Subject: [PATCH 126/152] fix: set redirectUri default in java code (#4329) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- apiml/src/main/resources/application.yml | 6 ----- .../config/oidc/ClientConfiguration.java | 26 ++++++++++++++++--- .../src/main/resources/application.yml | 7 ----- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 4de23e1bc9..498d19b247 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -33,12 +33,6 @@ spring: frame-options: sameorigin application: name: gateway - security: - oauth2: - client: - registration: - okta: - redirectUri: "{baseUrl}/gateway/{action}/oauth2/code/{registrationId}" main: allow-circular-references: true banner-mode: ${apiml.banner:"off"} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/oidc/ClientConfiguration.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/oidc/ClientConfiguration.java index 02fed5d1dd..73174f8d6f 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/oidc/ClientConfiguration.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/oidc/ClientConfiguration.java @@ -16,17 +16,23 @@ import lombok.Data; import lombok.Value; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** - * Reads OIDC Client configuration from environment variables or application configuration file. + * Reads OIDC Client configuration from Zowe launcher environment variables or application configuration file. */ @Data @Component @@ -34,6 +40,7 @@ @ConfigurationProperties(prefix = "spring.security.oauth2.client", ignoreInvalidFields = true) public class ClientConfiguration { + private static final String DEFAULT_REDIRECT_URI = "{baseUrl}/gateway/{action}/oauth2/code/{registrationId}"; private static final String SYSTEM_ENV_PREFIX = "ZWE_configs_spring_security_oauth2_client_"; private static final Pattern REGISTRATION_ID_PATTERN = Pattern.compile( "^" + SYSTEM_ENV_PREFIX + "(registration|provider)_([^_]+)_.*$" @@ -42,10 +49,10 @@ public class ClientConfiguration { public static final String REGISTRATION_ENV_TYPE = "registration"; public static final String PROVIDER_ENV_TYPE = "provider"; - private Map registration = new HashMap<>(); private Map provider = new HashMap<>(); + private String getSystemEnv(String id, String type, String name) { StringBuilder sb = new StringBuilder(); sb.append(SYSTEM_ENV_PREFIX).append(type).append('_').append(id).append('_').append(name); @@ -97,6 +104,19 @@ void updateWithSystemEnvironment() { update(registrationId, registration.computeIfAbsent(registrationId, k -> new Registration())); update(registrationId, provider.computeIfAbsent(registrationId, k -> new Provider())); } + processDefaults(); + } + + /* + * redirectUri was originally set as a property but for Okta provider only, without it it can be a breaking change. + * This makes sure any provider has a default redirectUri if no explicit one is provided + */ + private void processDefaults() { + for (Map.Entry entry : registration.entrySet()) { + if (StringUtils.isBlank(entry.getValue().getRedirectUri())) { + entry.getValue().setRedirectUri(DEFAULT_REDIRECT_URI); + } + } } public Map getConfigurations() { diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index ad569e21db..3f1fb254a4 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -68,12 +68,6 @@ spring: frame-options: sameorigin application: name: gateway - security: - oauth2: - client: - registration: - okta: - redirectUri: "{baseUrl}/gateway/{action}/oauth2/code/{registrationId}" main: allow-circular-references: true banner-mode: ${apiml.banner:"off"} @@ -200,7 +194,6 @@ management: include: health,info,gateway --- spring.config.activate.on-profile: wiretap - spring: cloud: gateway: From 9e7173c28d9a987ad496cd9f6616a711ad10b12b Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:01:15 +0200 Subject: [PATCH 127/152] fix: respect encoded slashes in redirect header (#4328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: ac892247 Co-authored-by: Pablo Carle Co-authored-by: Pavel Jareš <58428711+pj892031@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- .../detail-page/service-version-compare.cy.js | 2 +- .../routing/transform/TransformService.java | 63 ++- .../transform/TransformServiceTest.java | 382 +++++++----------- config/docker/api-defs/staticclient.yml | 20 + config/local/api-defs/staticclient.yml | 18 + .../filters/PageRedirectionFilterFactory.java | 47 ++- .../PageRedirectionFilterFactoryTest.java | 141 ++++--- .../gateway/PageRedirectionTest.java | 3 +- .../apiml/integration/proxy/RedirectTest.java | 139 +++++++ .../apiml/client/api/RedirectController.java | 50 +++ 10 files changed, 532 insertions(+), 333 deletions(-) create mode 100644 integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java create mode 100644 mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index eb6702362d..b9ecd51175 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -38,7 +38,7 @@ describe('>>> Service version compare Test', () => { 'exist' ); - const expectedServicesCount = 17; + const expectedServicesCount = 18; cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div .find('a.MuiTab-root') // Find all the anchor elements within the div diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index b92af1420d..848149d9f2 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -40,10 +40,10 @@ public class TransformService { /** * Construct the URL using gateway hostname and route * - * @param type the type of the route - * @param serviceId the service id - * @param serviceUrl the service URL - * @param routes the routes + * @param type the type of the route + * @param serviceId the service id + * @param serviceUrl the service URL + * @param routes the routes * @param httpsScheme https scheme flag * @return the new URL * @throws URLTransformationException if the path of the service URL is not valid @@ -59,7 +59,7 @@ public String transformURL(ServiceType type, } URI serviceUri = URI.create(serviceUrl); - String serviceUriPath = serviceUri.getPath(); + String serviceUriPath = serviceUri.getRawPath(); if (serviceUriPath == null) { String message = String.format("The URI %s is not valid.", serviceUri); throw new URLTransformationException(message); @@ -71,40 +71,61 @@ public String transformURL(ServiceType type, throw new URLTransformationException(message); } - if (serviceUri.getQuery() != null) { - serviceUriPath += "?" + serviceUri.getQuery(); + if (StringUtils.isNotBlank(serviceUri.getRawQuery())) { + serviceUriPath += "?" + serviceUri.getRawQuery(); } return transformURL(serviceId, serviceUriPath, route, httpsScheme, serviceUri); } - public String transformURL(String serviceId, - String serviceUriPath, - RoutedService route, - boolean httpsScheme, - URI originalUri + private String transformURL(String serviceId, + String serviceUriPath, + RoutedService route, + boolean httpsScheme, + URI originalUri ) throws URLTransformationException { - if (!gatewayClient.isInitialized()) { - apimlLog.log("org.zowe.apiml.common.gatewayNotFoundForTransformRequest"); - throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); - } - String endPoint = getShortEndPoint(route.getServiceUrl(), serviceUriPath); + String endPoint = getShortEndpoint(route.getServiceUrl(), serviceUriPath); if (!endPoint.isEmpty() && !endPoint.startsWith("/")) { throw new URLTransformationException("The path " + originalUri.getPath() + " of the service URL " + originalUri + " is not valid."); } ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - + if (originalUri != null && originalUri.toString().startsWith("//")) { + return String.format("//%s/%s%s%s", + gatewayConfigProperties.getHostname(), + serviceId, + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), + endPoint); + } String scheme = httpsScheme ? "https" : gatewayConfigProperties.getScheme(); - return String.format("%s://%s/%s/%s%s", + return String.format("%s://%s/%s%s%s", scheme, gatewayConfigProperties.getHostname(), serviceId, - route.getGatewayUrl(), + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), endPoint); } + public String transformAbsoluteURL(String serviceId, + String locationUri, + RoutedService route + ) throws URLTransformationException { + + String endpoint = getShortEndpoint(route.getServiceUrl(), locationUri); + if (isRelative(endpoint)) { + throw new URLTransformationException("The path " + locationUri + " of the service " + serviceId + " is not valid."); + } + return String.format("/%s%s%s", + serviceId, + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), + endpoint); + } + + boolean isRelative(String endpoint) { + return !endpoint.isEmpty() && !endpoint.startsWith("/"); + } + /** * Construct the API base path using the route * @@ -146,7 +167,7 @@ public String retrieveApiBasePath(String serviceId, * @param endPoint the endpoint of method * @return short endpoint */ - private String getShortEndPoint(String routeServiceUrl, String endPoint) { + private String getShortEndpoint(String routeServiceUrl, String endPoint) { String shortEndPoint = endPoint; if (!SEPARATOR.equals(routeServiceUrl) && StringUtils.isNotBlank(routeServiceUrl)) { shortEndPoint = shortEndPoint.replaceFirst(UrlUtils.removeLastSlash(routeServiceUrl), ""); diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java index c98866d014..b41ff2c72f 100644 --- a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java +++ b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java @@ -28,293 +28,192 @@ class TransformServiceTest { private static final String UI_PREFIX = "ui"; private static final String API_PREFIX = "api"; private static final String WS_PREFIX = "ws"; - private static final String SERVICE_ID = "service"; private GatewayClient gatewayClient; + private TransformService transformService; @BeforeEach void setup() { - ServiceAddress gatewayConfigProperties = ServiceAddress.builder() + ServiceAddress gatewayConfig = ServiceAddress.builder() .scheme("https") - .hostname("localhost") + .hostname("localhost:10010") .build(); - gatewayClient = new GatewayClient(gatewayConfigProperties); - } - - @Test - void givenHomePageAndUIRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/ui"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - - String expectedUrl = String.format("%s://%s/%s/%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX); - assertEquals(expectedUrl, actualUrl); + gatewayClient = new GatewayClient(gatewayConfig); + transformService = new TransformService(gatewayClient); } - @Test - void givenHomePageWithAttlsEnabled_whenTransform_thenUseNewUrlWithHttps() throws URLTransformationException { - String url = "http://localhost:8080/ui"; + private RoutedServices buildRoutedServices(RoutedService... routes) { RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, true); - - String expectedUrl = String.format("%s://%s/%s/%s", - "https", - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX); - assertEquals(expectedUrl, actualUrl); - } - - @Test - void givenHomePage_whenRouteNotFound_thenThrowException() { - String url = "https://localhost:8080/u"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - }); - assertEquals("Not able to select route for url https://localhost:8080/u of the service service. Original url used.", exception.getMessage()); + for (RoutedService route : routes) { + routedServices.addRoutedService(route); + } + return routedServices; } - @Test - void givenHomePageAndWSRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/ws"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - - String expectedUrl = String.format("%s://%s/%s/%s", + private String expectedUrl(String serviceId, String prefix, String path) { + return String.format("%s://%s/%s/%s%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - WS_PREFIX); - assertEquals(expectedUrl, actualUrl); + serviceId, + prefix, + path == null ? "" : path + ); } - @Test - void givenHomePageAndAPIRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/api"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, API_PREFIX, "/api"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.API, SERVICE_ID, url, routedServices, false); - - String expectedUrl = String.format("%s://%s/%s/%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - API_PREFIX); - assertEquals(expectedUrl, actualUrl); + @ParameterizedTest + @CsvSource({ + "https://localhost:8080/ui,UI,ui,''", + "https://localhost:8080/ws,WS,ws,''", + "https://localhost:8080/api,API,api,''", + "https://locahost:8080/ui/service/login.do?action=secure,UI,ui,'/login.do?action=secure'" + }) + void givenValidRoutes_whenTransform_thenReturnExpectedUrl( + String url, ServiceType type, String prefix, String extraPath + ) throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, prefix, url.contains("/ui/service") ? "/ui/service" : "/" + prefix), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + + String actual = transformService.transformURL(type, SERVICE_ID, url, routes, false); + assertEquals(expectedUrl(SERVICE_ID, prefix, extraPath), actual); } - @Test - void givenInvalidHomePage_thenThrowException() { - String url = "https:localhost:8080/wss"; - - TransformService transformService = new TransformService(gatewayClient); - - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(null, null, url, null, false); - }); - assertEquals("The URI " + url + " is not valid.", exception.getMessage()); + void givenSchemeRelativeUrl_whenTransform_thenKeepSchemeRelativeUrl() throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, "api", "/api"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + String actual = transformService.transformURL(ServiceType.API, SERVICE_ID, "//localhost:8080/api", routes, false); + assertEquals("//localhost:10010/service/api", actual); } - @Test - void givenEmptyGatewayClient_thenThrowException() { - String url = "https://localhost:8080/wss"; - - GatewayClient emptyGatewayClient = new GatewayClient(null); - TransformService transformService = new TransformService(emptyGatewayClient); - - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(null, null, url, null, false); - }); - assertEquals("Gateway not found yet, transform service cannot perform the request", exception.getMessage()); + @ParameterizedTest + @CsvSource({ + "https://localhost:8080/,WS,ws,'/'", + "https://locahost:8080/test,UI,ui,'/test'", + }) + void givenMissingRoutes_whenTransform_thenThrow( + String url, ServiceType type, String prefix, String extraPath + ) { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, prefix, url.contains("/ui/service") ? "/ui/service" : "/" + prefix), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + assertThrows(URLTransformationException.class, () -> transformService.transformURL(type, SERVICE_ID, url, routes, false)); } - @Test - void givenHomePage_whenPathIsNotValid_thenThrowException() { - String url = "https://localhost:8080/wss"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenHttpUrlAndAttlsEnabled_whenTransform_thenForceHttps() throws URLTransformationException { + String url = "http://localhost:8080/ui"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(gatewayClient); + String actual = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routes, true); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - }); - assertEquals("The path /wss of the service URL https://localhost:8080/wss is not valid.", exception.getMessage()); + assertEquals("https://localhost:10010/service/ui", actual); } @Test - void givenEmptyPathInHomePage_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenRouteNotFound_whenTransform_thenThrowException() { + String url = "https://localhost:8080/u"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routes, false)); - String actualUrl = transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - WS_PREFIX, - "/"); - assertEquals(expectedUrl, actualUrl); + assertEquals("Not able to select route for url https://localhost:8080/u of the service service. Original url used.", + ex.getMessage()); } @Test - void givenServiceUrl_whenItsRoot_thenKeepHomePagePathSame() throws URLTransformationException { - String url = "https://locahost:8080/test"; - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenInvalidUrl_whenTransform_thenThrowException() { + String url = "https:localhost:8080/wss"; + TransformService service = new TransformService(gatewayClient); - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.transformURL(null, null, url, null, false)); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX, - "/test"); - assertEquals(expectedUrl, actualUrl); + assertEquals("The URI " + url + " is not valid.", ex.getMessage()); } @Test - void givenUrlContainingPathAndQuery_whenTransform_thenKeepQueryPartInTheNewUrl() throws URLTransformationException { - String url = "https://locahost:8080/ui/service/login.do?action=secure"; - String path = "/login.do?action=secure"; - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui/service"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenEmptyGatewayClient_whenTransform_thenThrowException() { + GatewayClient emptyClient = new GatewayClient(null); + TransformService service = new TransformService(emptyClient); - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.transformURL(null, null, "https://localhost:8080/wss", null, false)); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX, - path); - assertEquals(expectedUrl, actualUrl); + assertEquals("Gateway not found yet, transform service cannot perform the request", ex.getMessage()); } @Test - void givenServiceAndApiRoute_whenGetApiBasePath_thenReturnApiPath() throws URLTransformationException { - String url = "https://localhost:8080/" + SERVICE_ID + "/" + API_PREFIX + "/v1"; - - String serviceUrl = String.format("/%s/%s", SERVICE_ID, API_PREFIX); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService = new RoutedService(SERVICE_ID, API_PREFIX, serviceUrl); - routedServices.addRoutedService(routedService); + void givenInvalidPath_whenTransform_thenThrowException() { + String url = "https://localhost:8080/wss"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routes, false)); - String actualPath = transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - String expectedPath = String.format("/%s/%s", - SERVICE_ID, - API_PREFIX); - assertEquals(expectedPath, actualPath); + assertEquals("The path /wss of the service URL https://localhost:8080/wss is not valid.", ex.getMessage()); } - @Test - void givenServiceAndApiRouteWithVersion_whenGetApiBasePath_thenReturnApiPath() throws URLTransformationException { - String url = "https://localhost:8080/" + SERVICE_ID + "/" + API_PREFIX + "/v1"; - - String serviceUrl = String.format("/%s/%s/%s", SERVICE_ID, API_PREFIX, "v1"); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService = new RoutedService(SERVICE_ID, API_PREFIX + "/v1", serviceUrl); - routedServices.addRoutedService(routedService); + @ParameterizedTest + @CsvSource({ + // url, routePath, expected + "https://localhost:8080/service/api/v1,/service/api,/service/api/{api-version}", + "https://localhost:8080/service/api/v1,/service/api/v1,/service/api/{api-version}" + }) + void givenApiRoute_whenRetrieveApiBasePath_thenReturnExpected( + String url, String routePath, String expected + ) throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, API_PREFIX + "/v1", routePath) + ); - TransformService transformService = new TransformService(null); + TransformService service = new TransformService(null); + String actual = service.retrieveApiBasePath(SERVICE_ID, url, routes); - String actualPath = transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - String expectedPath = String.format("/%s/%s/%s", SERVICE_ID, API_PREFIX, "{api-version}"); - assertEquals(expectedPath, actualPath); + assertEquals(expected, actual); } @Test - void givenInvalidUriPath_whenGetApiBasePath_thenThrowError() { + void givenInvalidUrl_whenRetrieveApiBasePath_thenThrowException() { String url = "https:localhost:8080/wss"; + TransformService service = new TransformService(null); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.retrieveApiBasePath(null, url, null)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.retrieveApiBasePath(null, url, null); - }); - assertEquals("The URI " + url + " is not valid.", exception.getMessage()); + assertEquals("The URI " + url + " is not valid.", ex.getMessage()); } @Test - void givenNoRoutes_whenGetApiBasePath_thenThrowError() { + void givenNoMatchingRoute_whenRetrieveApiBasePath_thenThrowException() { String url = "https://localhost:8080/u"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/api") + ); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/api"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + TransformService service = new TransformService(null); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.retrieveApiBasePath(SERVICE_ID, url, routes)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - }); - assertEquals("Not able to select API base path for the service " + SERVICE_ID + ". Original url used.", exception.getMessage()); + assertEquals("Not able to select API base path for the service " + SERVICE_ID + ". Original url used.", + ex.getMessage()); } @ParameterizedTest @@ -324,16 +223,17 @@ void givenNoRoutes_whenGetApiBasePath_thenThrowError() { "srv,wrong/url,api/v1,api/v1,", "srv,apiV1/home/page.html,api/v1,apiV1,/srv/api/{api-version}" }) - void testRetrieveApiBasePath(String serviceId, String url, String gatewayUrl, String serviceUrl, String expectedBasePath) { - RoutedService route = new RoutedService("api", gatewayUrl, serviceUrl); - - RoutedServices routedServices = new RoutedServices(); - routedServices.addRoutedService(route); - - TransformService transformService = new TransformService(null); + void testRetrieveApiBasePathParameterized( + String serviceId, String url, String gatewayUrl, String serviceUrl, String expectedBasePath + ) { + RoutedServices routes = buildRoutedServices( + new RoutedService("api", gatewayUrl, serviceUrl) + ); + + TransformService service = new TransformService(null); String basePath; try { - basePath = transformService.retrieveApiBasePath(serviceId, url, routedServices); + basePath = service.retrieveApiBasePath(serviceId, url, routes); } catch (URLTransformationException e) { basePath = null; } @@ -341,4 +241,24 @@ void testRetrieveApiBasePath(String serviceId, String url, String gatewayUrl, St assertEquals(expectedBasePath, basePath); } + @Test + void givenLocationUriNotMatchingServiceRoutes_whenTransformAbsoluteURL_thenThrowException() { + + RoutedService route = new RoutedService("dcpassticket", "api", "/api"); + + var ex = assertThrows(URLTransformationException.class, () -> + transformService.transformAbsoluteURL("dcpassticket", "/apisome-path", route)); + + assertEquals("The path /apisome-path of the service dcpassticket is not valid.", ex.getMessage()); + } + + @Test + void givenMatchingLocation_whenTransformAbsoluteURL_thenReturnAbsoluteURL() throws URLTransformationException { + + RoutedService route = new RoutedService("dcpassticket", "api", "/api"); + + var result = transformService.transformAbsoluteURL("dcpassticket", "/api/v1/some-path", route); + assertEquals("/dcpassticket/api/v1/some-path", result); + } + } diff --git a/config/docker/api-defs/staticclient.yml b/config/docker/api-defs/staticclient.yml index 7df25b9083..a478cb9f72 100644 --- a/config/docker/api-defs/staticclient.yml +++ b/config/docker/api-defs/staticclient.yml @@ -216,6 +216,26 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: redirectclient # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Redirect client # Title of the service in the API catalog + description: REST API in the mockservices to test redirect on a service without context path. + instanceBaseUrls: # list of base URLs for each instance + - https://mock-services:10013/ # 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: ui # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: # relativePath that is added to baseUrl of an instance + authentication: + scheme: zosmf + apiInfo: + - apiId: zowe.apiml.redirectclient + gatewayUrl: api/v1 + version: 1.0.0 + swaggerUrl: https://mock-services:10013/zosmf/api/docs + # Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 5f16df1b5f..bb778ce0a8 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -217,6 +217,24 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: redirectclient # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Redirect client # Title of the service in the API catalog + description: REST API in the mockservices to test redirect on a service without context path. + instanceBaseUrls: # list of base URLs for each instance + - https://localhost:10013/ # 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: ui # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: # relativePath that is added to baseUrl of an instance + apiInfo: + - apiId: zowe.apiml.redirectclient + gatewayUrl: ui + version: 1.0.0 + swaggerUrl: https://localhost:10013/zosmf/api/docs + # Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 8f28d6b61a..f9659801ea 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -24,6 +24,7 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; +import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.ServiceType; @@ -73,14 +74,14 @@ public GatewayFilter apply(Config config) { .then(Mono.defer(() -> processNewLocationUrl(exchange, config, instance))); } - private URI getHostUri(ServiceInstance instance) { + private URI getHostAndPortUri(ServiceInstance instance) { return UriComponentsBuilder.newInstance() .host(instance.getHost()) .port(instance.getPort()) .build().toUri(); } - private URI getHostUri(URI uri) { + private URI getHostAndPortUri(URI uri) { return UriComponentsBuilder.newInstance() .host(uri.getHost()) .port(uri.getPort()) @@ -92,18 +93,29 @@ private Optional getInstance(URI locationUri, Optional getHostUri(i).equals(hostUri)).orElse(false)) { + var locationHostAndPortUri = getHostAndPortUri(locationUri); + if (matchesInstance(instance, locationHostAndPortUri)) { return instance; } return discoveryClient.getServices().stream() .map(discoveryClient::getInstances) .flatMap(List::stream) - .filter(i -> hostUri.equals(getHostUri(i))) + .filter(i -> locationHostAndPortUri.equals(getHostAndPortUri(i))) .findFirst(); } + /** + * Compares URI with instance + * + * @param instance + * @param hostAndPortUri + * @return true if URI equals URI from instance + */ + private boolean matchesInstance(Optional instance, URI hostAndPortUri) { + return instance.map(i -> getHostAndPortUri(i).equals(hostAndPortUri)).orElse(false); + } + private String normalizePath(String path) { if (!path.startsWith(SLASH)) { path = SLASH + path; @@ -116,7 +128,7 @@ private String normalizePath(String path) { private boolean isMatching(RoutedService route, URI uri) { var servicePath = normalizePath(route.getServiceUrl()); - var locationPath = normalizePath(uri.getPath()); + var locationPath = normalizePath(uri.getRawPath()); return locationPath.startsWith(servicePath); } @@ -129,24 +141,28 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf var location = response.getHeaders().getFirst(HttpHeaders.LOCATION); if (StringUtils.isBlank(location)) { + log.debug("Location header is empty"); return Mono.empty(); } var locationUri = URI.create(location); var targetInstance = getInstance(locationUri, instance); + if (isGateway(targetInstance)) { + log.debug("Target instance is Gateway. Location header was not translated. {}", locationUri); + return Mono.empty(); + } var defaultRoute = config.getRoutedService(); AtomicReference newUrl = new AtomicReference<>(); if (targetInstance == instance && isMatching(defaultRoute, locationUri)) { // try the preferable route on the same instance (the same as in the original request) try { - newUrl.set(transformService.transformURL( + newUrl.set(transformService.transformAbsoluteURL( StringUtils.toRootLowerCase(config.serviceId), - UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getQuery()).build().toUri().toString(), - defaultRoute, - false, - locationUri + UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getRawQuery()).build().toUriString(), + defaultRoute )); + log.debug("Location is matching service URL. New Location header value is: {}", newUrl.get()); } catch (URLTransformationException e) { log.debug("Cannot transform URL on the same route", e); return Mono.empty(); @@ -166,6 +182,7 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf routes, false )); + log.debug("Target instance: {}. New Location header value is: {}", i.getInstanceId(), newUrl.get()); } catch (URLTransformationException e) { log.debug("Cannot transform URL", e); } @@ -174,8 +191,9 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf if (newUrl.get() != null) { // if the new URL was defined, decorate (scheme by AT-TLS) and set - if (isServerAttlsEnabled) { + if (isServerAttlsEnabled && newUrl.get().startsWith("http://")) { newUrl.set(UriComponentsBuilder.fromUriString(newUrl.get()).scheme("https").build().toUriString()); + log.debug("AT-TLS server is enabled. Location url was updated with: {}", newUrl.get()); } exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION, newUrl.toString()); @@ -185,6 +203,11 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf return Mono.empty(); } + boolean isGateway(Optional targetInstance) { + return targetInstance.filter(target -> CoreService.GATEWAY.getServiceId().equalsIgnoreCase(target.getServiceId())) + .isPresent(); + } + @Data public static class Config { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index f162bdc576..7dfd34785a 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -14,6 +14,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.gateway.filter.GatewayFilter; @@ -23,6 +26,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import reactor.core.publisher.Mono; @@ -32,6 +36,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -42,6 +47,8 @@ class PageRedirectionFilterFactoryTest { + public static final String DISCOVERABLECLIENT = "DISCOVERABLECLIENT"; + public static final String GATEWAY = CoreService.GATEWAY.toString(); private GatewayClient gatewayClient; private DiscoveryClient discoveryClient; private static final String GW_HOSTNAME = "gateway"; @@ -51,62 +58,82 @@ class PageRedirectionFilterFactoryTest { private final ServiceAddress serviceAddress = ServiceAddress.builder() .scheme(GW_SCHEME).hostname(GW_HOSTNAME + ":" + GW_PORT).build(); + PageRedirectionFilterFactory factory; + GatewayFilterChain chain; + ServerWebExchange exchange; + ServerHttpResponse res; + ServiceInstance serviceInstance; + @BeforeEach void setUp() { gatewayClient = mock(GatewayClient.class); discoveryClient = mock(DiscoveryClient.class); - } + factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - private void commonSetup(PageRedirectionFilterFactory factory, ServerWebExchange exchange, ServerHttpResponse res, GatewayFilterChain chain, boolean isAttlsEnabled) { - ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", isAttlsEnabled); + chain = mock(GatewayFilterChain.class); + exchange = mock(ServerWebExchange.class); + res = mock(ServerHttpResponse.class); + serviceInstance = mock(ServiceInstance.class); + when(gatewayClient.getGatewayConfigProperties()).thenReturn(serviceAddress); + when(gatewayClient.isInitialized()).thenReturn(true); + when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty()); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_MOVED_PERMANENTLY)); when(exchange.getResponse()).thenReturn(res); - when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty()); - when(gatewayClient.isInitialized()).thenReturn(true); - when(gatewayClient.getGatewayConfigProperties()).thenReturn(serviceAddress); } - private PageRedirectionFilterFactory.Config createConfig() { + + private PageRedirectionFilterFactory.Config createConfig(String serviceId) { var config = new PageRedirectionFilterFactory.Config(); - config.setInstanceId("instanceId"); - config.setServiceId("GATEWAY"); + config.setInstanceId("localhost:" + serviceId.toLowerCase() + ":10012"); + config.setServiceId(serviceId); config.setGatewayUrl("api/v1"); - config.setServiceUrl("/"); + config.setServiceUrl("/discoverableclient"); return config; } - private void setupInstanceInfo() { - var serviceInstance = mock(ServiceInstance.class); + private void mockServiceInstance(String serviceId) { Map metadata = new HashMap<>(); metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api/v1"); metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); when(serviceInstance.getMetadata()).thenReturn(metadata); when(serviceInstance.getInstanceId()).thenReturn("instanceId"); + when(serviceInstance.getServiceId()).thenReturn(serviceId); when(serviceInstance.getHost()).thenReturn("localhost"); when(serviceInstance.getPort()).thenReturn(10010); - when(discoveryClient.getInstances("GATEWAY")).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + when(discoveryClient.getInstances(serviceId)).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + when(discoveryClient.getServices()).thenReturn(Collections.singletonList(serviceId)); + } + + void mockLocationHeaderResponse(String url) { + var header = new HttpHeaders(); + header.put(HttpHeaders.LOCATION, Collections.singletonList(url)); + when(res.getHeaders()).thenReturn(header); } @Nested class GivenValidUrl { - @Test - void whenNoAttls_thenAddRedirectionUrl() { - var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); + static Stream locationUrls() { + return Stream.of( + Arguments.of("/discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "https://localhost:10010/discoverableclient/login?redirected_url=%2Fsome%2Fpath", false), + Arguments.of("discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", false), + Arguments.of("http://localhost:10010/api/v1/redirected_url?arg=1&arg=2", "http://localhost:10010/api/v1/redirected_url?arg=1&arg=2", true), + Arguments.of("%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", true), + Arguments.of("api/request", "api/request", true), + Arguments.of("//localhost:10010/api/v1/request", "//localhost:10010/api/v1/request", true), + Arguments.of("/discoverableclient/api/v1/api/v1/redirected_url?arg=1&arg=2", "http://localhost:10010/discoverableclient/api/v1/redirected_url?arg=1&arg=2", true) + + ); + } - header.put(HttpHeaders.LOCATION, Collections.singletonList("https://localhost:10010/api/v1/redirected_url")); - when(res.getHeaders()).thenReturn(header); + @ParameterizedTest + @MethodSource(value = "locationUrls") + void whenNoAttls_thenAddRedirectionUrl(String expectedUrl, String originalUrl, boolean attlsEnabled) { + ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", attlsEnabled); - commonSetup(factory, exchange, res, chain, false); - setupInstanceInfo(); - var config = createConfig(); + mockLocationHeaderResponse(originalUrl); + var config = createConfig(DISCOVERABLECLIENT); GatewayFilter gatewayFilter = factory.apply(config); StepVerifier.create(gatewayFilter.filter(exchange, chain)) @@ -117,23 +144,19 @@ void whenNoAttls_thenAddRedirectionUrl() { } @Test - void whenAttls_thenAddRedirectionUrl() { - var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url?arg=1&arg=2"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); - header.put(HttpHeaders.LOCATION, Collections.singletonList("http://localhost:10010/api/v1/redirected_url?arg=1&arg=2")); - when(res.getHeaders()).thenReturn(header); - - commonSetup(factory, exchange, res, chain, true); - setupInstanceInfo(); - var config = createConfig(); - - StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); - assertEquals(expectedUrl, res.getHeaders().getFirst(HttpHeaders.LOCATION)); + void whenTargetInstanceIsGateway_thenDoNotUpdate() { + var url = "https://localhost:10010/discoverableclient/api/v1/redirected_url?arg=1&arg=2"; + mockLocationHeaderResponse(url); + var config = createConfig(GATEWAY); + mockServiceInstance(GATEWAY); + GatewayFilter gatewayFilter = factory.apply(config); + StepVerifier.create(gatewayFilter.filter(exchange, chain)) + .thenAwait() + .expectComplete() + .verify(); + assertEquals(url, res.getHeaders().getFirst(HttpHeaders.LOCATION)); } + } @Nested @@ -142,16 +165,10 @@ class GivenMissingGwConfig { @Test void thenDoNotTransform() { var expectedUrl = GW_BASE_URL + "/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); - header.put(HttpHeaders.LOCATION, Collections.singletonList(expectedUrl)); - when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); - setupInstanceInfo(); - var config = createConfig(); + + mockLocationHeaderResponse(expectedUrl); + mockServiceInstance(DISCOVERABLECLIENT); + var config = createConfig(DISCOVERABLECLIENT); when(gatewayClient.isInitialized()).thenReturn(false); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); @@ -163,15 +180,11 @@ void thenDoNotTransform() { class GivenNullUrl { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); + var header = new HttpHeaders(); header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); - var config = createConfig(); + var config = createConfig(DISCOVERABLECLIENT); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); assertNull(res.getHeaders().getFirst(HttpHeaders.LOCATION)); @@ -182,18 +195,14 @@ void thenDoNotTransform() { class GivenDifferentResponseStatusCode { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); + var header = new HttpHeaders(); header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_CONTINUE)); - setupInstanceInfo(); - var config = createConfig(); + mockServiceInstance(DISCOVERABLECLIENT); + var config = createConfig(DISCOVERABLECLIENT); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); assertNull(res.getHeaders().getFirst(HttpHeaders.LOCATION)); diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java index e3e6d8f03c..a6d0419331 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java @@ -75,7 +75,6 @@ public PageRedirectionTest() throws URISyntaxException { @TestsNotMeantForZowe void apiRouteOfDiscoverableClient() { String location = String.format("%s://%s:%d%s", dcScheme, dcHost, dcPort, DISCOVERABLE_GREET); - String transformedLocation = String.format("%s://%s:%d%s", gatewayScheme, gatewayHost, gatewayPort, STATIC_GREET); RedirectLocation redirectLocation = new RedirectLocation(location); @@ -86,7 +85,7 @@ void apiRouteOfDiscoverableClient() { .post(requestUrl) .then() .statusCode(is(HttpStatus.TEMPORARY_REDIRECT.value())) - .header(LOCATION, transformedLocation); + .header(LOCATION, STATIC_GREET); } /** diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java new file mode 100644 index 0000000000..91bcede7f2 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -0,0 +1,139 @@ +/* + * 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.RestAssured; +import io.restassured.http.ContentType; +import lombok.Data; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; +import org.zowe.apiml.util.config.*; + +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; + +@DiscoverableClientDependentTest +class RedirectTest { + + static GatewayServiceConfiguration gwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + static DiscoverableClientConfiguration dcConf = ConfigReader.environmentConfiguration().getDiscoverableClientConfiguration(); + static String gatewayUrl; + static String dcUrl; + + @BeforeAll + static void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + SslContext.prepareSslAuthentication(ItSslConfigFactory.integrationTests()); + gatewayUrl = String.format("%s:%d", gwConf.getHost(), gwConf.getPort()); + dcUrl = String.format("%s:%d", dcConf.getHost(), dcConf.getPort()); + } + + static Stream headerValues() { + return Stream.of( + Arguments.of( + "relative URL with encoding", + "%2Fapi%2Frequest", + "%2Fapi%2Frequest" + ), + Arguments.of( + "relative URL with encoding and service route", + "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", + "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest" + ), + Arguments.of( + "absolute URL doesn't match service route", "/api/request", "/api/request"), + Arguments.of( + "relative URL", + "api/request", + "api/request"), + Arguments.of( + "absolute URL containing encoded characters matches service route", + "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", + "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest" + ), + Arguments.of( + "relative URL that contains service ID", + "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", + "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest" + ), + Arguments.of( + "absolute URL matches service route", + "/discoverableclient/api/v1/request", + "/discoverableclient/api/v1/request"), + Arguments.of( + "Full URL contains service host and port", + String.format("https://%s/discoverableclient/api/v1/request", dcUrl), + "/discoverableclient/api/v1/request" + ), + Arguments.of( + "Full URL contains gateway host and port", + String.format("https://%s/discoverableclient/api/v3/request", gatewayUrl), + String.format("https://%s/discoverableclient/api/v3/request", gatewayUrl) + ), + Arguments.of( + "scheme-relative URL contains service host and port", + String.format("//%s/discoverableclient/api/v1/request", dcUrl), + "/discoverableclient/api/v1/request" + ), + Arguments.of( + "scheme-relative URL contains gateway host and port", + String.format("//%s/discoverableclient/api/v1/request", gatewayUrl), + String.format("//%s/discoverableclient/api/v1/request", gatewayUrl) + ) + ); + } + + @ParameterizedTest(name = "given {0} then Location header value {1} was transform to {2}") + @MethodSource("headerValues") + void giveLocationHeaderFromService(String msg, String original, String translated) { + var baseUrl = String.format("%s://%s:%d", gwConf.getScheme(), gwConf.getHost(), gwConf.getPort()); + var targetUrl = baseUrl + "/discoverableclient/api/v1/redirect"; + given() + .body(new LocationReq(original)) + .contentType(ContentType.JSON) + .post(targetUrl) + .then().log().ifValidationFails() + .header("Location", translated) + .statusCode(307); + } + + static Stream urlsWithoutContextPath() { + return Stream.of( + Arguments.of( + "absolute URL matches / as a service route ", + "/common/login?returnUrl=%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", + "/redirectclient/ui/common/login?returnUrl=%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest") + ); + } + + @ParameterizedTest(name = "given {0} then Location header value {1} was transform to {2}") + @MethodSource("urlsWithoutContextPath") + void giveLocationHeaderFromServiceWithoutContextPath(String msg, String original, String translated) { + var baseUrl = String.format("%s://%s:%d", gwConf.getScheme(), gwConf.getHost(), gwConf.getPort()); + var targetUrl = baseUrl + "/redirectclient/ui/api/v1/redirect"; + given() + .body(new LocationReq(original)) + .contentType(ContentType.JSON) + .post(targetUrl) + .then().log().ifValidationFails() + .header("Location", translated) + .statusCode(302); + } + + @Data + static class LocationReq { + final String location; + } +} diff --git a/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java b/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java new file mode 100644 index 0000000000..9c541082bc --- /dev/null +++ b/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java @@ -0,0 +1,50 @@ +/* + * 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.client.api; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.Data; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.HttpHeaders.LOCATION; + +@RestController +public class RedirectController { + + /** + * Get url from POST request body, then set the url to Location response header, and set status code to 307 + * + * @param redirectLocation request body which contains a url + * @param response return the same data as request body + * @return + */ + @PostMapping( + value = "/api/v1/redirect", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @ResponseStatus(HttpStatus.FOUND) + public RedirectLocation redirectPage(@RequestBody RedirectLocation redirectLocation, + HttpServletResponse response) { + response.setHeader(LOCATION, redirectLocation.getLocation()); + return redirectLocation; + } + + @Data + static class RedirectLocation { + private String location; + } +} From 477defd32c2889162e3389b3abc683e082b02831 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:57:17 +0200 Subject: [PATCH 128/152] chore: debug log (#4332) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .../apiml/gateway/filters/PageRedirectionFilterFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index f9659801ea..192d11136b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -144,7 +144,7 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf log.debug("Location header is empty"); return Mono.empty(); } - + log.debug("Location header in response: {}", location); var locationUri = URI.create(location); var targetInstance = getInstance(locationUri, instance); if (isGateway(targetInstance)) { From aa85927d016924aba6169d9dbee91abc85981976 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Mon, 29 Sep 2025 11:31:21 +0000 Subject: [PATCH 129/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.12'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3dd1ec9453..7cf95655f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.12-SNAPSHOT +version=3.3.12 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From a1cd95d1d850a1b16788ad92a64e215830bfb561 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Mon, 29 Sep 2025 11:31:22 +0000 Subject: [PATCH 130/152] [Gradle Release plugin] Create new version: 'v3.3.13-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7cf95655f0..6fd24bba42 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.12 +version=3.3.13-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 80eccb149a6d48ff3e62e227fb9863a664462dcc Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Mon, 29 Sep 2025 11:31:23 +0000 Subject: [PATCH 131/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index d5c3c40f7e..44740829c8 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.12-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.13-SNAPSHOT From 698b2e06c0cbf3f78a61225e7a0a40efe5b2429b Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 29 Sep 2025 16:03:39 +0200 Subject: [PATCH 132/152] fix: update modulith version of z/OSMF static definition (#4333) Signed-off-by: Pablo Carle Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../main/resources/zosmf-static-definition.yaml.template | 8 ++++++-- .../main/resources/zosmf-static-definition.yaml.template | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apiml-package/src/main/resources/zosmf-static-definition.yaml.template b/apiml-package/src/main/resources/zosmf-static-definition.yaml.template index d2bd325871..2b252d05b2 100644 --- a/apiml-package/src/main/resources/zosmf-static-definition.yaml.template +++ b/apiml-package/src/main/resources/zosmf-static-definition.yaml.template @@ -1,12 +1,12 @@ # Static definition for z/OSMF # # Once configured you can access z/OSMF via the API gateway: -# curl -k -X GET -H "X-CSRF-ZOSMF-HEADER: *" https://${ZOWE_EXPLORER_HOST}:${GATEWAY_PORT}/zosmf/api/v1/info +# curl -k -X GET -H "X-CSRF-ZOSMF-HEADER: *" https://${ZWE_zowe_externalDomains_0}:${GATEWAY_PORT}/ibmzosmf/api/v1/info' # services: - serviceId: ibmzosmf title: IBM z/OSMF - description: 'IBM z/OS Management Facility REST API service. Once configured you can access z/OSMF via the API gateway: https://${ZOWE_EXPLORER_HOST}:${GATEWAY_PORT}/ibmzosmf/api/v1/info' + description: 'IBM z/OS Management Facility REST API service. Once configured you can access z/OSMF via the API gateway: https://${ZWE_zowe_externalDomains_0}:${GATEWAY_PORT}/ibmzosmf/api/v1/info' catalogUiTileId: zosmf instanceBaseUrls: - ${ZOSMF_SCHEME}://${ZOSMF_HOST}:${ZOSMF_PORT}/ @@ -25,6 +25,10 @@ services: apiml: enableUrlEncodedCharacters: true headersToIgnore: Origin + service: + scheme: ${ZOSMF_SCHEME} + nonSecurePortEnabled: ${ZOSMF_NON_SECURE_PORT_ENABLED:-false} + securePortEnabled: ${ZOSMF_SECURE_PORT_ENABLED:-true} catalogUiTiles: zosmf: title: z/OSMF services diff --git a/discovery-package/src/main/resources/zosmf-static-definition.yaml.template b/discovery-package/src/main/resources/zosmf-static-definition.yaml.template index ef9425c24c..d292100d34 100644 --- a/discovery-package/src/main/resources/zosmf-static-definition.yaml.template +++ b/discovery-package/src/main/resources/zosmf-static-definition.yaml.template @@ -1,7 +1,7 @@ # Static definition for z/OSMF # # Once configured you can access z/OSMF via the API gateway: -# curl -k -X GET -H "X-CSRF-ZOSMF-HEADER: *" https://${ZWE_zowe_externalDomains_0}:${GATEWAY_PORT}/zosmf/api/v1/info +# curl -k -X GET -H "X-CSRF-ZOSMF-HEADER: *" https://${ZWE_zowe_externalDomains_0}:${GATEWAY_PORT}/ibmzosmf/api/v1/info # services: - serviceId: ibmzosmf From e39becc77594ae8f008eabddbd292098751d7345 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 3 Oct 2025 00:42:23 +0000 Subject: [PATCH 133/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.13'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6fd24bba42..a80827a256 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.13-SNAPSHOT +version=3.3.13 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 324b9973c5b1973af86e6187ff50d5b1a5344387 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 3 Oct 2025 00:42:25 +0000 Subject: [PATCH 134/152] [Gradle Release plugin] Create new version: 'v3.3.14-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a80827a256..206b393eae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.13 +version=3.3.14-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From b502e87a033b7a40f7d71c978dfc95ee30fcb617 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 3 Oct 2025 00:42:26 +0000 Subject: [PATCH 135/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 44740829c8..6ca1a43402 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.13-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.14-SNAPSHOT From f6280a42228148e6edbc1483d6c05a00c35e43b2 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:34:49 +0200 Subject: [PATCH 136/152] fix: avoid duplicate startup message (#4339) Signed-off-by: ac892247 Co-authored-by: Pablo Carle Signed-off-by: Gowtham Selvaraj --- .../service/ServiceStartupEventHandler.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java b/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java index 4a1337649f..c3ae8aecd7 100644 --- a/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java +++ b/apiml-utility/src/main/java/org/zowe/apiml/product/service/ServiceStartupEventHandler.java @@ -14,12 +14,14 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import lombok.NoArgsConstructor; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import java.lang.management.ManagementFactory; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; @Component @NoArgsConstructor @@ -28,10 +30,16 @@ public class ServiceStartupEventHandler { public static final int DEFAULT_DELAY_FACTOR = 5; private final ApimlLogger apimlLog = ApimlLogger.of(ServiceStartupEventHandler.class, - YamlMessageServiceInstance.getInstance()); + YamlMessageServiceInstance.getInstance()); + + private final Set registeredServices = ConcurrentHashMap.newKeySet(); @SuppressWarnings("squid:S1172") public void onServiceStartup(String serviceName, int delayFactor) { + if (registeredServices.contains(serviceName)) { + return; + } + registeredServices.add(serviceName); long uptime = ManagementFactory.getRuntimeMXBean().getUptime(); apimlLog.log("org.zowe.apiml.common.serviceStarted", serviceName, uptime / 1000.0); From 0ee8535a8f3266af64e05e7a87dc75d1858f7c1d Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:39:52 +0200 Subject: [PATCH 137/152] fix: keep all gateways registered (#4345) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- .../java/org/zowe/apiml/ModulithConfig.java | 4 +- .../controller/ApimlHomepageController.java | 109 ++++++++++ apiml/src/main/resources/templates/home.html | 190 ++++++++++++++++++ .../GatewayHomepageController.java | 2 + .../service/zosmf/AbstractZosmfService.java | 2 +- 5 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 apiml/src/main/java/org/zowe/apiml/controller/ApimlHomepageController.java create mode 100644 apiml/src/main/resources/templates/home.html diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index 48b00c4f8d..4e257c7979 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -114,10 +114,10 @@ ApplicationInfo applicationInfo() { private InstanceInfo getInstanceInfo(String serviceId) { var leaseInfo = LeaseInfo.Builder.newBuilder() - .setDurationInSecs(Integer.MAX_VALUE) + .setDurationInSecs(90) .setRegistrationTimestamp(System.currentTimeMillis()) .setRenewalTimestamp(System.currentTimeMillis()) - .setRenewalIntervalInSecs(Integer.MAX_VALUE) + .setRenewalIntervalInSecs(30) .setServiceUpTimestamp(System.currentTimeMillis()) .build(); diff --git a/apiml/src/main/java/org/zowe/apiml/controller/ApimlHomepageController.java b/apiml/src/main/java/org/zowe/apiml/controller/ApimlHomepageController.java new file mode 100644 index 0000000000..1527a91cc9 --- /dev/null +++ b/apiml/src/main/java/org/zowe/apiml/controller/ApimlHomepageController.java @@ -0,0 +1,109 @@ +/* + * 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.controller; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.zowe.apiml.config.ApplicationInfo; +import org.zowe.apiml.product.version.BuildInfo; +import org.zowe.apiml.product.version.BuildInfoDetails; +import org.zowe.apiml.zaas.security.login.Providers; +import org.zowe.apiml.zaas.security.service.JwtSecurity; + +import java.util.List; + +/** + * Main page for API ML, displaying status and build version information + */ +@Tag(name = "Home page") +@RequiredArgsConstructor +@Controller +public class ApimlHomepageController { + + private static final String SUCCESS_ICON_NAME = "success"; + private static final String WARNING_ICON_NAME = "warning"; + + private final DiscoveryClient discoveryClient; + private final BuildInfo buildInfo; + + private final ApplicationInfo applicationInfo; + private final ApplicationContext applicationContext; + + private String buildString; + + @PostConstruct + public void init() { + initializeBuildInfos(); + } + + @Hidden + @GetMapping("/") + public String home(Model model) { + + initializeParameters(model); + return "home"; + } + + private void initializeBuildInfos() { + BuildInfoDetails buildInfoDetails = buildInfo.getBuildInfoDetails(); + buildString = "Build information is not available"; + if (!buildInfoDetails.getVersion().equalsIgnoreCase("unknown")) { + buildString = String.format("Version %s build # %s", buildInfoDetails.getVersion(), buildInfoDetails.getNumber()); + } + } + + private void initializeParameters(Model model) { + long apimlInstances = apimlInstancesCount(); + var apimlStatusText = "Number of API ML instances: " + apimlInstances; + model.addAttribute("apimlStatusText", apimlStatusText); + if (isAuthReady()) { + model.addAttribute("authStatusText", "Authentication service is ready"); + model.addAttribute("authIconName", SUCCESS_ICON_NAME); + } else { + model.addAttribute("authStatusText", "Authentication service is not ready"); + model.addAttribute("authIconName", WARNING_ICON_NAME); + } + + model.addAttribute("catalogLink", "/apicatalog/ui/v1"); + model.addAttribute("isAnyCatalogAvailable", true); + model.addAttribute("catalogIconName", SUCCESS_ICON_NAME); + model.addAttribute("catalogLinkEnabled", true); + model.addAttribute("catalogStatusText", "The API Catalog"); + model.addAttribute("buildInfoText", buildString); + } + + private boolean isAuthReady() { + var jwtSec = applicationContext.getBean(JwtSecurity.class); + var providers = applicationContext.getBean(Providers.class); + if (!providers.isZosfmUsed()) { + return true; + } + return jwtSec.getZosmfListener().isZosmfReady(); + } + + private int apimlInstancesCount() { + List apimlInstances = discoveryClient.getInstances(applicationInfo.getAuthServiceId()); + if (apimlInstances != null) { + return apimlInstances.size(); + } + return 0; + } + +} + diff --git a/apiml/src/main/resources/templates/home.html b/apiml/src/main/resources/templates/home.html new file mode 100644 index 0000000000..993f915461 --- /dev/null +++ b/apiml/src/main/resources/templates/home.html @@ -0,0 +1,190 @@ + + + + + API Mediation Layer + + + + + + + + +
    +

    API Mediation Layer

    +
      +
    • + +
    • +
    • +
      Authentication service running icon +

      +
    • +
    • +
      +

      +
    • +
    +
    +
    +
    + + diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java index 4b63fcc0bf..f1f682dea9 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayHomepageController.java @@ -15,6 +15,7 @@ import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Controller; @@ -34,6 +35,7 @@ @Tag(name = "Home page") @RequiredArgsConstructor @Controller +@ConditionalOnMissingBean(name = "modulithConfig") public class GatewayHomepageController { private static final String SUCCESS_ICON_NAME = "success"; diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/AbstractZosmfService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/AbstractZosmfService.java index db029c2990..75d0efe98b 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/AbstractZosmfService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/AbstractZosmfService.java @@ -166,7 +166,7 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re } if (re instanceof HttpClientErrorException.Unauthorized) { - log.warn("Request to z/OSMF requires authentication {}", re.getMessage()); + log.debug("Request to z/OSMF requires authentication {}", re.getMessage()); return new BadCredentialsException("Invalid Credentials"); } From 8e139fc98addae9faf2494a63f1234f4d15583e9 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 10 Oct 2025 00:43:13 +0000 Subject: [PATCH 138/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.14'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 206b393eae..d0168e46f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.14-SNAPSHOT +version=3.3.14 defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From 6ac4c63b876d8fca2d6e9a8b176a70e345c60643 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 10 Oct 2025 00:43:14 +0000 Subject: [PATCH 139/152] [Gradle Release plugin] Create new version: 'v3.3.15-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d0168e46f3..5ef271d21e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.14 +version=3.3.15-SNAPSHOT defaultSpringBootVersion=2.0.2.RELEASE defaultSpringBootCloudVersion=2.0.0.RELEASE From c2d9596379a10d3cab11b625abf6b0cbd4923ff5 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 10 Oct 2025 00:43:16 +0000 Subject: [PATCH 140/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- api-catalog-ui/frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 6ca1a43402..ca68caffd5 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.14-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.15-SNAPSHOT From 1de6993810b918379ae7d209adbf3ca79728030f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= <58428711+pj892031@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:03:43 +0200 Subject: [PATCH 141/152] fix: Fix of Tomcat customizers to be supported also by reactive stuff (#4336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Gowtham Selvaraj --- .../apicatalog/ApiCatalogApplicationTest.java | 66 +++++++++++++++ .../security/WebServerSecurityConfig.java | 12 +-- .../java/org/zowe/apiml/util/Recorder.java | 26 ++++++ .../java/org/zowe/apiml/util/TestLogger.java | 84 +++++++++++++++++++ .../testFixtures/resources/logback-spring.xml | 25 ++++++ .../product/web/TomcatAcceptFixConfig.java | 2 + .../apiml/product/web/TomcatKeyringFix.java | 10 ++- apiml/src/main/resources/application.yml | 4 +- .../apiml/caching/config/GeneralConfig.java | 3 +- .../src/main/resources/application.yml | 3 +- .../CachingServiceApplicationTest.java | 66 +++++++++++++++ .../apiml/caching/config/AttlsConfigTest.java | 8 -- .../caching/config/SecurityConfigTest.java | 3 - .../functional/InMemoryFunctionalTest.java | 4 +- .../storage/InfinispanStartupTest.java | 11 ++- .../src/test/resources/application.yml | 4 + .../configuration/TomcatConfiguration.java | 6 +- .../DiscoveryServiceApplicationTest.java | 66 +++++++++++++++ gateway-service/build.gradle | 1 + .../gateway/GatewayServiceApplication.java | 1 + .../GatewayServiceApplicationTest.java | 66 +++++++++++++++ .../zowe/apiml/zaas/ZaasApplicationTest.java | 66 +++++++++++++++ 22 files changed, 508 insertions(+), 29 deletions(-) create mode 100644 api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogApplicationTest.java create mode 100644 apiml-common/src/testFixtures/java/org/zowe/apiml/util/Recorder.java create mode 100644 apiml-common/src/testFixtures/java/org/zowe/apiml/util/TestLogger.java create mode 100644 apiml-common/src/testFixtures/resources/logback-spring.xml create mode 100644 caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceApplicationTest.java create mode 100644 discovery-service/src/test/java/org/zowe/apiml/discovery/DiscoveryServiceApplicationTest.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java create mode 100644 zaas-service/src/test/java/org/zowe/apiml/zaas/ZaasApplicationTest.java diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogApplicationTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogApplicationTest.java new file mode 100644 index 0000000000..f0190d369e --- /dev/null +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogApplicationTest.java @@ -0,0 +1,66 @@ +/* + * 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.apicatalog; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.zowe.apiml.util.Recorder; +import org.zowe.apiml.util.TestLogger; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ApiCatalogApplicationTest { + + private Recorder recorder = TestLogger.getHandler(); + + @BeforeAll + void startRecording() { + recorder.startRecording(); + } + + @AfterAll + void stopRecording() { + recorder.stopRecording(); + } + + @Nested + @SpringBootTest( + properties = { + "apiml.enabled=false", + "logging.level.org.zowe.apiml.product.web=DEBUG", + "logging.level.org.zowe.apiml.product.security=DEBUG" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class TomcatInitialization { + + @ParameterizedTest(name = "Check if {0} is initialized on startup") + @CsvSource({ + "ServletContainerCustomizer,servletContainerCustomizer initialized", + "TomcatKeyringFix,TomcatKeyringFix applied", + "TomcatAcceptFixConfig,TomcatAcceptFixConfig applied" + }) + void givenStandardConfiguration_whenServiceStarted_thenComponentIsInitialized( + String componentName, String logMessage + ) { + assertFalse(recorder.find(logMessage).isEmpty()); + } + + } + +} diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/security/WebServerSecurityConfig.java b/apiml-common/src/main/java/org/zowe/apiml/product/security/WebServerSecurityConfig.java index bc73ca6532..a31a19a7f1 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/security/WebServerSecurityConfig.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/security/WebServerSecurityConfig.java @@ -10,10 +10,10 @@ package org.zowe.apiml.product.security; +import lombok.extern.slf4j.Slf4j; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -22,16 +22,18 @@ /** * Configuration of web server security */ +@Slf4j @Configuration @ConditionalOnMissingBean(name = "modulithConfig") public class WebServerSecurityConfig { @Bean - public WebServerFactoryCustomizer servletContainerCustomizer() { - return factory -> factory.addConnectorCustomizers(connector -> { + public TomcatConnectorCustomizer servletContainerCustomizer() { + return connector -> { AbstractHttp11Protocol abstractProtocol = (AbstractHttp11Protocol) connector.getProtocolHandler(); Arrays.stream(abstractProtocol.findSslHostConfigs()).forEach(sslHost -> sslHost.setHonorCipherOrder(true)); - }); + log.debug("servletContainerCustomizer initialized"); + }; } } diff --git a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/Recorder.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/Recorder.java new file mode 100644 index 0000000000..753fcf525f --- /dev/null +++ b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/Recorder.java @@ -0,0 +1,26 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.util; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +public interface Recorder { + + void startRecording(); + void stopRecording(); + Optional findFirst(Predicate filter); + Optional findFirst(String messageInfix); + List find(Predicate filter); + List find(String messageInfix); + +} diff --git a/apiml-common/src/testFixtures/java/org/zowe/apiml/util/TestLogger.java b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/TestLogger.java new file mode 100644 index 0000000000..7a2d558fbb --- /dev/null +++ b/apiml-common/src/testFixtures/java/org/zowe/apiml/util/TestLogger.java @@ -0,0 +1,84 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.util; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + +public class TestLogger extends AppenderBase implements Recorder { + + private static AtomicReference lastInstance = new AtomicReference<>(); + + private boolean listening; + private List records; + + public TestLogger() { + if (lastInstance.get() != null) { + listening = lastInstance.get().listening; + records = lastInstance.get().records; + } else { + records = new LinkedList<>(); + } + lastInstance.set(this); + } + + @Override + protected void append(ILoggingEvent eventObject) { + if (listening) { + records.add(eventObject); + } + } + + public static Recorder getHandler() { + return lastInstance.getAndUpdate( + x -> x == null ? new TestLogger() : x + ); + } + + @Override + public void startRecording() { + listening = true; + } + + @Override + public void stopRecording() { + records.clear(); + listening = false; + } + + @Override + public Optional findFirst(Predicate filter) { + return records.stream().filter(filter).findFirst(); + } + + @Override + public Optional findFirst(String messageInfix) { + return records.stream().filter(x -> x.getMessage().contains(messageInfix)).findFirst(); + } + + @Override + public List find(Predicate filter) { + return records.stream().filter(filter).toList(); + } + + @Override + public List find(String messageInfix) { + return records.stream().filter(x -> x.getMessage().contains(messageInfix)).toList(); + } + +} + diff --git a/apiml-common/src/testFixtures/resources/logback-spring.xml b/apiml-common/src/testFixtures/resources/logback-spring.xml new file mode 100644 index 0000000000..ea9a249948 --- /dev/null +++ b/apiml-common/src/testFixtures/resources/logback-spring.xml @@ -0,0 +1,25 @@ + + + + + + + + UTF-8 + + ${apimlLogPattern} + + + + + + + + + + + + + + + diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java index 24fc6da0ab..e4656e3bab 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java @@ -129,6 +129,8 @@ public TomcatConnectorCustomizer tomcatAcceptorFix() { return connector -> connector.addLifecycleListener(event -> { if (event.getLifecycle().getState() == LifecycleState.STARTED) { update(connector); + + log.debug("TomcatAcceptFixConfig applied"); } }); } diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java index 5a11d9412d..619a3f4f54 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java @@ -10,8 +10,9 @@ package org.zowe.apiml.product.web; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.stereotype.Component; @@ -19,8 +20,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +@Slf4j @Component -public class TomcatKeyringFix implements WebServerFactoryCustomizer { +public class TomcatKeyringFix implements WebServerFactoryCustomizer { private static final Pattern KEYRING_PATTERN = Pattern.compile("^(safkeyring[^:]*):/{2,4}([^/]+)/(.+)$"); private static final String KEYRING_PASSWORD = "password"; @@ -59,7 +61,7 @@ static String formatKeyringUrl(String keyringUrl) { } @Override - public void customize(TomcatServletWebServerFactory factory) { + public void customize(AbstractConfigurableWebServerFactory factory) { Ssl ssl = factory.getSsl(); if (isKeyring(keyStore)) { ssl.setKeyStore(formatKeyringUrl(keyStore)); @@ -72,6 +74,8 @@ public void customize(TomcatServletWebServerFactory factory) { ssl.setTrustStore(formatKeyringUrl(trustStore)); ssl.setTrustStorePassword(trustStorePassword == null ? KEYRING_PASSWORD : String.valueOf(trustStorePassword)); } + + log.debug("TomcatKeyringFix applied"); } } diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 498d19b247..834cbf6690 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -303,8 +303,8 @@ logging: reactor.netty: WARN reactor.netty.http.client: INFO reactor.netty.http.client.HttpClientConnect: OFF - org.infinispan: WARN - org.jgroups: WARN + org.infinispan: ERROR + org.jgroups: ERROR management: endpoint: diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java index dbe0a800a4..95158bfd29 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/GeneralConfig.java @@ -20,13 +20,14 @@ import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.zowe.apiml.filter.AttlsHttpHandler; +import org.zowe.apiml.product.security.WebServerSecurityConfig; import org.zowe.apiml.product.service.ServiceStartupEventHandler; import org.zowe.apiml.product.web.ApimlTomcatCustomizer; import org.zowe.apiml.product.web.TomcatAcceptFixConfig; import org.zowe.apiml.product.web.TomcatKeyringFix; @Configuration -@Import({TomcatKeyringFix.class, TomcatAcceptFixConfig.class, ApimlTomcatCustomizer.class, AttlsHttpHandler.class,}) +@Import({TomcatKeyringFix.class, TomcatAcceptFixConfig.class, ApimlTomcatCustomizer.class, AttlsHttpHandler.class, WebServerSecurityConfig.class}) @Data @ToString public class GeneralConfig implements WebMvcConfigurer { diff --git a/caching-service/src/main/resources/application.yml b/caching-service/src/main/resources/application.yml index ff08675dd8..b21426c905 100644 --- a/caching-service/src/main/resources/application.yml +++ b/caching-service/src/main/resources/application.yml @@ -33,7 +33,8 @@ logging: org.springdoc: WARN org.springframework: WARN org.springframework.boot: OFF - org.jgroups.protocols: ERROR + org.infinispan: ERROR + org.jgroups: ERROR com.netflix: WARN com.netflix.discovery: ERROR com.netflix.config: ERROR diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceApplicationTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceApplicationTest.java new file mode 100644 index 0000000000..66c8efba64 --- /dev/null +++ b/caching-service/src/test/java/org/zowe/apiml/caching/CachingServiceApplicationTest.java @@ -0,0 +1,66 @@ +/* + * 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.caching; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.zowe.apiml.util.Recorder; +import org.zowe.apiml.util.TestLogger; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CachingServiceApplicationTest { + + private Recorder recorder = TestLogger.getHandler(); + + @BeforeAll + void startRecording() { + recorder.startRecording(); + } + + @AfterAll + void stopRecording() { + recorder.stopRecording(); + } + + @Nested + @SpringBootTest( + properties = { + "apiml.enabled=false", + "logging.level.org.zowe.apiml.product.web=DEBUG", + "logging.level.org.zowe.apiml.product.security=DEBUG" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class TomcatInitialization { + + @ParameterizedTest(name = "Check if {0} is initialized on startup") + @CsvSource({ + "ServletContainerCustomizer,servletContainerCustomizer initialized", + "TomcatKeyringFix,TomcatKeyringFix applied", + "TomcatAcceptFixConfig,TomcatAcceptFixConfig applied" + }) + void givenStandardConfiguration_whenServiceStarted_thenComponentIsInitialized( + String componentName, String logMessage + ) { + assertFalse(recorder.find(logMessage).isEmpty()); + } + + } + +} 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 2f91ef6209..4da67d9cee 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.CachingServiceApplication; @@ -55,12 +54,6 @@ private String getUri(String hostname, int port, String scheme) { webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) @ActiveProfiles({ "AttlsConfigTestCachingService", "attlsClient", "attlsServer" }) - @TestPropertySource( - properties = { - "caching.storage.mode=inMemory" - } - ) - @DirtiesContext @Nested class GivenAttlsModeEnabled { @@ -130,7 +123,6 @@ void requestFailsWithAttlsReasonWithHttp() { } ) @ActiveProfiles({ "attlsClient", "attlsServer" }) - @DirtiesContext @SpringBootTest( classes = CachingServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java index edddd90170..108f44c090 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/config/SecurityConfigTest.java @@ -20,7 +20,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.TestPropertySource; import org.zowe.apiml.caching.CachingServiceApplication; import org.zowe.apiml.util.config.SslContext; @@ -49,7 +48,6 @@ private String getUri(String hostname, int port) { "apiml.service.ssl.verifySslCertificatesOfServices=false" } ) - @DirtiesContext @SpringBootTest( classes = CachingServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT @@ -78,7 +76,6 @@ void thenDoNotRequireAuth() { "apiml.service.ssl.verifySslCertificatesOfServices=true" } ) - @DirtiesContext @SpringBootTest( classes = CachingServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java index 55409d7d6c..d6e92e65cb 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/functional/InMemoryFunctionalTest.java @@ -24,9 +24,7 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; -@SpringBootTest( - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT -) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestInstance(Lifecycle.PER_CLASS) public class InMemoryFunctionalTest { diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStartupTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStartupTest.java index 0edc645374..9c8dab12f4 100644 --- a/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStartupTest.java +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/storage/InfinispanStartupTest.java @@ -14,11 +14,18 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; import static org.junit.jupiter.api.Assertions.assertNotNull; -@SpringBootTest(properties = {"caching.storage.mode=infinispan", - "jgroups.bind.port=7600", "jgroups.bind.address=localhost", "apiml.enabled=false"}) +@SpringBootTest(properties = { + "caching.storage.mode=infinispan", + "infinispan.embedded.enabled=true", + "jgroups.bind.port=7600", + "jgroups.bind.address=localhost", + "apiml.enabled=false" +}) +@DirtiesContext // infinispan.embedded.enabled register JMXBeans. There could be registered only once class InfinispanStartupTest { @Autowired diff --git a/caching-service/src/test/resources/application.yml b/caching-service/src/test/resources/application.yml index a73bda02e8..c2cfaeda1a 100644 --- a/caching-service/src/test/resources/application.yml +++ b/caching-service/src/test/resources/application.yml @@ -13,10 +13,14 @@ spring: caching: storage: + mode: inMemory infinispan: initialHosts: localhost[7600] persistence: dataLocation: data + +infinispan.embedded.enabled: false + apiml: enabled: true service: diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/TomcatConfiguration.java b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/TomcatConfiguration.java index 9dfaf09e62..84c8db8d6e 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/TomcatConfiguration.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/TomcatConfiguration.java @@ -26,9 +26,13 @@ @Configuration public class TomcatConfiguration { + @Bean + public TomcatConnectorCustomizer urlTomcatCustomizer() { + return new UrlTomcatCustomizer(); + } + @Bean public ServletWebServerFactory servletContainer(List connectorCustomizers) { - connectorCustomizers.add(new UrlTomcatCustomizer()); TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); tomcat.setProtocol(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); tomcat.addConnectorCustomizers(connectorCustomizers.toArray(new TomcatConnectorCustomizer[0])); diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/DiscoveryServiceApplicationTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/DiscoveryServiceApplicationTest.java new file mode 100644 index 0000000000..e76d417673 --- /dev/null +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/DiscoveryServiceApplicationTest.java @@ -0,0 +1,66 @@ +/* + * 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.discovery; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.zowe.apiml.util.Recorder; +import org.zowe.apiml.util.TestLogger; + +import static org.junit.jupiter.api.Assertions.*; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DiscoveryServiceApplicationTest { + + private Recorder recorder = TestLogger.getHandler(); + + @BeforeAll + void startRecording() { + recorder.startRecording(); + } + + @AfterAll + void stopRecording() { + recorder.stopRecording(); + } + + @Nested + @SpringBootTest( + properties = { + "apiml.enabled=false", + "logging.level.org.zowe.apiml.product.web=DEBUG", + "logging.level.org.zowe.apiml.product.security=DEBUG" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class TomcatInitialization { + + @ParameterizedTest(name = "Check if {0} is initialized on startup") + @CsvSource({ + "ServletContainerCustomizer,servletContainerCustomizer initialized", + "TomcatKeyringFix,TomcatKeyringFix applied", + "TomcatAcceptFixConfig,TomcatAcceptFixConfig applied" + }) + void givenStandardConfiguration_whenServiceStarted_thenComponentIsInitialized( + String componentName, String logMessage + ) { + assertFalse(recorder.find(logMessage).isEmpty()); + } + + } + +} diff --git a/gateway-service/build.gradle b/gateway-service/build.gradle index 5e2ccbcdb7..d56b1fa759 100644 --- a/gateway-service/build.gradle +++ b/gateway-service/build.gradle @@ -99,6 +99,7 @@ dependencies { testImplementation libs.rest.assured testImplementation libs.rest.assured.web.test.client + testImplementation(testFixtures(project(":apiml-common"))) testFixturesImplementation libs.spring.boot.starter.test testFixturesImplementation libs.commons.io testFixturesImplementation libs.http.client5 diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java index 14f23e87c1..492fae786b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/GatewayServiceApplication.java @@ -23,6 +23,7 @@ "org.zowe.apiml.product.gateway", "org.zowe.apiml.product.version", "org.zowe.apiml.product.logging", + "org.zowe.apiml.product.security", "org.zowe.apiml.product.service", "org.zowe.apiml.security" }, diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java new file mode 100644 index 0000000000..a2864606c0 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java @@ -0,0 +1,66 @@ +/* + * 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 ch.qos.logback.classic.spi.ILoggingEvent; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.zowe.apiml.util.Recorder; +import org.zowe.apiml.util.TestLogger; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GatewayServiceApplicationTest { + + private Recorder recorder = TestLogger.getHandler(); + + @BeforeAll + void startRecording() { + recorder.startRecording(); + } + + @AfterAll + void stopRecording() { + recorder.stopRecording(); + } + + @Nested + @SpringBootTest( + properties = { + "apiml.enabled=false", + "logging.level.org.zowe.apiml.product.web=DEBUG", + "logging.level.org.zowe.apiml.product.security=DEBUG" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class TomcatInitialization { + + @ParameterizedTest(name = "Check if {0} is initialized on startup") + @CsvSource({ + "ServletContainerCustomizer,servletContainerCustomizer initialized", + "TomcatKeyringFix,TomcatKeyringFix applied", + "TomcatAcceptFixConfig,TomcatAcceptFixConfig applied" + }) + void givenStandardConfiguration_whenServiceStarted_thenComponentIsInitialized( + String componentName, String logMessage + ) { + assertFalse(recorder.find(logMessage).isEmpty()); + } + + } + +} diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/ZaasApplicationTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/ZaasApplicationTest.java new file mode 100644 index 0000000000..4ba3283d57 --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/ZaasApplicationTest.java @@ -0,0 +1,66 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.zaas; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.zowe.apiml.util.Recorder; +import org.zowe.apiml.util.TestLogger; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ZaasApplicationTest { + + private Recorder recorder = TestLogger.getHandler(); + + @BeforeAll + void startRecording() { + recorder.startRecording(); + } + + @AfterAll + void stopRecording() { + recorder.stopRecording(); + } + + @Nested + @SpringBootTest( + properties = { + "apiml.enabled=false", + "logging.level.org.zowe.apiml.product.web=DEBUG", + "logging.level.org.zowe.apiml.product.security=DEBUG" + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + ) + class TomcatInitialization { + + @ParameterizedTest(name = "Check if {0} is initialized on startup") + @CsvSource({ + "ServletContainerCustomizer,servletContainerCustomizer initialized", + "TomcatKeyringFix,TomcatKeyringFix applied", + "TomcatAcceptFixConfig,TomcatAcceptFixConfig applied" + }) + void givenStandardConfiguration_whenServiceStarted_thenComponentIsInitialized( + String componentName, String logMessage + ) { + assertFalse(recorder.find(logMessage).isEmpty()); + } + + } + +} From 36e1a3e4fc27add2e73b11000c4b74b2104248cb Mon Sep 17 00:00:00 2001 From: Andrea Tabone <39694626+taban03@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:08:04 +0200 Subject: [PATCH 142/152] fix: Requirement of client certificate on ZAAS call when AT-TLS is used & add AT-TLS support to DC (#4347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš Signed-off-by: Andrea Tabone Co-authored-by: Pavel Jareš Signed-off-by: Gowtham Selvaraj --- .../common/filter/CategorizeCertsFilter.java | 35 +++++++---- .../filter/CategorizeCertsFilterTest.java | 62 +++++++++++++++++++ .../org/zowe/apiml/filter/AttlsFilter.java | 24 ++++--- .../zowe/apiml/filter/AttlsHttpHandler.java | 1 + .../product/web/ApimlTomcatCustomizer.java | 16 +++++ .../product/web/TomcatAcceptFixConfig.java | 2 +- .../apiml/product/web/TomcatKeyringFix.java | 28 +++++++++ .../zowe/apiml/filter/AttlsFilterTest.java | 46 ++++++++++++-- .../zowe/apiml/util/ServletRequestUtils.java | 40 ++++++++++++ .../apiml/util/ServletRequestUtilsTest.java | 49 +++++++++++++++ discoverable-client/build.gradle | 11 ++++ .../DiscoverableClientSampleApplication.java | 5 ++ .../src/main/resources/application.yml | 5 ++ .../gateway/config/AuthEndpointConfig.java | 13 ++-- .../apiml/util/service/RunningService.java | 10 +++ 15 files changed, 315 insertions(+), 32 deletions(-) create mode 100644 common-service-core/src/main/java/org/zowe/apiml/util/ServletRequestUtils.java create mode 100644 common-service-core/src/test/java/org/zowe/apiml/util/ServletRequestUtilsTest.java diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java index 2e44347610..0462658b16 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilter.java @@ -35,6 +35,8 @@ import java.util.*; import java.util.function.Predicate; +import static org.zowe.apiml.util.ServletRequestUtils.isClientCertificateIgnored; + /** * This filter processes certificates on request. It decides, which certificates are considered for client authentication */ @@ -66,21 +68,30 @@ public class CategorizeCertsFilter extends OncePerRequestFilter { * @param request Request to filter certificates */ private void categorizeCerts(ServletRequest request) { - X509Certificate[] certs = (X509Certificate[]) request.getAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE); + var httpServletRequest = (HttpServletRequest) request; + var certs = (X509Certificate[]) httpServletRequest.getAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE); if (certs != null && certs.length > 0) { - Optional clientCert = getClientCertFromHeader((HttpServletRequest) request); - if (certificateValidator.isForwardingEnabled() && certificateValidator.hasGatewayChain(certs) && clientCert.isPresent()) { - certificateValidator.updateAPIMLPublicKeyCertificates(certs); - // add the client certificate to the certs array - String subjectDN = ((X509Certificate) clientCert.get()).getSubjectX500Principal().getName(); - log.debug("Found client certificate in header, adding it to the request. Subject DN: {}", subjectDN); - request.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(new X509Certificate[]{(X509Certificate) clientCert.get()}, certificateForClientAuth)); - } else { - request.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth)); - request.setAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, apimlCertificate)); + Optional clientCert = getClientCertFromHeader(httpServletRequest); + if (certificateValidator.isForwardingEnabled() && certificateValidator.hasGatewayChain(certs)) { + if (clientCert.isPresent()) { + certificateValidator.updateAPIMLPublicKeyCertificates(certs); + // add the client certificate to the certs array + String subjectDN = ((X509Certificate) clientCert.get()).getSubjectX500Principal().getName(); + log.debug("Found client certificate in header, adding it to the request. Subject DN: {}", subjectDN); + httpServletRequest.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(new X509Certificate[]{(X509Certificate) clientCert.get()}, certificateForClientAuth)); + return; + } else if (isClientCertificateIgnored(httpServletRequest)) { + log.debug("Client certificate is ignored."); + httpServletRequest.removeAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE); + httpServletRequest.removeAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE); + return; + } } - log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, request.getAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE)); + httpServletRequest.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth)); + httpServletRequest.setAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, apimlCertificate)); + + log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, httpServletRequest.getAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE)); } } diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java index dff641bfbc..79c611c6d5 100644 --- a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java @@ -486,4 +486,66 @@ void thenClientCertHeaderIgnored() throws ServletException, IOException { } } } + + @Nested + class GivenNoCertificateHeader { + + @BeforeEach + void setUp() { + certificateValidator = mock(CertificateValidator.class); + when(certificateValidator.isForwardingEnabled()).thenReturn(true); + when(certificateValidator.hasGatewayChain(any())).thenReturn(true); + + filter = new CategorizeCertsFilter(new HashSet<>(), certificateValidator); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + chain = new MockFilterChain(); + } + + @Test + void whenClientCertHeaderEmpty_thenAttributesRemovedAndReturn() throws ServletException, IOException { + X509Certificate[] certs = new X509Certificate[]{ + X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1")) + }; + request.setAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, certs); + + request.addHeader(CategorizeCertsFilter.CLIENT_CERT_HEADER, ""); + + filter.doFilter(request, response, chain); + + assertNull(request.getAttribute(CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE), + "Client auth cert attribute should be removed"); + assertNull(request.getAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE), + "Jakarta servlet cert attribute should be removed"); + + assertNotNull(chain.getRequest()); + } + + @Test + void whenClientCertHeaderNotDefined_thenReturnFalse() throws ServletException, IOException { + + filter = new CategorizeCertsFilter(new HashSet<>(), certificateValidator); + + X509Certificate[] certs = new X509Certificate[]{ + X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1")) + }; + request.setAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, certs); + request.setAttribute(CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, certs); + + request.addHeader(CategorizeCertsFilter.CLIENT_CERT_HEADER, ""); + + filter.doFilter(request, response, chain); + + assertNull( + request.getAttribute(CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE), + "Expected client.auth.X509Certificate attribute to be removed" + ); + assertNull( + request.getAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE), + "Expected jakarta.servlet.request.X509Certificate attribute to be removed" + ); + + assertNotNull(chain.getRequest(), "Filter chain should continue normally"); + } + } } diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsFilter.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsFilter.java index 07af80682d..ab6c20bdcb 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsFilter.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsFilter.java @@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.lang.NonNull; import org.springframework.web.filter.OncePerRequestFilter; +import org.zowe.apiml.util.ServletRequestUtils; import org.zowe.commons.attls.InboundAttls; import java.io.ByteArrayInputStream; @@ -34,17 +35,24 @@ @Slf4j public class AttlsFilter extends OncePerRequestFilter { + private static final String CLIENT_CERT_HEADER = "Client-Cert"; + @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { - try { - byte[] certificate = InboundAttls.getCertificate(); - if (certificate != null && certificate.length > 0) { - log.debug("Certificate length: {}", certificate.length); - populateRequestWithCertificate(request, certificate); + if (ServletRequestUtils.isClientCertificateIgnored(request)) { + log.debug("Client certificate is ignored."); + } else { + log.debug("Updating request with client certificate from the AT-TLS context."); + try { + byte[] certificate = InboundAttls.getCertificate(); + if (certificate != null && certificate.length > 0) { + log.debug("Certificate length: {}", certificate.length); + populateRequestWithCertificate(request, certificate); + } + } catch (Exception e) { + logger.error("Not possible to get certificate from AT-TLS context", e); + AttlsErrorHandler.handleError(response, "Exception reading certificate"); } - } catch (Exception e) { - logger.error("Not possible to get certificate from AT-TLS context", e); - AttlsErrorHandler.handleError(response, "Exception reading certificate"); } filterChain.doFilter(request, response); } diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java index 3ee92021b8..27410e62ff 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/filter/AttlsHttpHandler.java @@ -112,6 +112,7 @@ ServerHttpRequest updateCertificate(ServerHttpRequest request, HttpServletReques public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof HttpHandler httpHandler) { return (HttpHandler) (request, response) -> { + log.debug("Initialize reactive AT-TLS handler"); try { var attlsContext = InboundAttls.get(); diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java index 124efc9cb0..13f32517ae 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/ApimlTomcatCustomizer.java @@ -31,6 +31,18 @@ import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.SocketChannel; +/** + * Customizes Tomcat connectors to enable AT-TLS support. + *

    + * This component replaces the default Tomcat socket handler with a custom + * {@link ApimlAttlsHandler} that initializes and disposes AT-TLS contexts for + * each incoming connection. It allows the API ML to operate in AT-TLS mode on z/OS. + *

    + * + *

    + * Activated when server.attlsServer.enabled=true is set. + *

    + */ @Slf4j @Component @ConditionalOnProperty(name = "server.attlsServer.enabled", havingValue = "true") @@ -61,6 +73,10 @@ public void customize(Connector connector) { } } + /** + * Custom Tomcat socket handler that wraps request processing with AT-TLS context + * initialization and cleanup. + */ public static class ApimlAttlsHandler implements AbstractEndpoint.Handler { @Delegate(excludes = Overridden.class) diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java index e4656e3bab..3bd7a1c8c8 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatAcceptFixConfig.java @@ -45,7 +45,7 @@ * This bean is related only to z/OS. */ @Slf4j -@Configuration +@Configuration(proxyBeanMethods = false) public class TomcatAcceptFixConfig { @Value("${server.tomcat.retryRebindTimeoutSecs:10}") diff --git a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java index 619a3f4f54..93b4340509 100644 --- a/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java +++ b/apiml-tomcat-common/src/main/java/org/zowe/apiml/product/web/TomcatKeyringFix.java @@ -20,6 +20,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Customizer for Tomcat SSL configuration that fixes SAF keyring URLs. + *

    + * If a keyStore or trustStore uses a SAF keyring (e.g. {@code safkeyring:///USER/KEYRING}), + * this class reformats the URL and sets default passwords if none are provided. + *

    + */ @Slf4j @Component public class TomcatKeyringFix implements WebServerFactoryCustomizer { @@ -51,6 +58,17 @@ boolean isKeyring(String input) { return matcher.matches(); } + /** + * Normalizes the given keyring URL into a Tomcat-compatible format. + *

    + * For example, converts: + *

    +     * safkeyring:///USER/KEYRING → safkeyring://USER/KEYRING
    +     * 
    + * + * @param keyringUrl the original keyring URL + * @return the normalized URL or {@code null} if the input was {@code null} + */ static String formatKeyringUrl(String keyringUrl) { if (keyringUrl == null) return null; Matcher matcher = KEYRING_PATTERN.matcher(keyringUrl); @@ -60,6 +78,16 @@ static String formatKeyringUrl(String keyringUrl) { return keyringUrl; } + /** + * Customizes the Tomcat {@link Ssl} configuration before the web server starts. + *

    + * If keyring-based stores are detected, this method updates their configuration + * (URL format, alias, and password) to ensure that Tomcat can correctly access + * the keyring at runtime. + *

    + * + * @param factory the Tomcat web server factory being customized + */ @Override public void customize(AbstractConfigurableWebServerFactory factory) { Ssl ssl = factory.getSsl(); diff --git a/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsFilterTest.java b/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsFilterTest.java index 0ff53ec206..a5636de2a7 100644 --- a/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsFilterTest.java +++ b/apiml-tomcat-common/src/test/java/org/zowe/apiml/filter/AttlsFilterTest.java @@ -14,23 +14,24 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.apache.tomcat.util.codec.binary.Base64; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.zowe.commons.attls.ContextIsNotInitializedException; +import org.zowe.commons.attls.InboundAttls; import java.io.IOException; import java.security.cert.CertificateException; +import java.util.Base64; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.*; class AttlsFilterTest { @Test - void providedCertificateInCorrectFormat_thenPopulateRequest() throws CertificateException, ContextIsNotInitializedException { + void providedCertificateInCorrectFormat_thenPopulateRequest() throws CertificateException { AttlsFilter attlsFilter = new AttlsFilter(); String certificate = """ MIID8TCCAtmgAwIBAgIUVyBCWfHF/ZwZKVsBEpTNIBj9mQcwDQYJKoZIhvcNAQEL @@ -58,7 +59,8 @@ void providedCertificateInCorrectFormat_thenPopulateRequest() throws Certificate """.stripIndent(); HttpServletRequest request = new MockHttpServletRequest(); - attlsFilter.populateRequestWithCertificate(request, Base64.decodeBase64(certificate)); + byte[] decoded = Base64.getMimeDecoder().decode(certificate); + attlsFilter.populateRequestWithCertificate(request, decoded); assertNotNull(request.getAttribute("jakarta.servlet.request.X509Certificate")); } @@ -71,4 +73,38 @@ void whenExceptionOccurs_thenCreateCorrectResponse() throws ServletException, IO assertEquals(500, response.getStatus()); } + @Test + void whenClientCertIsEmpty_thenIgnoreCertAndContinueWithChain() throws ServletException, IOException { + AttlsFilter filter = new AttlsFilter(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Client-Cert", ""); + + FilterChain chain = mock(FilterChain.class); + HttpServletResponse response = new MockHttpServletResponse(); + filter.doFilterInternal(request, response, chain); + assertEquals(200, response.getStatus()); + } + + @Test + void whenValidCertificateFromInboundAttls_thenPopulateRequestAndContinueChain() throws ServletException, IOException, CertificateException { + AttlsFilter filter = spy(new AttlsFilter()); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + + byte[] dummyCert = "dummyCertificate".getBytes(); + + try (MockedStatic mockedInboundAttls = mockStatic(InboundAttls.class)) { + mockedInboundAttls.when(InboundAttls::getCertificate).thenReturn(dummyCert); + + doNothing().when(filter).populateRequestWithCertificate(request, dummyCert); + + filter.doFilterInternal(request, response, chain); + + mockedInboundAttls.verify(InboundAttls::getCertificate); + verify(filter, times(1)).populateRequestWithCertificate(request, dummyCert); + verify(chain, times(1)).doFilter(request, response); + } + } + } diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/ServletRequestUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/ServletRequestUtils.java new file mode 100644 index 0000000000..70b3458539 --- /dev/null +++ b/common-service-core/src/main/java/org/zowe/apiml/util/ServletRequestUtils.java @@ -0,0 +1,40 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.util; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * Utility class providing helper methods for working with {@link jakarta.servlet.http.HttpServletRequest}. + */ +@Slf4j +public class ServletRequestUtils { + + private static final String CLIENT_CERT_HEADER = "Client-Cert"; + + /** + * Determines whether the client certificate should be ignored based on the Client-Cert HTTP header. + * @param request the HTTP request to inspect + * @return true if the client certificate should be ignored, false otherwise. + */ + public static boolean isClientCertificateIgnored(HttpServletRequest request) { + var forwardedClientCertificate = request.getHeader(CLIENT_CERT_HEADER); + if (forwardedClientCertificate == null) { + // no header means the certificate shouldn't be removed + log.debug("Request header Client-Cert was not defined."); + return false; + } + // empty header means to ignore the certificate from the request + return StringUtils.isBlank(forwardedClientCertificate); + } +} diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/ServletRequestUtilsTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/ServletRequestUtilsTest.java new file mode 100644 index 0000000000..4e7f116da4 --- /dev/null +++ b/common-service-core/src/test/java/org/zowe/apiml/util/ServletRequestUtilsTest.java @@ -0,0 +1,49 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.util; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServletRequestUtilsTest { + + @Test + void whenClientCertHeaderNotDefined_thenReturnFalse() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + boolean result = ServletRequestUtils.isClientCertificateIgnored(request); + + assertFalse(result, "Expected false when Client-Cert header is not defined"); + } + + @Test + void whenClientCertHeaderEmpty_thenReturnTrue() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Client-Cert", ""); + + boolean result = ServletRequestUtils.isClientCertificateIgnored(request); + + assertTrue(result, "Expected true when Client-Cert header is empty"); + } + + @Test + void whenClientCertHeaderHasValue_thenReturnFalse() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Client-Cert", "some-cert-data"); + + boolean result = ServletRequestUtils.isClientCertificateIgnored(request); + + assertFalse(result, "Expected false when Client-Cert header has a value"); + } +} diff --git a/discoverable-client/build.gradle b/discoverable-client/build.gradle index 272edb15b3..a122c8da84 100644 --- a/discoverable-client/build.gradle +++ b/discoverable-client/build.gradle @@ -50,6 +50,17 @@ bootRun { args project.args.split(',') } + jvmArgs([ + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.invoke=ALL-UNNAMED', + '--add-opens=java.base/java.nio.channels.spi=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', + '--add-opens=java.base/javax.net.ssl=ALL-UNNAMED', + '--add-opens=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + ]) + debugOptions { port = 5012 suspend = false diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java b/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java index 7e05e3b82e..4a870e5e1b 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/DiscoverableClientSampleApplication.java @@ -10,6 +10,7 @@ package org.zowe.apiml.client; +import org.springframework.context.annotation.Import; import org.zowe.apiml.enable.EnableApiDiscovery; import org.zowe.apiml.product.logging.annotations.EnableApimlLogger; import org.zowe.apiml.product.monitoring.LatencyUtilsConfigInitializer; @@ -21,12 +22,16 @@ import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.zowe.apiml.product.web.ApimlTomcatCustomizer; +import org.zowe.apiml.product.web.TomcatAcceptFixConfig; +import org.zowe.apiml.product.web.TomcatKeyringFix; @SpringBootApplication @EnableApiDiscovery @EnableWebSocket @EnableApimlLogger @RequiredArgsConstructor +@Import({TomcatKeyringFix.class, TomcatAcceptFixConfig.class, ApimlTomcatCustomizer.class}) public class DiscoverableClientSampleApplication implements ApplicationListener { private final ServiceStartupEventHandler handler; diff --git a/discoverable-client/src/main/resources/application.yml b/discoverable-client/src/main/resources/application.yml index eb7a82666f..02c6f7c028 100644 --- a/discoverable-client/src/main/resources/application.yml +++ b/discoverable-client/src/main/resources/application.yml @@ -241,3 +241,8 @@ apiml: server: attlsClient: enabled: true + +eureka: + instance: + nonSecurePortEnabled: true + securePortEnabled: false diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java index 1a10fc33dd..90022b1d5a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java @@ -87,17 +87,18 @@ private WebClient.RequestBodySpec getWebclient(ServerRequest serverRequest, Stri .headers(headers -> headers.addAll(serverRequest.headers().asHttpHeaders())) .headers(headers -> headers.remove(CLIENT_CERT_HEADER)); - if (sslInfo != null) { - return request.headers(headers -> { + return request.headers(headers -> { + if (sslInfo != null) { try { headers.add(CLIENT_CERT_HEADER, X509Util.getEncodedClientCertificate(sslInfo)); } catch (CertificateEncodingException e) { throw new IllegalStateException("Cannot forward client certificate", e); } - }); - } - - return request; + } else { + // this empty certificate is to mark request as without x509 because AT-TLS adds client certificate + headers.add(CLIENT_CERT_HEADER, ""); + } + }); } private Mono resend(ServerRequest request, String path, String body) { diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java b/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java index 53eafa9319..7feb3657d0 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/service/RunningService.java @@ -57,6 +57,16 @@ public void start(String... envs) throws IOException { } shellCommand.add(path + "java"); + shellCommand.addAll(Arrays.asList( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", + "--add-opens=java.base/java.nio.channels.spi=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens=java.base/javax.net.ssl=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens=java.base/java.io=ALL-UNNAMED" + )); parametersBefore .forEach((key1, value1) -> shellCommand.add(key1 + '=' + value1)); From ce502b798ba159762a8f9fbefaef8d52e49ff815 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:42:49 +0200 Subject: [PATCH 143/152] fix: correct description in API doc (#4348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: ac892247 Signed-off-by: achmelo <37397715+achmelo@users.noreply.github.com> Co-authored-by: Pavel Jareš <58428711+pj892031@users.noreply.github.com> Signed-off-by: Gowtham Selvaraj --- .../config/SwaggerConfiguration.java | 32 +++++++++++++++++++ apiml-package/src/main/resources/bin/start.sh | 4 +-- .../java/org/zowe/apiml/ModulithConfig.java | 22 +++++++++++++ apiml/src/main/resources/application.yml | 2 ++ .../apiml/caching/config/SwaggerConfig.java | 16 ++++++++++ .../java/org/zowe/apiml/util/CorsUtils.java | 8 +++-- .../org/zowe/apiml/util/CorsUtilsTest.java | 7 ++-- .../gateway/config/ConnectionsConfig.java | 22 ++++++++----- .../apiml/gateway/config/SwaggerConfig.java | 25 ++++++--------- .../src/main/resources/application.yml | 1 + .../config/AdditionalRegistrationTest.java | 2 +- .../gateway/config/ConnectionsConfigTest.java | 7 ++-- .../config/ServiceCorsUpdaterTest.java | 11 +++---- .../ApiCatalogEndpointIntegrationTest.java | 2 -- .../src/main/resources/application.yml | 1 + .../mapping/model/MapperResponse.java | 4 ++- .../service/CorsMetadataProcessorTest.java | 3 +- 17 files changed, 124 insertions(+), 45 deletions(-) diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SwaggerConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SwaggerConfiguration.java index 6f42c1d8cb..82bd16a47d 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SwaggerConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/config/SwaggerConfiguration.java @@ -10,6 +10,10 @@ package org.zowe.apiml.apicatalog.config; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; @@ -21,6 +25,34 @@ @Configuration @ConditionalOnMissingBean(name = "modulithConfig") +@OpenAPIDefinition( + security = { + @SecurityRequirement(name = "LoginBasicAuth"), + @SecurityRequirement(name = "Bearer"), + @SecurityRequirement(name = "CookieAuth") + }, + info = @io.swagger.v3.oas.annotations.info.Info(title = "API Catalog", description = """ + REST API for the API Catalog, which is a component of the API Mediation Layer. + Use this API to perform tasks such as retrieve the catalog containers with the Open API documentation. + """) +) +@io.swagger.v3.oas.annotations.security.SecurityScheme( + name = "LoginBasicAuth", + type = SecuritySchemeType.HTTP, + scheme = "basic" +) +@io.swagger.v3.oas.annotations.security.SecurityScheme( + name = "Bearer", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT" +) +@io.swagger.v3.oas.annotations.security.SecurityScheme( + name = "CookieAuth", + type = SecuritySchemeType.APIKEY, + in = SecuritySchemeIn.COOKIE, + paramName = "apimlAuthenticationToken" +) public class SwaggerConfiguration { @Value("${apiml.service.apiDoc.title}") diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index e1a7959e6c..0ba5a93c80 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -124,12 +124,12 @@ if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi -if [ "${ZWE_configs_debug:-${ZWE_components_gateway_debug:-${ZWE_components_discovery_debug:-false}}}" = "true" ]; then +if [ "${ZWE_components_gateway_debug:-${ZWE_configs_debug:-false}}" = "true" ]; then # TODO should this be a merge of the profiles in gateway and discovery (and other modules later added?) if [ -n "${ZWE_configs_spring_profiles_active:-${ZWE_components_gateway_spring_profiles_active:-${ZWE_components_discovery_spring_profiles_active}}}" ]; then ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active:-${ZWE_components_gateway_spring_profiles_active:-${ZWE_components_discovery_spring_profiles_active}}}," fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}debug" + add_profile "debug" fi if [ "${ZWE_configs_apiml_security_auth_uniqueCookie:-${ZWE_components_gateway_apiml_security_auth_uniqueCookie:-false}}" = "true" ]; then diff --git a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java index 4e257c7979..5cc979be51 100644 --- a/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/ModulithConfig.java @@ -19,6 +19,11 @@ import com.netflix.discovery.shared.Applications; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import jakarta.servlet.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -81,6 +86,23 @@ @EnableConfigurationProperties @DependsOn(value = {"gatewayHealthIndicator"}) @Slf4j +@OpenAPIDefinition( + security = { + @SecurityRequirement(name = "LoginBasicAuth"), + @SecurityRequirement(name = "ClientCert") + }, + info = @Info(title = "API Mediation Layer", description = "The API Mediation Layer REST API.") +) +@SecurityScheme( + name = "LoginBasicAuth", + type = SecuritySchemeType.HTTP, + scheme = "basic" +) +@SecurityScheme( + type = SecuritySchemeType.MUTUALTLS, + name = "ClientCert", + description = "Client certificate X509" +) public class ModulithConfig implements InitializingBean { private final ApplicationContext applicationContext; diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 834cbf6690..b2bc6fa823 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -189,6 +189,7 @@ apiml: service: apimlId: apiml1 corsEnabled: true + corsAllowedEndpoints: /gateway/**,/apicatalog/**,/cachingservice/**,/v3/api-docs/** allowEncodedSlashes: true discoveryServiceUrls: https://localhost:10011/eureka/ forwardClientCertEnabled: false @@ -343,6 +344,7 @@ logging: org.springframework.context.support.[PostProcessorRegistrationDelegate$BeanPostProcessorChecker]: DEBUG org.springframework.security: TRACE org.springframework.security.web: TRACE + org.springdoc.core: DEBUG org.zowe.apiml: DEBUG reactor.netty: DEBUG reactor.netty.http.client: DEBUG diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java index 6b47c66f0a..acc875d163 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java @@ -10,6 +10,10 @@ package org.zowe.apiml.caching.config; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.beans.factory.annotation.Value; @@ -19,6 +23,18 @@ @Configuration @ConditionalOnMissingBean(name = "modulithConfig") +@OpenAPIDefinition( + security = @SecurityRequirement(name = "ClientCert"), + info = @io.swagger.v3.oas.annotations.info.Info(title = "Caching service", description = """ + REST API for the Caching service, which is a module of the API Mediation Layer. + Use this API to perform tasks such as store, read and update items under the client certificate as a key. + """) +) +@SecurityScheme( + type = SecuritySchemeType.MUTUALTLS, + name = "ClientCert", + description = "Client certificate X509" +) public class SwaggerConfig { @Value("${apiml.service.title}") 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 1a3b0d9a72..b03053bb5c 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,6 +10,7 @@ package org.zowe.apiml.util; +import lombok.NonNull; import org.apache.logging.log4j.util.TriConsumer; import org.springframework.web.cors.CorsConfiguration; @@ -24,11 +25,12 @@ public class CorsUtils { private final List allowedCorsHttpMethods; private final boolean corsEnabled; 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 List corsAllowedEndpoints; - public CorsUtils(boolean corsEnabled, List corsAllowedMethods) { + public CorsUtils(boolean corsEnabled, List corsAllowedMethods, @NonNull List allowedEndpoints) { this.corsEnabled = corsEnabled; this.allowedCorsHttpMethods = corsAllowedMethods; + this.corsAllowedEndpoints = allowedEndpoints; } public boolean isCorsEnabledForService(Map metadata) { @@ -75,7 +77,7 @@ public void registerDefaultCorsConfiguration(BiConsumer metadata = new HashMap<>(); List defaultCorsMethods = List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"); + List allowedEndpoints = List.of("/gateway/**"); + @BeforeEach void setup() { metadata.put("apiml.routes.v1.gateway", "api/v1"); @@ -37,7 +40,7 @@ class GivenCorsEnabled { @Nested class givenDefaultCorsAllowedMethods { - CorsUtils corsUtils = new CorsUtils(true, defaultCorsMethods); + CorsUtils corsUtils = new CorsUtils(true, defaultCorsMethods, allowedEndpoints); @Test void registerDefaultConfig() { @@ -93,7 +96,7 @@ void registerConfigForServiceWithCustomOrigins() { @Nested class GivenCorsDisabled { - CorsUtils corsUtils = new CorsUtils(false, null); + CorsUtils corsUtils = new CorsUtils(false, null, Arrays.asList("/gateway/**", "/api-docs")); @Test void registerEmptyDefaultConfig() { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index 4cb9e28b9b..b6ef7af67a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -99,6 +99,9 @@ public class ConnectionsConfig { @Value("${apiml.service.externalUrl:}") private String externalUrl; + @Value("${apiml.service.corsAllowedEndpoints:/gateway/**}") + private final List corsEnabledEndpoints; + /** * @param httpClient default http client * @param headersFiltersProvider header filter for spring gateway router @@ -149,7 +152,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro @RefreshScope @ConditionalOnMissingBean(EurekaClient.class) CloudEurekaClient primaryEurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, - @Autowired(required = false) HealthCheckHandler healthCheckHandler) { + @Autowired(required = false) HealthCheckHandler healthCheckHandler) { ApplicationInfoManager appManager; if (AopUtils.isAopProxy(manager)) { appManager = ProxyUtils.getTargetObject(manager); @@ -242,7 +245,7 @@ private boolean isRouteKey(String key) { return StringUtils.startsWith(key, ROUTES + ".") && ( StringUtils.endsWith(key, "." + ROUTES_GATEWAY_URL) || - StringUtils.endsWith(key, "." + ROUTES_SERVICE_URL) + StringUtils.endsWith(key, "." + ROUTES_SERVICE_URL) ); } @@ -272,12 +275,12 @@ Customizer defaultCustomizer() { .timeLimiterConfig( TimeLimiterConfig.custom() .timeoutDuration(Duration.ofMillis(config.getRequestConnectionTimeout())) - .build()).build()); + .build()).build()); } @Bean CorsUtils corsUtils() { - return new CorsUtils(corsEnabled, corsAllowedMethods); + return new CorsUtils(corsEnabled, corsAllowedMethods, corsEnabledEndpoints); } @Bean @@ -285,7 +288,7 @@ WebFilter corsWebFilter(ServiceCorsUpdater serviceCorsUpdater) { return new CorsWebFilter(serviceCorsUpdater.getUrlBasedCorsConfigurationSource()); } - public InstanceInfo create(EurekaInstanceConfig config) { + public InstanceInfo create(EurekaInstanceConfig config) { LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder() .setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds()) .setDurationInSecs(config.getLeaseExpirationDurationInSeconds()); @@ -321,7 +324,7 @@ public InstanceInfo create(EurekaInstanceConfig config) { .setSecureVIPAddress(config.getSecureVirtualHostName()) .setHomePageUrl(null, UriComponentsBuilder.fromUriString(externalUrl).path(config.getHomePageUrlPath()).toUriString()) .setStatusPageUrl(null, UriComponentsBuilder.fromUriString(externalUrl).path(config.getStatusPageUrlPath()).toUriString()) - .setHealthCheckUrls(config.getHealthCheckUrlPath(), null,null) + .setHealthCheckUrls(config.getHealthCheckUrlPath(), null, null) .setASGName(config.getASGName()); // Start off with the STARTING state to avoid traffic @@ -331,8 +334,7 @@ public InstanceInfo create(EurekaInstanceConfig config) { log.info("Setting initial instance status as: " + initialStatus); } builder.setStatus(initialStatus); - } - else { + } else { if (log.isInfoEnabled()) { log.info("Setting initial instance status as: " + InstanceInfo.InstanceStatus.UP + ". This may be too early for the instance to advertise itself as available. " @@ -398,9 +400,13 @@ public String getStatusPageUrl() { interface NonDelegated { String getHostName(boolean refresh); + String getHealthCheckUrl(); + String getSecureHealthCheckUrl(); + String getHomePageUrl(); + String getStatusPageUrl(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java index f6160cff96..25bd29ec0d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/SwaggerConfig.java @@ -13,7 +13,6 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -48,8 +47,14 @@ @ConditionalOnMissingBean(name = "modulithConfig") @RequiredArgsConstructor @OpenAPIDefinition( - security = @SecurityRequirement(name = "LoginBasicAuth"), - info = @Info(title = "API Gateway", description = "REST API for the API Gateway, which is a component of the API\nMediation Layer. Use this API to perform tasks such as logging in with the\nmainframe credentials and checking authorization to mainframe resources.") + security = { + @SecurityRequirement(name = "LoginBasicAuth"), + @SecurityRequirement(name = "ClientCert") + }, + info = @Info(title = "API Gateway", description = """ + REST API for the API Gateway, which is a component of the API Mediation Layer. + Use this API to perform tasks such as logging in with the mainframe credentials and checking authorization to mainframe resources. + """) ) @SecurityScheme( name = "LoginBasicAuth", @@ -57,19 +62,7 @@ scheme = "basic" ) @SecurityScheme( - name = "Bearer", - type = SecuritySchemeType.HTTP, - scheme = "bearer", - bearerFormat = "JWT" -) -@SecurityScheme( - name = "CookieAuth", - type = SecuritySchemeType.APIKEY, - in = SecuritySchemeIn.COOKIE, - paramName = "apimlAuthenticationToken" -) -@SecurityScheme( - type = SecuritySchemeType.HTTP, + type = SecuritySchemeType.MUTUALTLS, name = "ClientCert", description = "Client certificate X509" ) diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 3f1fb254a4..fd1ca812fb 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -95,6 +95,7 @@ apiml: service: apimlId: apiml1 corsEnabled: true + corsAllowedEndpoints: /gateway/** allowEncodedSlashes: true discoveryServiceUrls: https://localhost:10011/eureka/ forwardClientCertEnabled: false diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java index cfc22c4d00..fe2ec544cf 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java @@ -67,7 +67,7 @@ public class AdditionalRegistrationTest { @BeforeEach void setUp() { - connectionsConfig = new ConnectionsConfig(context, config); + connectionsConfig = new ConnectionsConfig(context, config, Arrays.asList("/gateway/**", "/apicatalog/**")); } @ExtendWith(MockitoExtension.class) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java index 2f2e4ad3a5..da5e9cf787 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java @@ -52,6 +52,7 @@ import java.security.*; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -294,7 +295,7 @@ private EurekaInstanceConfig createConfig() { @Test void givenInvalidUrl_whenCreate_thenThrowAnException() { - var connectionsConfig = new ConnectionsConfig(null, null); + var connectionsConfig = new ConnectionsConfig(null, null, Collections.emptyList()); ReflectionTestUtils.setField(connectionsConfig, "externalUrl", "invalidUrl"); var e = assertThrows(RuntimeException.class, () -> connectionsConfig.create(createConfig())); assertInstanceOf(MalformedURLException.class, e.getCause()); @@ -303,7 +304,7 @@ void givenInvalidUrl_whenCreate_thenThrowAnException() { @Test void givenValidInputs_whenCreate_thenCreateIt() { var config = createConfig(); - var connectionsConfig = new ConnectionsConfig(null, null); + var connectionsConfig = new ConnectionsConfig(null, null, Collections.emptyList()); ReflectionTestUtils.setField(connectionsConfig, "externalUrl", "https://domain:1234/"); InstanceInfo instanceInfo = connectionsConfig.create(config); @@ -323,7 +324,7 @@ void givenMetadataWithUrl_whenCreate_thenUpdateThem() { var config = createConfig(); doReturn(metadata).when(config).getMetadataMap(); - var connectionsConfig = new ConnectionsConfig(null, null); + var connectionsConfig = new ConnectionsConfig(null, null, Collections.emptyList()); ReflectionTestUtils.setField(connectionsConfig, "externalUrl", "https://domain:1234/"); InstanceInfo instanceInfo = connectionsConfig.create(config); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java index d7f76cbf43..a27bd9e4e6 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ServiceCorsUpdaterTest.java @@ -37,10 +37,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -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.*; @ExtendWith(MockitoExtension.class) class ServiceCorsUpdaterTest { @@ -48,7 +45,8 @@ class ServiceCorsUpdaterTest { private static final String SERVICE_ID = "myserviceid"; private static final String APIML_ID = "apimlid"; - private CorsUtils corsUtils = spy(new CorsUtils(true, List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"))); + List allowedEndpoints = List.of("/gateway/**"); + private CorsUtils corsUtils = spy(new CorsUtils(true, List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"), allowedEndpoints)); @Mock private ReactiveDiscoveryClient discoveryClient; @@ -106,7 +104,8 @@ void givenApimlId_whenSetCors_thenServiceIdIsReplacedWithApimlId() { @Test void givenNoApimlId_whenSetCors_thenServiceIdIsUsed() { - TriConsumer corsLambda = getCorsLambda(md -> {}); + TriConsumer corsLambda = getCorsLambda(md -> { + }); corsLambda.accept(null, SERVICE_ID, null); diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java index 40601bf50f..cbbf3dd296 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/apicatalog/ApiCatalogEndpointIntegrationTest.java @@ -165,7 +165,6 @@ void whenSpecificCatalogApiDoc() throws Exception { assertNotNull(componentSchemas.get("APIContainer"), apiCatalogSwagger); assertNotNull(componentSchemas.get("APIService"), apiCatalogSwagger); assertNotNull(securitySchemes.get(BASIC_SCHEME), apiCatalogSwagger); - assertNotNull(securitySchemes.get("CookieAuth"), apiCatalogSwagger); } @Test @@ -200,7 +199,6 @@ void whenDefaultCatalogApiDoc() throws Exception { assertNotNull(componentSchemas.get("APIContainer"), apiCatalogSwagger); assertNotNull(componentSchemas.get("APIService"), apiCatalogSwagger); assertNotNull(securitySchemes.get(BASIC_SCHEME), apiCatalogSwagger); - assertNotNull(securitySchemes.get("CookieAuth"), apiCatalogSwagger); } } diff --git a/mock-services/src/main/resources/application.yml b/mock-services/src/main/resources/application.yml index 3118de48a2..c462f69496 100644 --- a/mock-services/src/main/resources/application.yml +++ b/mock-services/src/main/resources/application.yml @@ -48,6 +48,7 @@ zss: userMapping: "[zoweson@zowe.com]": USER + --- spring.config.activate.on-profile: attlsServer diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/model/MapperResponse.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/model/MapperResponse.java index 73452111f1..060c497095 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/model/MapperResponse.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/mapping/model/MapperResponse.java @@ -14,7 +14,9 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; @Data @@ -36,7 +38,7 @@ public class MapperResponse { private int racfRs; @InjectApimlLogger - private final ApimlLogger apimlLog = ApimlLogger.empty(); + private final ApimlLogger apimlLog = ApimlLogger.of(EurekaMetadataParser.class, YamlMessageServiceInstance.getInstance()); public String toString() { return "User: " + userId + ", rc=" + rc + ", safRc=" + safRc + ", racfRc=" + racfRc + ", racfRs=" + racfRs; diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java index ef0685120e..51d62f1b86 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/metadata/service/CorsMetadataProcessorTest.java @@ -33,11 +33,12 @@ class CorsMetadataProcessorTest { private CorsUtils corsUtils; private UrlBasedCorsConfigurationSource configurationSource; private ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(CorsConfiguration.class); + List allowedEndpoints = List.of("/gateway/**"); @BeforeEach void setUp() { configurationSource = mock(UrlBasedCorsConfigurationSource.class); - corsUtils = new CorsUtils(true, List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS")); + corsUtils = new CorsUtils(true, List.of("GET", "HEAD", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"), allowedEndpoints); } @Nested From 8827b64e79b116ca4c08f735aa0d6a44e9db8e0d Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:25:44 +0200 Subject: [PATCH 144/152] chore: sperate enablers and core components release (#4353) Signed-off-by: ac892247 Co-authored-by: Zowe Robot Signed-off-by: Gowtham Selvaraj --- .../workflows/automated-release-nodejs.yml | 132 ++++++++++++++++++ .github/workflows/automated-release.yml | 25 +--- api-catalog-ui/frontend/.env | 2 +- apiml/src/test/resources/application.yml | 10 +- build.gradle | 78 ++++++++++- enabler.properties | 2 + gradle.properties | 4 +- gradle/publish.gradle | 31 +++- 8 files changed, 249 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/automated-release-nodejs.yml create mode 100644 enabler.properties diff --git a/.github/workflows/automated-release-nodejs.yml b/.github/workflows/automated-release-nodejs.yml new file mode 100644 index 0000000000..313e097cc7 --- /dev/null +++ b/.github/workflows/automated-release-nodejs.yml @@ -0,0 +1,132 @@ +# This workflow will release project with Gradle +name: Automated publish of nodejs and python enabler + +on: + workflow_dispatch: + inputs: + scope: + description: 'Specify scope that is to be used.' + required: true + type: choice + options: + - patch + - minor + - major + default: 'patch' + version: + description: 'Set version to override value in package.json in case of existing version in npm registry. Use the latest version in npm registry so the release plugin will properly increase from the current version.' + required: false + type: string + enablerType: + description: 'Specify which enabler to release.' + type: choice + required: false + options: + - node + - python + default: node + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 40 + if: inputs.enablerType == 'node' + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.ZOWE_ROBOT_TOKEN }} + + - uses: ./.github/actions/setup + + - name: Clean git + run: git reset --hard HEAD + + - name: Set email + run: git config user.email "zowe-robot@users.noreply.github.com" + + - name: Set name + run: git config user.name "Zowe Robot" + + - name: Update version manually + if: github.event.inputs.version != '' + run: | + cd onboarding-enabler-nodejs + sed -i.bak "s/\"version\": \".*\"/\"version\": \"$NEW_VERSION\"/" package.json + env: + NEW_VERSION: ${{ github.event.inputs.version }} + + - name: Release to NPM automatic + shell: bash + run: | + cd onboarding-enabler-nodejs + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc + npm version ${{ github.event.inputs.scope || env.DEFAULT_SCOPE }} + npm publish --access public + git add package.json + git add package-lock.json + git commit -m "[skip ci] Update version" + git push --no-verify + cd .. + env: + DEFAULT_SCOPE: 'patch' + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - uses: ./.github/actions/teardown + + release-python-enabler: + runs-on: ubuntu-latest + timeout-minutes: 40 + needs: release + if: inputs.enablerType == 'python' + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.ZOWE_ROBOT_TOKEN }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install Dependencies + run: | + cd onboarding-enabler-python + python -m venv venv + source venv/bin/activate + python -m pip install --upgrade pip setuptools wheel twine build + if [ -f Pipfile.lock ]; then + pip install pipenv + pipenv install --deploy --ignore-pipfile + fi + + - name: Build Python Package + run: | + export SETUPTOOLS_SCM_PRETEND_VERSION=${{ github.event.inputs.version }} + ./gradlew onboarding-enabler-python:buildPython + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels + path: onboarding-enabler-python/dist/ + + - name: Install twine globally + run: python3 -m pip install --upgrade twine + + - uses: zowe-actions/octorelease@v1 + env: + GIT_COMMITTER_NAME: ${{ secrets.ZOWE_ROBOT_USER }} + GIT_COMMITTER_EMAIL: ${{ secrets.ZOWE_ROBOT_EMAIL }} + GIT_CREDENTIALS: x-access-token:${{ secrets.ZOWE_ROBOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_ROBOT_TOKEN }} + with: + working-dir: onboarding-enabler-python + dry-run: ${{ inputs.dry-run }} + new-version: ${{ steps.update-version.outputs.version }} diff --git a/.github/workflows/automated-release.yml b/.github/workflows/automated-release.yml index 1a9b08ce58..af3c731716 100644 --- a/.github/workflows/automated-release.yml +++ b/.github/workflows/automated-release.yml @@ -16,7 +16,11 @@ on: - minor - major default: 'patch' - + enablers: + description: 'Release enablers only.' + required: false + type: boolean + default: false jobs: release: runs-on: ubuntu-latest @@ -39,22 +43,6 @@ jobs: - name: Set name run: git config user.name "Zowe Robot" -# - name: Release to NPM automatic -# shell: bash -# run: | -# cd onboarding-enabler-nodejs -# echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc -# npm version ${{ github.event.inputs.scope || env.DEFAULT_SCOPE }} --allow-same-version -# npm publish --access public -# git add package.json -# git add package-lock.json -# git commit -m "[skip ci] Update version" -# git push --no-verify -# cd .. -# env: -# DEFAULT_SCOPE: 'patch' -# NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Build with Gradle shell: bash run: | @@ -63,7 +51,7 @@ jobs: - name: Release with Gradle automatic shell: bash run: | - ./gradlew release -Prelease.useAutomaticVersion=true -Prelease.scope=${{ github.event.inputs.scope || env.DEFAULT_SCOPE }} -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD + ./gradlew release $RELEASE_ARGS -Prelease.useAutomaticVersion=true -Prelease.scope=${{ github.event.inputs.scope || env.DEFAULT_SCOPE }} -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD released_version=$(cat gradle.properties | grep "version=" | sed "s/version=//g") sed -i "/REACT_APP_ZOWE_BUILD_INFO=/c\REACT_APP_ZOWE_BUILD_INFO=${released_version}" api-catalog-ui/frontend/.env git add api-catalog-ui/frontend/.env @@ -75,6 +63,7 @@ jobs: DEFAULT_SCOPE: 'patch' BUILD_NUMBER: ${{ github.run_number }} BRANCH_NAME: ${{ github.ref_name }} + RELEASE_ARGS: ${{ github.event.inputs.enablers == 'true' && '-PreleaseEnablers' || '' }} - name: Store test results uses: actions/upload-artifact@v4 diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index ca68caffd5..9f60d79c78 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.15-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.16-SNAPSHOT diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml index fd8ae11935..63dced5234 100644 --- a/apiml/src/test/resources/application.yml +++ b/apiml/src/test/resources/application.yml @@ -19,7 +19,15 @@ eureka: server: max-threads-for-peer-replication: 2 useReadOnlyResponseCache: false - +management: + endpoint: + gateway: + access: unrestricted + endpoints: + web: + base-path: /application + exposure: + include: "*" apiml: catalog: serviceId: apicatalog diff --git a/build.gradle b/build.gradle index fbceaf60ff..6f31a0d9f7 100644 --- a/build.gradle +++ b/build.gradle @@ -166,6 +166,60 @@ subprojects { } } +ext.enablers = [ + 'onboarding-enabler-spring', + 'onboarding-enabler-java', + //'onboarding-enabler-micronaut' + 'zaas-client', +] + +ext.javaLibraries = [ + 'apiml-utility', + 'apiml-common', + 'apiml-security-common', + 'apiml-tomcat-common', + 'certificate-analyser', + 'common-service-core', + 'security-service-client-spring', + 'apiml-sample-extension', + 'apiml-sample-extension-package', + 'apiml-extension-loader' +] + +ext.servicesToPublish = [ + 'apiml', + 'apiml-package', + 'apiml-common-lib-package', + 'api-catalog-services', + 'api-catalog-package', + 'caching-service', + 'caching-service-package', + 'discovery-service', + 'discovery-package', + 'zaas-service', + 'zaas-package', + 'gateway-service', + 'gateway-package' +] +ext.projectsToPublish = ext.servicesToPublish + ext.javaLibraries + +task 'buildZoweServer' { + group 'Zowe Server components' + description 'Builds only main zowe server components.' + dependsOn projectsToPublish.collect { "$it:build" } + doLast { + println "Successfully built all 'Zowe Server components' modules." + } +} +task 'buildEnablers' { + group 'Enablers' + description 'Builds only enablers.' + dependsOn enablers.collect { "$it:build" } + doLast { + println "Successfully built all 'Enabler' modules." + } +} + task buildCore(dependsOn: [':discovery-service:build', ':api-catalog-services:build', ':api-catalog-ui:build', ':discoverable-client:build', ':zaas-client:build', ':apiml-sample-extension:build', ':gateway-service:build']) { description "Build core components" @@ -214,9 +268,17 @@ task runIdPrefixReplacerTests(dependsOn: [":integration-tests:runIdPrefixReplace task publishAllVersions { group 'Zowe Publishing' - description 'Publish ZIP file and SDK libraries to Zowe Artifactory' + description 'Publish Zowe server ZIP files and libraries to Zowe Artifactory' + doLast { + println 'Published Zowe server ZIP files and libraries' + } +} + +task publishAllSDKVersions { + group 'Zowe Publishing' + description 'Publish SDK libraries to Zowe Artifactory' doLast { - println 'Published ZIP file and libraries' + println 'Published SDK libraries' } } @@ -266,7 +328,8 @@ task runChaoticHATests(dependsOn: ":integration-tests:runChaoticHATests") { clean.dependsOn nodejsClean -publishAllVersions.dependsOn publishSdkArtifacts +publishAllVersions.dependsOn publishZoweServerArtifacts +publishAllSDKVersions.dependsOn publishSDKArtifacts //-----------Release part start apply plugin: 'net.researchgate.release' @@ -285,7 +348,8 @@ release { tagCommitMessage = 'Release:' tagTemplate = 'v${version}' newVersionCommitMessage = 'Create new version:' - versionPropertyFile = 'gradle.properties' + versionPropertyFile = project.hasProperty('releaseEnablers') ? 'enabler.properties' : 'gradle.properties' + buildTasks = project.hasProperty('releaseEnablers') ? ['buildEnablers'] : ['buildZoweServer'] if (releaseScope == 'minor') { versionPatterns = [ @@ -314,7 +378,11 @@ release { } } -afterReleaseBuild.dependsOn publishAllVersions +if(project.hasProperty('releaseEnablers')) { + afterReleaseBuild.dependsOn publishAllSDKVersions +} else { + afterReleaseBuild.dependsOn publishAllVersions +} //-----------Release part end diff --git a/enabler.properties b/enabler.properties new file mode 100644 index 0000000000..d3a745bf0b --- /dev/null +++ b/enabler.properties @@ -0,0 +1,2 @@ + +version=3.3.17-SNAPSHOT diff --git a/gradle.properties b/gradle.properties index 5ef271d21e..d58b2d2255 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,10 +17,8 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.15-SNAPSHOT +version=3.3.16-SNAPSHOT -defaultSpringBootVersion=2.0.2.RELEASE -defaultSpringBootCloudVersion=2.0.0.RELEASE nodejsVersion=14.16.0 cleanNodeModules=false cleanNode=false diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 12b25bd6b8..d9532f04e0 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -1,7 +1,8 @@ ext.enablers = [ 'onboarding-enabler-spring', - 'onboarding-enabler-java' + 'onboarding-enabler-java', //'onboarding-enabler-micronaut' + 'zaas-client', ] ext.javaLibraries = [ @@ -12,7 +13,6 @@ ext.javaLibraries = [ 'certificate-analyser', 'common-service-core', 'security-service-client-spring', - 'zaas-client', 'apiml-sample-extension', 'apiml-sample-extension-package', 'apiml-extension-loader' @@ -34,8 +34,10 @@ ext.servicesToPublish = [ 'gateway-package' ] +ext.zoweComponentsToPublish = ext.servicesToPublish + ext.javaLibraries +ext.projectsToPublish = ext.zoweComponentsToPublish + ext.enablers + ext.sdksToPublish = ext.enablers + ext.javaLibraries -ext.projectsToPublish = ext.servicesToPublish + ext.sdksToPublish configure(subprojects.findAll { it.name in projectsToPublish }) { apply plugin: 'maven-publish' @@ -133,17 +135,32 @@ configure(subprojects.findAll { it.name in servicesToPublish }) { } -ext.publishTasksList = projectsToPublish.collect { +ext.publishZoweServerTasksList = zoweComponentsToPublish.collect { ":" + it + ":publish" } + ":platform:publish" +ext.publishTasksList = enablers.collect { + ":" + it + ":publish" +} + ":platform:publish" + +//noinspection GroovyAssignabilityCheck +tasks.register('publishZoweServerArtifacts') { + doLast { + println 'Published Zowe Server component libraries to Zowe Artifactory' + } + + group 'Zowe Publishing' + description 'Publish Zowe Server component libraries to Zowe Artifactory' + dependsOn publishZoweServerTasksList +} + //noinspection GroovyAssignabilityCheck -task publishSdkArtifacts { +tasks.register('publishSDKArtifacts') { doLast { - println 'Published SDK libraries for main version of Spring Boot to Zowe Artifactory' + println 'Published SDK libraries to Zowe Artifactory' } group 'Zowe Publishing' - description 'Publish SDK libraries for main version of Spring Boot to Zowe Artifactory' + description 'Publish SDK libraries to Zowe Artifactory' dependsOn publishTasksList } From bc9c2267988fd762985f43a2ea5a295fac86ed1d Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Wed, 22 Oct 2025 12:26:52 +0000 Subject: [PATCH 145/152] [skip ci] Update version Signed-off-by: Gowtham Selvaraj --- onboarding-enabler-nodejs/package-lock.json | 4 ++-- onboarding-enabler-nodejs/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/onboarding-enabler-nodejs/package-lock.json b/onboarding-enabler-nodejs/package-lock.json index 8a82817c18..f9964bbe8f 100644 --- a/onboarding-enabler-nodejs/package-lock.json +++ b/onboarding-enabler-nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zowe/apiml-onboarding-enabler-nodejs", - "version": "3.2.2", + "version": "3.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zowe/apiml-onboarding-enabler-nodejs", - "version": "3.2.2", + "version": "3.2.3", "license": "EPL-2.0", "dependencies": { "async": "3.2.6", diff --git a/onboarding-enabler-nodejs/package.json b/onboarding-enabler-nodejs/package.json index 680ab2dd88..08eb9ecea7 100644 --- a/onboarding-enabler-nodejs/package.json +++ b/onboarding-enabler-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/apiml-onboarding-enabler-nodejs", - "version": "3.2.2", + "version": "3.2.3", "description": "NodeJS enabler for Zowe API Mediation Layer", "type": "module", "main": "src/index.js", From aa387c8dac6d68b3016b8c379ec2942cc5c917ff Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 23 Oct 2025 08:23:12 +0000 Subject: [PATCH 146/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.16'. Signed-off-by: Gowtham Selvaraj --- enabler.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enabler.properties b/enabler.properties index d3a745bf0b..58157206b6 100644 --- a/enabler.properties +++ b/enabler.properties @@ -1,2 +1,2 @@ -version=3.3.17-SNAPSHOT +version=3.3.16 From fb8ec9c3cca57adc849378b210b09824942a36e2 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:48:08 +0200 Subject: [PATCH 147/152] chore: separate tag for enablers (#4356) Signed-off-by: ac892247 Signed-off-by: Gowtham Selvaraj --- build.gradle | 21 +++++++++++++++-- enabler.properties | 2 +- gradle/publish.gradle | 55 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 6f31a0d9f7..f29b9d5e2f 100644 --- a/build.gradle +++ b/build.gradle @@ -336,6 +336,21 @@ apply plugin: 'net.researchgate.release' ext.releaseScope = project.hasProperty('release.scope') ? project.getProperty('release.scope') : 'patch' + + +//1. Define properties for enablers +def enablerProps = new Properties() +def enablerPropsFile = new File(rootProject.projectDir, "enabler.properties") + +//2. Load the file if it exists +if (enablerPropsFile.exists()) { + enablerPropsFile.withInputStream { stream -> + enablerProps.load(stream) + } +} + +//3. Get the version, or use the rootProject.version as a fallback +def sdkVersion = enablerProps.getProperty("version") release { failOnCommitNeeded = true failOnPublishNeeded = true @@ -346,11 +361,13 @@ release { preCommitText = '[Gradle Release plugin]' preTagCommitMessage = '[skip ci] Before tag commit' tagCommitMessage = 'Release:' - tagTemplate = 'v${version}' + tagTemplate = project.hasProperty('releaseEnablers') ? 'enablers-v${version}' : 'v${version}' newVersionCommitMessage = 'Create new version:' versionPropertyFile = project.hasProperty('releaseEnablers') ? 'enabler.properties' : 'gradle.properties' buildTasks = project.hasProperty('releaseEnablers') ? ['buildEnablers'] : ['buildZoweServer'] - + if(project.hasProperty('releaseEnablers')){ + project.setVersion(sdkVersion) + } if (releaseScope == 'minor') { versionPatterns = [ /[.]*\.(\d+)\.(\d+)[.]*/: { Matcher m, Project p -> m.replaceAll(".${(m[0][1] as int) + 1}.0") } diff --git a/enabler.properties b/enabler.properties index 58157206b6..d3a745bf0b 100644 --- a/enabler.properties +++ b/enabler.properties @@ -1,2 +1,2 @@ -version=3.3.16 +version=3.3.17-SNAPSHOT diff --git a/gradle/publish.gradle b/gradle/publish.gradle index d9532f04e0..2bf95d1834 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -35,11 +35,10 @@ ext.servicesToPublish = [ ] ext.zoweComponentsToPublish = ext.servicesToPublish + ext.javaLibraries -ext.projectsToPublish = ext.zoweComponentsToPublish + ext.enablers ext.sdksToPublish = ext.enablers + ext.javaLibraries -configure(subprojects.findAll { it.name in projectsToPublish }) { +configure(subprojects.findAll { it.name in zoweComponentsToPublish }) { apply plugin: 'maven-publish' apply plugin: 'java' @@ -71,12 +70,58 @@ configure(subprojects.findAll { it.name in projectsToPublish }) { } } +//1. Define properties for enablers +def enablerProps = new Properties() +def enablerPropsFile = new File(rootProject.projectDir, "enabler.properties") + +//2. Load the file if it exists +if (enablerPropsFile.exists()) { + enablerPropsFile.withInputStream { stream -> + enablerProps.load(stream) + } +} + +//3. Get the version, or use the rootProject.version as a fallback +def sdkVersion = enablerProps.getProperty("version") + +configure(subprojects.findAll { it.name in sdksToPublish }) { + apply plugin: 'maven-publish' + apply plugin: 'java' + + publishing { + repositories.maven { + credentials { + username project.hasProperty("zowe.deploy.username") ? project.getProperty("zowe.deploy.username") : "" + password project.hasProperty("zowe.deploy.password") ? project.getProperty("zowe.deploy.password") : "" + } + if (sdkVersion.endsWith("-SNAPSHOT")) { + setUrl(artifactoryPublishingMavenSnapshotRepo) + } else { + setUrl(artifactoryPublishingMavenRepo) + } + } + + publications { + mavenJava(MavenPublication) { + pom { + licenses { + license { + name = 'Eclipse Public License, v2.0' + url = 'https://www.eclipse.org/legal/epl-2.0/' + } + } + } + } + } + } +} + configure(subprojects.findAll { it.name in sdksToPublish }) { publishing { publications { mavenJava(MavenPublication) { groupId 'org.zowe.apiml.sdk' - version rootProject.version + version sdkVersion artifactId "${project.name}" from components.java @@ -95,11 +140,11 @@ configure(subprojects.findAll { it.name in sdksToPublish }) { tasks.withType(Jar) { manifest { attributes "Specification-Title": project.name - attributes "Specification-Version": rootProject.version + attributes "Specification-Version": sdkVersion attributes "Specification-Vendor": "Zowe org" attributes "Implementation-Title": project.name - attributes "Implementation-Version": rootProject.version + attributes "Implementation-Version": sdkVersion attributes "Implementation-Vendor": "Zowe org" attributes "Implementation-Vendor-Id": "org.zowe.apiml.sdk" } From 465d5de6d6d52131f7bfcbc68fae9241b7bac277 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 23 Oct 2025 13:04:00 +0000 Subject: [PATCH 148/152] [Gradle Release plugin] [skip ci] Before tag commit 'enablers-v3.3.17'. Signed-off-by: Gowtham Selvaraj --- enabler.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enabler.properties b/enabler.properties index d3a745bf0b..feda0cf8dc 100644 --- a/enabler.properties +++ b/enabler.properties @@ -1,2 +1,2 @@ -version=3.3.17-SNAPSHOT +version=3.3.17 From cf4b25d1d963d92f32b3e0be01c2d84d97fc0467 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Thu, 23 Oct 2025 13:04:01 +0000 Subject: [PATCH 149/152] [Gradle Release plugin] Create new version: 'enablers-v3.3.18-SNAPSHOT'. Signed-off-by: Gowtham Selvaraj --- enabler.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enabler.properties b/enabler.properties index feda0cf8dc..72afec631d 100644 --- a/enabler.properties +++ b/enabler.properties @@ -1,2 +1,2 @@ -version=3.3.17 +version=3.3.18-SNAPSHOT From d53574299d6fce2b8adf8d8d747412ecfd404226 Mon Sep 17 00:00:00 2001 From: Zowe Robot Date: Fri, 24 Oct 2025 00:41:27 +0000 Subject: [PATCH 150/152] [Gradle Release plugin] [skip ci] Before tag commit 'v3.3.16'. Signed-off-by: Gowtham Selvaraj --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d58b2d2255..b8d2b4cec2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.16-SNAPSHOT +version=3.3.16 nodejsVersion=14.16.0 cleanNodeModules=false From 28d54323b989271213a515c6b642012e1faadddf Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:21:39 +0100 Subject: [PATCH 151/152] chore: env update in release (#4359) Signed-off-by: ac892247 Co-authored-by: Zowe Robot Signed-off-by: Gowtham Selvaraj --- .github/workflows/automated-release.yml | 16 ++++--- api-catalog-ui/frontend/.env | 2 +- apiml-package/src/main/resources/bin/start.sh | 16 +++---- enabler.properties | 2 +- gradle.properties | 2 +- gradle/publish.gradle | 43 ++++++++++++++++--- integration-tests/build.gradle | 3 +- 7 files changed, 61 insertions(+), 23 deletions(-) diff --git a/.github/workflows/automated-release.yml b/.github/workflows/automated-release.yml index af3c731716..5bb3e25c70 100644 --- a/.github/workflows/automated-release.yml +++ b/.github/workflows/automated-release.yml @@ -52,11 +52,16 @@ jobs: shell: bash run: | ./gradlew release $RELEASE_ARGS -Prelease.useAutomaticVersion=true -Prelease.scope=${{ github.event.inputs.scope || env.DEFAULT_SCOPE }} -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD - released_version=$(cat gradle.properties | grep "version=" | sed "s/version=//g") - sed -i "/REACT_APP_ZOWE_BUILD_INFO=/c\REACT_APP_ZOWE_BUILD_INFO=${released_version}" api-catalog-ui/frontend/.env - git add api-catalog-ui/frontend/.env - git commit -m "[skip ci] Update version" - git push + if [ -n "$RELEASE_ARGS" ]; then + echo "RELEASE_ARGS is empty. Skipping api catalog .env update." + else + released_version=$(cat gradle.properties | grep "version=" | sed "s/version=//g") + sed -i "/REACT_APP_ZOWE_BUILD_INFO=/c\REACT_APP_ZOWE_BUILD_INFO=${released_version}" api-catalog-ui/frontend/.env + git add api-catalog-ui/frontend/.env + git commit -m "[skip ci] Update version" + git push + fi + env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} @@ -64,7 +69,6 @@ jobs: BUILD_NUMBER: ${{ github.run_number }} BRANCH_NAME: ${{ github.ref_name }} RELEASE_ARGS: ${{ github.event.inputs.enablers == 'true' && '-PreleaseEnablers' || '' }} - - name: Store test results uses: actions/upload-artifact@v4 if: failure() diff --git a/api-catalog-ui/frontend/.env b/api-catalog-ui/frontend/.env index 9f60d79c78..c50719c591 100644 --- a/api-catalog-ui/frontend/.env +++ b/api-catalog-ui/frontend/.env @@ -8,5 +8,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=10 REACT_APP_STATUS_UPDATE_DEBOUNCE=300 REACT_APP_CA_ENV=false REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000 -REACT_APP_ZOWE_BUILD_INFO=3.3.16-SNAPSHOT +REACT_APP_ZOWE_BUILD_INFO=3.3.21-SNAPSHOT diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 0ba5a93c80..0d241bcb2b 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -106,6 +106,14 @@ # - ZWE_configs_storage_vsam_name # Optional variables: +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + if [ -n "${LAUNCH_COMPONENT}" ]; then JAR_FILE="${LAUNCH_COMPONENT}/apiml-lite.jar" else @@ -180,14 +188,6 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" fi -add_profile() { - new_profile=$1 - if [ -n "${ZWE_configs_spring_profiles_active}" ]; then - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," - fi - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" -} - ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" diff --git a/enabler.properties b/enabler.properties index 72afec631d..83cb28a0ca 100644 --- a/enabler.properties +++ b/enabler.properties @@ -1,2 +1,2 @@ -version=3.3.18-SNAPSHOT +version=3.3.19-SNAPSHOT diff --git a/gradle.properties b/gradle.properties index b8d2b4cec2..668deeb5a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ artifactoryPublishingMavenRepo=https://zowe.jfrog.io/zowe/libs-release-local artifactoryPublishingMavenSnapshotRepo=https://zowe.jfrog.io/zowe/libs-snapshot-local # Artifacts version -version=3.3.16 +version=3.3.21-SNAPSHOT nodejsVersion=14.16.0 cleanNodeModules=false diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 2bf95d1834..3621184014 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -36,8 +36,6 @@ ext.servicesToPublish = [ ext.zoweComponentsToPublish = ext.servicesToPublish + ext.javaLibraries -ext.sdksToPublish = ext.enablers + ext.javaLibraries - configure(subprojects.findAll { it.name in zoweComponentsToPublish }) { apply plugin: 'maven-publish' apply plugin: 'java' @@ -84,7 +82,7 @@ if (enablerPropsFile.exists()) { //3. Get the version, or use the rootProject.version as a fallback def sdkVersion = enablerProps.getProperty("version") -configure(subprojects.findAll { it.name in sdksToPublish }) { +configure(subprojects.findAll { it.name in enablers }) { apply plugin: 'maven-publish' apply plugin: 'java' @@ -116,7 +114,7 @@ configure(subprojects.findAll { it.name in sdksToPublish }) { } } -configure(subprojects.findAll { it.name in sdksToPublish }) { +configure(subprojects.findAll { it.name in enablers }) { publishing { publications { mavenJava(MavenPublication) { @@ -150,6 +148,41 @@ configure(subprojects.findAll { it.name in sdksToPublish }) { } } +} +configure(subprojects.findAll { it.name in javaLibraries }) { + publishing { + publications { + mavenJava(MavenPublication) { + groupId 'org.zowe.apiml.sdk' + version rootProject.version + artifactId "${project.name}" + + from components.java + + pom.withXml { + asNode().dependencies.'*'.findAll() { + it.scope.text() == 'runtime' && project.configurations.implementation.allDependencies.find { dep -> + dep.name == it.artifactId.text() + } + }.each { it.scope*.value = 'compile' } + } + } + } + } + + tasks.withType(Jar) { + manifest { + attributes "Specification-Title": project.name + attributes "Specification-Version": rootProject.version + attributes "Specification-Vendor": "Zowe org" + + attributes "Implementation-Title": project.name + attributes "Implementation-Version": rootProject.version + attributes "Implementation-Vendor": "Zowe org" + attributes "Implementation-Vendor-Id": "org.zowe.apiml.sdk" + } + } + } configure(subprojects.findAll { it.name in servicesToPublish }) { @@ -186,7 +219,7 @@ ext.publishZoweServerTasksList = zoweComponentsToPublish.collect { ext.publishTasksList = enablers.collect { ":" + it + ":publish" -} + ":platform:publish" +} //noinspection GroovyAssignabilityCheck tasks.register('publishZoweServerArtifacts') { diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 29f10bf7c3..4728afc488 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -291,7 +291,8 @@ task runAllIntegrationTestsForZoweModulithNonHaTestingOnZos(type: Test) { 'GatewayProxyTest', 'GatewayCentralRegistry', 'SafIdTokenTest', - 'ZaasTest' // These tests require ZAAS as a separate service with its own port and specific paths that are not part of the public API + 'ZaasTest', // These tests require ZAAS as a separate service with its own port and specific paths that are not part of the public API + 'NonModulithTest' ) } From a0ea3148cef7c423882360b365d7482f1839cec3 Mon Sep 17 00:00:00 2001 From: Gowtham Selvaraj Date: Wed, 29 Oct 2025 19:22:41 +0530 Subject: [PATCH 152/152] added the license Signed-off-by: Gowtham Selvaraj --- .../zowe/apiml/zaas/controllers/StsControllerTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java index 78240e824f..26440df5c5 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java @@ -1,3 +1,13 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + package org.zowe.apiml.zaas.controllers; import org.junit.jupiter.api.BeforeEach;