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 index 2566253ab1..c8d2cbdb82 100644 --- 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 @@ -32,6 +32,8 @@ 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.*; @@ -144,22 +146,25 @@ private void generateExample(String serviceId, OpenAPI swagger, String method, O * @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.getOpenAPI(); - Paths paths = swagger.getPaths(); - - for (Map.Entry pathItemEntry : paths.entrySet()) { - for (Map.Entry operationEntry : pathItemEntry.getValue().readOperationsMap().entrySet()) { - generateExample(serviceId, swagger, operationEntry.getKey().name(), operationEntry.getValue(), pathItemEntry.getKey()); + 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). 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 index 1c525d1f13..ed911c0e2f 100644 --- 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 @@ -10,9 +10,13 @@ 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; @@ -20,6 +24,7 @@ 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; @@ -27,13 +32,13 @@ 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.doReturn; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.*; class ExampleServiceTest { @@ -67,10 +72,27 @@ void existingGetExample() throws JSONException { } @Test - void generateExampleDoesNotThrowException() { - assertDoesNotThrow( () -> exampleService.generateExamples("testService", " ")); + 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"); diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java b/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java index d1ade2e026..307b1aa7db 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/UrlUtils.java @@ -40,9 +40,8 @@ public String getEncodedUrl(String url) { if (url != null) { return url.replaceAll("\\W", "-"); } else { - SecureRandom random = new SecureRandom(); byte[] bytes = new byte[20]; - random.nextBytes(bytes); + new SecureRandom().nextBytes(bytes); return Arrays.toString(bytes); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/conformance/OpenApiV2Validator.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/conformance/OpenApiV2Validator.java index 47b5d39610..f7ed8c27e5 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/conformance/OpenApiV2Validator.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/conformance/OpenApiV2Validator.java @@ -55,7 +55,7 @@ private HashMap> getValidResponses(Path value) { HashMap> result = new HashMap<>(); for (HttpMethod httpMethod : getMethod(value)) { Map responseMap = value.getOperationMap().get(convertSpringHttpToSwagger(httpMethod)).getResponses(); - if ( !responseMap.isEmpty()) { + if (responseMap != null && !responseMap.isEmpty()) { result.put(httpMethod.name(), responseMap.keySet()); } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/CompoundAuthProvider.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/CompoundAuthProvider.java index 0068cea618..2596febaa5 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/CompoundAuthProvider.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/config/CompoundAuthProvider.java @@ -58,7 +58,10 @@ private void warnForDummyProvider(String defaultProviderName) { } private AuthenticationProvider getConfiguredLoginAuthProvider() { - String providerBeanName = loginProvider.getAuthProviderBeanName(); + String providerBeanName; + synchronized (this) { + providerBeanName = loginProvider.getAuthProviderBeanName(); + } AuthenticationProvider authenticationProvider = authProvidersMap.get(providerBeanName); if (authenticationProvider == null) { log.warn("Login provider {} is not available.", providerBeanName); 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 c9f5449362..9b5bf79b6b 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 @@ -149,7 +149,7 @@ public String getHash(String token) throws CachingServiceClientException { return getSecurePassword(token, getSalt()); } - private String initializeSalt() throws CachingServiceClientException { + private String initializeSalt() throws CachingServiceClientException,SecureTokenInitializationException { String localSalt; try { CachingServiceClient.KeyValue keyValue = cachingServiceClient.read("salt"); @@ -194,10 +194,13 @@ private void storeSalt(byte[] salt) throws CachingServiceClientException { } public static byte[] generateSalt() { - SecureRandom random = new SecureRandom(); byte[] salt = new byte[16]; - random.nextBytes(salt); - return salt; + try { + SecureRandom.getInstanceStrong().nextBytes(salt); + return salt; + } catch (NoSuchAlgorithmException e) { + throw new SecureTokenInitializationException(e); + } } public static String getSecurePassword(String password, byte[] salt) { diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/SecureTokenInitializationException.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/SecureTokenInitializationException.java new file mode 100644 index 0000000000..c392d030cf --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/SecureTokenInitializationException.java @@ -0,0 +1,18 @@ +/* + * 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; + +public class SecureTokenInitializationException extends RuntimeException { + + public SecureTokenInitializationException(Throwable t) { + super(t); + } +} diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProviderTest.java index 0ba781d62b..51da646481 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProviderTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/ApimlAccessTokenProviderTest.java @@ -19,12 +19,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; + +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.zowe.apiml.zaas.cache.CachingServiceClient; import org.zowe.apiml.zaas.cache.CachingServiceClientException; import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.models.AccessTokenContainer; import org.zowe.apiml.security.common.token.QueryResponse; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.*; import java.util.stream.Stream; @@ -47,7 +52,7 @@ class ApimlAccessTokenProviderTest { QueryResponse queryResponseWithoutScopes = new QueryResponse(null, "user", issued, new Date(), "issuer", Collections.emptyList(), QueryResponse.Source.ZOWE_PAT); @BeforeEach - void setup() throws CachingServiceClientException { + void setup() throws CachingServiceClientException,SecureTokenInitializationException { cachingServiceClient = mock(CachingServiceClient.class); as = mock(AuthenticationService.class); when(cachingServiceClient.read("salt")).thenReturn(new CachingServiceClient.KeyValue("salt", new String(ApimlAccessTokenProvider.generateSalt()))); @@ -121,6 +126,37 @@ void givenSaltNotAlreadyInCache_thenGenerateAndStoreNew() throws CachingServiceC assertNotNull(salt); } + @Test + void givenSaltIsInvalid_thenThrowException() throws RuntimeException { + + try (MockedStatic apimlAccessTokenProviderMock = Mockito.mockStatic(ApimlAccessTokenProvider.class)) { + apimlAccessTokenProviderMock.when(() -> ApimlAccessTokenProvider.generateSalt()).thenThrow(new SecureTokenInitializationException(new Throwable("cause"))); + assertThrows(SecureTokenInitializationException.class, () -> ApimlAccessTokenProvider.generateSalt()); + } + } + + @Test + void givenNominalCase_thenReturnSaltSuccessfully() throws CachingServiceClientException { + + try (MockedStatic mock = Mockito.mockStatic(ApimlAccessTokenProvider.class)) { + byte[] expectedSalt = new byte[24]; + mock.when(ApimlAccessTokenProvider::generateSalt).thenReturn(expectedSalt); + + byte[] actualSalt = ApimlAccessTokenProvider.generateSalt(); + assertNotNull(actualSalt); + assertEquals(expectedSalt.length, actualSalt.length); + } + } + @Test + void given_whenSecureRandomThrowsNoSuchAlgorithmException_thenThrowSecureTokenInitializationException() { + + try (MockedStatic mockedSecureRandom = Mockito.mockStatic(SecureRandom.class)) { + mockedSecureRandom.when(SecureRandom::getInstanceStrong).thenThrow(new NoSuchAlgorithmException()); + + assertThrows(SecureTokenInitializationException.class, () -> ApimlAccessTokenProvider.generateSalt()); + } + } + @Test void givenDifferentToken_returnNotInvalidated() throws Exception { String differentToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZG9tIjoiRHVtbXkgcHJvdmlkZXIiLCJpYXQiOjE2NTQ1MzAwMDUsImV4cCI6MTY1NDU1ODgwNSwiaXNzIjoiQVBJTUwiLCJqdGkiOiIwYTllNzAyMS1jYzY2LTQzMDMtYTc4YS0wZGQwMWM3MjYyZjkifQ.HNfmAzw_bsKVrft5a527LaF9zsBMkfZK5I95mRmdftmRtI9dQNEFQR4Eg10FiBP53asixz6vmereJGKV04uSZIJzAKOpRk-NlGrZ06UZ3cTCBaLmB1l2HYnrAGkWJ8gCaAAOxRN2Dy4LIa_2UrtT-87DfU1T0OblgUdqfgf1_WKw0JIl6uMjdsJrSKdP61GeacFuaGQGxxZBRR7r9D5mxdVLQaHAjzjK89ZqZuQP04jV1BR-0OnFNA84XsQdWG61dYbWDMDkjPcp-nFK65w5X6GLO0BKFHWn4vSIQMKLEb6A9j7ym9N7pAXdt-eXCdLRiHHGQDjYcNSh_zRHtXwwkdA"; @@ -152,7 +188,6 @@ void givenTokenWithUserIdMatchingRule_returnInvalidated() { when(cachingServiceClient.readAllMaps()).thenReturn(cacheMap); assertTrue(accessTokenProvider.isInvalidated(TOKEN_WITHOUT_SCOPES)); } - @Test void givenTokenWithScopeMatchingRule_returnInvalidated() { String serviceId = accessTokenProvider.getHash("service"); @@ -184,6 +219,13 @@ void givenScopedToken_whenScopeIsListed_thenReturnValid() { assertTrue(accessTokenProvider.isValidForScopes(SCOPED_TOKEN, "gateway")); } + @Test + void givenNoTimestamp_thenUserSystemTimeToInvalidateAllTokensForUser() { + String userId = "user"; + accessTokenProvider.invalidateAllTokensForUser(userId, 0); + verify(cachingServiceClient, times(1)).appendList(eq(ApimlAccessTokenProvider.INVALID_USERS_KEY), any()); + } + static Stream invalidScopes() { return Stream.of("invalidService", "", null); } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/SecureTokenInitializationExceptionTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/SecureTokenInitializationExceptionTest.java new file mode 100644 index 0000000000..2279890f34 --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/SecureTokenInitializationExceptionTest.java @@ -0,0 +1,28 @@ +/* + * 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 org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SecureTokenInitializationExceptionTest { + + @Nested + class GivenExceptionCause { + @Test + void thenReturnMessage() { + SecureTokenInitializationException exception = new SecureTokenInitializationException(new Throwable("cause")); + assertEquals("cause", exception.getCause().getMessage()); + } + } +}