diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/ZosmfLoginTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/ZosmfLoginTest.java index f93574eb6e..14be7a19ea 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/ZosmfLoginTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/providers/ZosmfLoginTest.java @@ -11,7 +11,9 @@ package org.zowe.apiml.integration.authentication.providers; import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; import io.restassured.http.Cookie; +import io.restassured.specification.RequestSpecification; import org.apache.http.message.BasicNameValuePair; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; @@ -30,8 +32,7 @@ import java.util.Optional; import static io.restassured.RestAssured.given; -import static org.apache.http.HttpStatus.SC_NO_CONTENT; -import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.*; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.core.Is.is; @@ -43,11 +44,11 @@ class ZosmfLoginTest implements TestWithStartedInstances { private final static boolean ZOS_TARGET = Boolean.parseBoolean(System.getProperty("environment.zos.target", "false")); + private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getClientUser(); private final static String ZOSMF_SERVICE_ID = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getServiceId(); private static final String ZOSMF_CONTEXT_ROOT = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getContextRoot(); - private final static String ZOSMF_ENDPOINT_GW = "/" + ZOSMF_SERVICE_ID + "/api/v1/" + (StringUtils.hasText(ZOSMF_CONTEXT_ROOT) ? ZOSMF_CONTEXT_ROOT + "/" : "") + "restfiles/ds"; - private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getClientUser(); - private final static String ZOSMF_ENDPOINT_MOCK = "/" + ZOSMF_SERVICE_ID + "/api/zosmf/restfiles/ds"; + private final static String ZOSMF_ENDPOINT_GW = "/" + ZOSMF_SERVICE_ID + "/api/v1/" + (StringUtils.hasText(ZOSMF_CONTEXT_ROOT) ? ZOSMF_CONTEXT_ROOT + "/" : "") + "restfiles/"; + private final static String ZOSMF_ENDPOINT_MOCK = "/" + ZOSMF_SERVICE_ID + "/api/zosmf/restfiles/"; private final static String ZOSMF_ENDPOINT = ZOS_TARGET ? ZOSMF_ENDPOINT_GW : ZOSMF_ENDPOINT_MOCK; @BeforeAll @@ -65,20 +66,52 @@ void givenValidCertificate_thenReturnExistingDatasets() { String dsname1 = "SYS1.PARMLIB"; String dsname2 = "SYS1.PROCLIB"; - URI uri = HttpRequestUtils.getUriFromGateway(ZOSMF_ENDPOINT, new BasicNameValuePair("dslevel", "sys1.p*")); + URI uri = HttpRequestUtils.getUriFromGateway(ZOSMF_ENDPOINT + "ds", new BasicNameValuePair("dslevel", "sys1.p*")); given() .config(SslContext.clientCertUser) .header("X-CSRF-ZOSMF-HEADER", "") - .when() + .when() .get(uri) - .then() + .then() .statusCode(is(SC_OK)) - .body( - "items.dsname", hasItems(dsname1, dsname2) - ) + .body("items.dsname", hasItems(dsname1, dsname2)) + .onFailMessage("Accessing " + uri); + } + + @Test + void givenValidCertificate_whenPathContainsEncodedCharacters_thenReturnBadRequest() { + URI uri = HttpRequestUtils.getRawUriFromGateway(ZOSMF_ENDPOINT + "fs%2Fc%2Fuser%2Ffile.txt"); + RequestSpecification mySpec = new RequestSpecBuilder().setUrlEncodingEnabled(false).build(); + given() + .config(SslContext.clientCertUser) + .log().all() + .spec(mySpec) + .header("X-CSRF-ZOSMF-HEADER", "") + .when() + .get(uri) + .then() + .log().all() + .statusCode(is(SC_BAD_REQUEST)) .onFailMessage("Accessing " + uri); } + + @Test + void givenValidCertificate_whenQueryParamsEncoded_thenReturnFile() { + URI uri = HttpRequestUtils.getRawUriFromGateway(ZOSMF_ENDPOINT + "fs?path=c%2Fuser%2Ffile.txt"); + RequestSpecification mySpec = new RequestSpecBuilder().setUrlEncodingEnabled(false).build(); + given() + .config(SslContext.clientCertUser) + .log().all() + .spec(mySpec) + .header("X-CSRF-ZOSMF-HEADER", "") + .when() + .get(uri) + .then() + .log().all() + .statusCode(is(SC_OK)); + } + } @Nested @@ -92,9 +125,9 @@ void givenClientX509Cert(URI loginUrl) { given() .config(SslContext.clientCertValid) .noContentType() - .when() + .when() .post(loginUrl) - .then() + .then() .statusCode(is(SC_NO_CONTENT)) .cookie(COOKIE_NAME, not(is(emptyString()))) .onFailMessage("Accessing " + loginUrl) @@ -112,9 +145,9 @@ void givenValidClientCertAndInvalidBasic(URI loginUrl) { .config(SslContext.clientCertValid) .auth().basic("Bob", "The Builder") .noContentType() - .when() + .when() .post(loginUrl) - .then() + .then() .statusCode(is(SC_NO_CONTENT)) .cookie(COOKIE_NAME, not(is(emptyString()))) .onFailMessage("Accessing " + loginUrl) 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 af9fa1567f..64a942b64d 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 @@ -123,6 +123,27 @@ public static URI getUriFromGateway(String endpoint, NameValuePair...arguments) return getUriFromService(ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(), endpoint, arguments); } + /** + * Build a gateway URI from a raw (already percent-encoded) path without re-encoding. + * Use this instead of {@link #getUriFromGateway} when the path contains encoded characters + * like {@code %2F} that must not be double-encoded. + */ + public static URI getRawUriFromGateway(String rawPath) { + var config = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + var host = config.getHost(); + var hostnameTokenizer = new StringTokenizer(host, ","); + host = hostnameTokenizer.nextToken(); + if (StringUtils.isNotBlank(config.getDvipaHost())) { + host = config.getDvipaHost(); + } + try { + return new URI(config.getScheme() + "://" + host + ":" + config.getPort() + rawPath); + } catch (URISyntaxException e) { + log.error("Can't create raw URI for path '{}'", rawPath); + return null; + } + } + public static URI getUriFromGateway(String endpoint, String gatewayHostname, NameValuePair...arguments) { return getUriFromService(ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(), endpoint, s -> gatewayHostname, arguments); } diff --git a/mock-services/src/main/java/org/zowe/apiml/client/api/FilesController.java b/mock-services/src/main/java/org/zowe/apiml/client/api/FilesController.java index 1eecdff976..d8f84013c0 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/api/FilesController.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/api/FilesController.java @@ -10,6 +10,7 @@ package org.zowe.apiml.client.api; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,7 +18,6 @@ import org.springframework.web.bind.annotation.RestController; import org.zowe.apiml.client.services.AparBasedService; -import jakarta.servlet.http.HttpServletResponse; import java.util.Map; @RestController @@ -27,11 +27,20 @@ public class FilesController { private final AparBasedService files; @GetMapping(value = "/zosmf/restfiles/ds", produces = "application/json; charset=utf-8") - public ResponseEntity readFiles( + public ResponseEntity readDatasets( HttpServletResponse response, @RequestHeader Map headers ) { return files.process("files", "read", response, headers); } + + @GetMapping(value = {"/zosmf/restfiles/fs/**", "/zosmf/restfiles/fs"}, produces = "application/json; charset=utf-8") + public ResponseEntity readFile( + HttpServletResponse response, + @RequestHeader Map headers + ) { + return files.process("files", "readFile", response, headers); + } + } 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 0cfa8fbd3a..fda0f91d52 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 @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.HashMap; @SuppressWarnings({"squid:S1452", "squid:S1172"}) public class FunctionalApar implements Apar { @@ -89,7 +90,11 @@ public Optional> apply(Object... parameters) { } if (calledService.equals("files")) { - result = handleFiles(headers); + if ("readFile".equals(calledMethod)) { + result = handleFileContent(headers); + } else { + result = handleFiles(headers); + } } if (calledService.equals("jwtKeys")) { @@ -160,6 +165,15 @@ protected ResponseEntity handleFiles(Map headers) { return null; } + /** + * Override to provide a response entity when a specific file content is requested. + */ + protected ResponseEntity handleFileContent(Map headers) { + var body = new HashMap(); + body.put("file", "content"); + return new ResponseEntity<>(body, HttpStatus.OK); + } + protected boolean noAuthentication(Map headers) { String basicAuth = getAuthorizationHeader(headers); String cookie = getAuthCookie(headers);