From b4653044c15607246c5b66ed9ee895c76667c0ac Mon Sep 17 00:00:00 2001 From: ac892247 Date: Tue, 16 Jun 2026 09:02:51 +0200 Subject: [PATCH 1/5] verify that encoded path is rejected by mock-services Signed-off-by: ac892247 --- .../providers/ZosmfLoginTest.java | 26 +++++++++++++++---- .../apiml/util/http/HttpRequestUtils.java | 21 +++++++++++++++ .../apiml/client/api/FilesController.java | 13 ++++++++-- .../client/services/apars/FunctionalApar.java | 13 +++++++++- 4 files changed, 65 insertions(+), 8 deletions(-) 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..33e63ee8fb 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; @@ -74,11 +75,26 @@ void givenValidCertificate_thenReturnExistingDatasets() { .get(uri) .then() .statusCode(is(SC_OK)) - .body( - "items.dsname", hasItems(dsname1, dsname2) - ) + .body("items.dsname", hasItems(dsname1, dsname2)) .onFailMessage("Accessing " + uri); } + + @Test + void givenValidCertificate_thenReturnExistingFile() { + URI uri = HttpRequestUtils.getRawUriFromGateway("/" + ZOSMF_SERVICE_ID + "/api/v1/zosmf/restfiles/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() + .statusCode(is(SC_BAD_REQUEST)) + .onFailMessage("Accessing " + uri); + } + } @Nested 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..8ee108ad99 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/**", 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..9265a5ace4 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 @@ -89,7 +89,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 +164,13 @@ 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) { + return null; + } + protected boolean noAuthentication(Map headers) { String basicAuth = getAuthorizationHeader(headers); String cookie = getAuthCookie(headers); From 31eb29da12f3320056ff5156c82a273c2709aef2 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Tue, 16 Jun 2026 10:47:57 +0200 Subject: [PATCH 2/5] test encoded characters in query Signed-off-by: ac892247 --- .../providers/ZosmfLoginTest.java | 34 ++++++++++++++----- .../client/services/apars/FunctionalApar.java | 5 ++- 2 files changed, 29 insertions(+), 10 deletions(-) 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 33e63ee8fb..13ceb4fb60 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 @@ -71,16 +71,16 @@ void givenValidCertificate_thenReturnExistingDatasets() { 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)) .onFailMessage("Accessing " + uri); } @Test - void givenValidCertificate_thenReturnExistingFile() { + void givenValidCertificate_whenPathContainsEncodedCharacters_thenReturnBadRequest() { URI uri = HttpRequestUtils.getRawUriFromGateway("/" + ZOSMF_SERVICE_ID + "/api/v1/zosmf/restfiles/fs%2Fc%2Fuser%2Ffile.txt"); RequestSpecification mySpec = new RequestSpecBuilder().setUrlEncodingEnabled(false).build(); given() @@ -88,13 +88,29 @@ void givenValidCertificate_thenReturnExistingFile() { .log().all() .spec(mySpec) .header("X-CSRF-ZOSMF-HEADER", "") - .when() + .when() .get(uri) - .then() + .then() .statusCode(is(SC_BAD_REQUEST)) .onFailMessage("Accessing " + uri); } + @Test + void givenValidCertificate_whenQueryParamsEncoded_thenReturnFile() { + URI uri = HttpRequestUtils.getRawUriFromGateway("/" + ZOSMF_SERVICE_ID + "/api/v1/zosmf/restfiles/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() + .statusCode(is(SC_OK)) + .onFailMessage("Accessing " + uri); + } + } @Nested @@ -108,9 +124,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) @@ -128,9 +144,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/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 9265a5ace4..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 { @@ -168,7 +169,9 @@ protected ResponseEntity handleFiles(Map headers) { * Override to provide a response entity when a specific file content is requested. */ protected ResponseEntity handleFileContent(Map headers) { - return null; + var body = new HashMap(); + body.put("file", "content"); + return new ResponseEntity<>(body, HttpStatus.OK); } protected boolean noAuthentication(Map headers) { From 4045a0d97cf6c2b101124137ff843249b0a75aa0 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Tue, 16 Jun 2026 13:25:33 +0200 Subject: [PATCH 3/5] log all Signed-off-by: ac892247 --- .../integration/authentication/providers/ZosmfLoginTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 13ceb4fb60..3a6530c330 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 @@ -91,6 +91,7 @@ void givenValidCertificate_whenPathContainsEncodedCharacters_thenReturnBadReques .when() .get(uri) .then() + .log().all() .statusCode(is(SC_BAD_REQUEST)) .onFailMessage("Accessing " + uri); } @@ -107,8 +108,8 @@ void givenValidCertificate_whenQueryParamsEncoded_thenReturnFile() { .when() .get(uri) .then() - .statusCode(is(SC_OK)) - .onFailMessage("Accessing " + uri); + .log().all() + .statusCode(is(SC_OK)); } } From 3f5ef2e1ef5eb6aeafe5664ec89d5850e303f574 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Tue, 16 Jun 2026 13:31:08 +0200 Subject: [PATCH 4/5] catch both paths Signed-off-by: ac892247 --- .../main/java/org/zowe/apiml/client/api/FilesController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ee108ad99..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 @@ -34,7 +34,7 @@ public ResponseEntity readDatasets( return files.process("files", "read", response, headers); } - @GetMapping(value = "/zosmf/restfiles/fs/**", produces = "application/json; charset=utf-8") + @GetMapping(value = {"/zosmf/restfiles/fs/**", "/zosmf/restfiles/fs"}, produces = "application/json; charset=utf-8") public ResponseEntity readFile( HttpServletResponse response, @RequestHeader Map headers From bffc1f6341290b27515a7d3de4656e9460db0490 Mon Sep 17 00:00:00 2001 From: ac892247 Date: Tue, 16 Jun 2026 14:53:14 +0200 Subject: [PATCH 5/5] reuse URL Signed-off-by: ac892247 --- .../authentication/providers/ZosmfLoginTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 3a6530c330..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 @@ -44,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 @@ -66,7 +66,7 @@ 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) @@ -81,7 +81,7 @@ void givenValidCertificate_thenReturnExistingDatasets() { @Test void givenValidCertificate_whenPathContainsEncodedCharacters_thenReturnBadRequest() { - URI uri = HttpRequestUtils.getRawUriFromGateway("/" + ZOSMF_SERVICE_ID + "/api/v1/zosmf/restfiles/fs%2Fc%2Fuser%2Ffile.txt"); + URI uri = HttpRequestUtils.getRawUriFromGateway(ZOSMF_ENDPOINT + "fs%2Fc%2Fuser%2Ffile.txt"); RequestSpecification mySpec = new RequestSpecBuilder().setUrlEncodingEnabled(false).build(); given() .config(SslContext.clientCertUser) @@ -98,7 +98,7 @@ void givenValidCertificate_whenPathContainsEncodedCharacters_thenReturnBadReques @Test void givenValidCertificate_whenQueryParamsEncoded_thenReturnFile() { - URI uri = HttpRequestUtils.getRawUriFromGateway("/" + ZOSMF_SERVICE_ID + "/api/v1/zosmf/restfiles/fs?path=c%2Fuser%2Ffile.txt"); + URI uri = HttpRequestUtils.getRawUriFromGateway(ZOSMF_ENDPOINT + "fs?path=c%2Fuser%2Ffile.txt"); RequestSpecification mySpec = new RequestSpecBuilder().setUrlEncodingEnabled(false).build(); given() .config(SslContext.clientCertUser)