From 76ab2d77f9ef41997d80833ceab7cb30b2b594da Mon Sep 17 00:00:00 2001 From: nxhafa Date: Thu, 2 Apr 2026 16:15:26 +0200 Subject: [PATCH 01/29] add log signal attributes for invalid authentication; invalid authentication during login Signed-off-by: nxhafa --- .../zowe/apiml/filter/BasicLoginFilter.java | 5 ++- .../FailedAuthenticationWebHandler.java | 7 +++- ...penTelemetryResourceAttributesZosTest.java | 41 ++++++++++--------- .../opentelemetry/OtelRequestContext.java | 19 ++++++++- .../opentelemetry/OtelRequestContextTest.java | 17 +++++++- 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java b/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java index 82ba534c12..ad9470a1fc 100644 --- a/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java +++ b/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java @@ -25,6 +25,7 @@ import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.zowe.apiml.handler.FailedAuthenticationWebHandler; +import org.zowe.apiml.product.opentelemetry.OtelRequestContext; import org.zowe.apiml.security.common.login.LoginFilter; import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.zaas.security.config.CompoundAuthProvider; @@ -50,7 +51,7 @@ * * *

This filter is intended to be used on /login endpoints.

- * + *

* Caution: Filter will read the body and make it available as a request attribute * * @see LoginRequest @@ -72,6 +73,8 @@ public BasicLoginFilter(CompoundAuthProvider compoundAuthProvider, FailedAuthent @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { var hasBody = Optional.ofNullable(exchange.getAttribute(CachedBodyFilter.CACHED_BODY_ATTR)).isPresent(); + var otelContext = OtelRequestContext.of(exchange); + otelContext.authMethod(OtelRequestContext.BASIC_AUTH_TYPE); exchange.getAttributes().put(X509AuthFilter.SKIP_X509_AUTH_ATTR, hasBody); return extractBasicAuth(exchange) .map(this::useCredentials) diff --git a/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java b/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java index 2e20cd68b3..28ab96a943 100644 --- a/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java +++ b/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java @@ -26,6 +26,7 @@ import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.product.opentelemetry.OtelRequestContext; import org.zowe.apiml.security.common.error.AuthExceptionHandler; import reactor.core.publisher.Mono; @@ -48,11 +49,15 @@ public class FailedAuthenticationWebHandler implements ServerAuthenticationFailu public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) { var exchange = webFilterExchange.getExchange(); var requestUri = exchange.getRequest().getURI().getPath(); + var otelContext = OtelRequestContext.of(exchange); log.debug("Unauthorized access to '{}' endpoint", requestUri); + otelContext.authenticationFailed(); + otelContext.authErrorMessage(exception.getMessage()); var bufferFactory = new DefaultDataBufferFactory(); AtomicReference buffer = new AtomicReference<>(); BiConsumer consumer = (message, status) -> { exchange.getResponse().setStatusCode(status); + otelContext.authErrorType(status.getReasonPhrase()); if (message != null) { exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); try { @@ -64,7 +69,7 @@ public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, A buffer.set(bufferFactory.wrap(new byte[0])); } }; - var addHeader = (BiConsumer)(name, value) -> exchange.getResponse().getHeaders().add(name, value); + var addHeader = (BiConsumer) (name, value) -> exchange.getResponse().getHeaders().add(name, value); try { handler.handleException(requestUri, consumer, addHeader, exception); } catch (ServletException e) { diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index f2f67eff38..8881f4ff62 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -69,7 +69,7 @@ private boolean assertAttributesBase(Attributes attributes, int port) { @Nested @AcceptanceTest - @ActiveProfiles({ "OpenTelemetryTest", "zos" }) + @ActiveProfiles({"OpenTelemetryTest", "zos"}) @TestPropertySource( properties = { "otel.sdk.disabled=false", @@ -118,7 +118,7 @@ void thenLogCustomAttributes() { "apiml.security.filterChainConfiguration=new" } ) - @ActiveProfiles({ "OpenTelemetryTest", "zos" }) + @ActiveProfiles({"OpenTelemetryTest", "zos"}) class WhenOnboardedService extends AcceptanceTestWithMockServices { private static final String VALID_OIDC_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; @@ -217,7 +217,7 @@ void givenRouted_whenAuthFail_thenLog() { given() .get(basePath + "/testservice/api/v1/200") .then() - .statusCode(200); + .statusCode(200); var logRecord = assertOneLogRecordExported(); @@ -226,7 +226,7 @@ void givenRouted_whenAuthFail_thenLog() { var logBody = logRecord.getBodyValue().asString(); assertEquals("testservice", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("FAILED", getAttribute(logBody, "auth.status")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); assertEquals("200", getAttribute(logBody, "service.response_code")); assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); @@ -246,27 +246,28 @@ private Object getAttribute(String logBody, String attributeName) { } @Test - @Disabled("This test is for invalid authentication (server error). To be reviewed in follow up story") void givenLoginEndpoint_thenLog() { given() .auth().preemptive() .basic("wronguser", "wrongpass") .post(basePath + "/gateway/api/v1/auth/login") .then() - .statusCode(500); + .statusCode(401); var logRecord = assertOneLogRecordExported(); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); - assertEquals("apicatalog", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("FAILED", getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("gateway", getAttribute(logBody, "service.id")); + assertEquals("POST", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("EACCES: Permission is denied; the specified password is incorrect", getAttribute(logBody, "auth.error.message")); + assertEquals("Unauthorized", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:gateway:" + port, getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/gateway/api/v1/auth/login", getAttribute(logBody, "url.path")); assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.method")); + assertEquals("basicAuth", getAttribute(logBody, "auth.service.auth.method")); } @Test @@ -320,7 +321,7 @@ void givenNoRoute_thenLog() { given() .get(basePath + "/nonexistant/api/v1/200") .then() - .statusCode(404); + .statusCode(404); var logRecord = assertOneLogRecordExported(); assertAttributesBase(logRecord.getResource().getAttributes(), port); @@ -400,7 +401,7 @@ void givenRouted_withMisconfiguredAuthPassTicket_thenLog() { assertNull(getAttribute(logBody, "user.id")); assertEquals("testservicepterror", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("FAILED", getAttribute(logBody, "auth.status")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); assertEquals(mockServicePassTicketMisconfigured.getInstanceId(), getAttribute(logBody, "service.instance.id")); assertEquals("200", getAttribute(logBody, "service.response_code")); assertEquals("/testservicepterror/api/v1/200", getAttribute(logBody, "url.path")); @@ -465,11 +466,11 @@ private String login() { var token = given() .contentType(ContentType.JSON) .body(""" - { - "username": "USER", - "password": "validPassword" - } - """) + { + "username": "USER", + "password": "validPassword" + } + """) .log().all() .when() .post(URI.create(basePath + LOGIN_ENDPOINT)) diff --git a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java index dc4e5e433b..1d70c42771 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java +++ b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java @@ -32,7 +32,8 @@ public final class OtelRequestContext { public static final String OTEL_CONTEXT = "otel-context"; private static final String OK = "OK"; - private static final String FAILED = "FAILED"; + private static final String ERROR = "ERROR"; + public static final String BASIC_AUTH_TYPE = "basicAuth"; private static final String OTEL_ATTRIBUTE_METHOD = "http.request.method"; private static final String OTEL_ATTRIBUTE_SCHEME = "url.scheme"; @@ -43,6 +44,8 @@ public final class OtelRequestContext { private static final String OTEL_ATTRIBUTE_AUTH_METHOD = "auth.service.auth.method"; private static final String OTEL_ATTRIBUTE_AUTH_SOURCE_TYPE = "auth.method"; private static final String OTEL_ATTRIBUTE_AUTH_STATUS = "auth.status"; + private static final String OTEL_ATTRIBUTE_AUTH_ERROR_TYPE = "auth.error.type"; + private static final String OTEL_ATTRIBUTE_AUTH_ERROR_MESSAGE = "auth.error.message"; private static final String OTEL_ATTRIBUTE_USER_ID = "user.id"; private static final String OTEL_ATTRIBUTE_DISTRIBUTED_USER_ID = "user.distributed.id"; @@ -97,8 +100,20 @@ public OtelRequestContext authMethod(AuthenticationScheme authenticationScheme) return put(OTEL_ATTRIBUTE_AUTH_METHOD, String.valueOf(authenticationScheme)); } + public OtelRequestContext authMethod(String authenticationScheme) { + return put(OTEL_ATTRIBUTE_AUTH_METHOD, authenticationScheme); + } + public OtelRequestContext authenticationFailed() { - return put(OTEL_ATTRIBUTE_AUTH_STATUS, FAILED); + return put(OTEL_ATTRIBUTE_AUTH_STATUS, ERROR); + } + + public OtelRequestContext authErrorType(String authErrorType) { + return put(OTEL_ATTRIBUTE_AUTH_ERROR_TYPE, authErrorType); + } + + public OtelRequestContext authErrorMessage(String authErrorMessage) { + return put(OTEL_ATTRIBUTE_AUTH_ERROR_MESSAGE, authErrorMessage); } public OtelRequestContext authenticationSuccess() { diff --git a/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java b/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java index 922b156005..e16f595652 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java @@ -113,7 +113,19 @@ void givenOtelContext_whenSetZoweJwtAuthMethod_thenTransformToString() { @Test void givenOtelContext_whenAuthenticationFailed_thenStoreFailedStringAsStatus() { OtelRequestContext.of(exchange).authenticationFailed(); - assertEquals("FAILED", getValue("auth.status")); + assertEquals("ERROR", getValue("auth.status")); + } + + @Test + void givenOtelContext_whenAuthErrorMessage_thenStoreMessageAsAuthErrorMessage() { + OtelRequestContext.of(exchange).authErrorMessage("Invalid credentials"); + assertEquals("Invalid credentials", getValue("auth.error.message")); + } + + @Test + void givenOtelContext_whenAuthErrorType_thenStoreErrorTypeAsAuthErrorType() { + OtelRequestContext.of(exchange).authErrorType("Forbidden"); + assertEquals("Forbidden", getValue("auth.error.type")); } @Test @@ -154,7 +166,8 @@ void givenInvalidData_whenObjectMapperFails_thenThrowIllegalStateException() thr exchange = MockServerWebExchange.from(request); var objectMapper = mock(ObjectMapper.class); var otelRequestContext = spy(OtelRequestContext.of(exchange)); - var jsonProcessingException = new JsonProcessingException("test") {}; + var jsonProcessingException = new JsonProcessingException("test") { + }; doReturn(objectMapper).when(otelRequestContext).getObjectMapper(); doThrow(jsonProcessingException).when(objectMapper).writeValueAsString(any()); From 28510ab578335ac1ba45d5ce75e046ee310b41dc Mon Sep 17 00:00:00 2001 From: nxhafa Date: Thu, 2 Apr 2026 17:57:57 +0200 Subject: [PATCH 02/29] add otel logs on routing failures Signed-off-by: nxhafa --- .../filters/AbstractAuthSchemeFactory.java | 94 +++++++++---------- .../AbstractTokenFilterFactoryTest.java | 15 ++- ...ngConfigurationErrorFilterFactoryTest.java | 28 +++++- 3 files changed, 81 insertions(+), 56 deletions(-) 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 7cc474a8b8..528a3980e6 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 @@ -20,6 +20,7 @@ import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; @@ -60,43 +61,38 @@ * Example: * class MyScheme extends AbstractAuthSchemeFactory { * - * @param Class of config class. It should extend {@link AbstractAuthSchemeFactory.AbstractConfig} - * @param Class of expended response from the ZAAS - * @Override public GatewayFilter apply(Config config) { - * try { - * return createGatewayFilter(config); - * } catch (Exception e) { - * return ((exchange, chain) -> { - * ServerHttpRequest request = updateHeadersForError(exchange, e.getMessage()); - * return chain.filter(exchange.mutate().request(request).build()); - * }); - * } - * } - * - * @Override - * protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials(ServerWebExchange exchange, Config config) { - * return super.createRequestCredentials(exchange, config) - * .applId(config.getApplicationName()); - * } - * - * @Override protected Mono processResponse(ServerWebExchange exchange, GatewayFilterChain chain, MyResponse response) { - * ServerHttpRequest request; - * if (response.getToken() != null) { - * request = exchange.getRequest().mutate().headers(headers -> - * headers.add("mySchemeHeader", response.getToken()) - * ).build(); - * } else { - * request = updateHeadersForError(exchange, "Invalid or missing authentication"); - * } - * exchange = exchange.mutate().request(request).build(); - * return chain.filter(exchange); - * } - * - * @EqualsAndHashCode(callSuper = true) - * public static class Config extends AbstractAuthSchemeFactory.AbstractConfig { - * } + * @param Class of config class. It should extend {@link AbstractAuthSchemeFactory.AbstractConfig} + * @param Class of expended response from the ZAAS + * @Override public GatewayFilter apply(Config config) { + * try { + * return createGatewayFilter(config); + * } catch (Exception e) { + * return ((exchange, chain) -> { + * ServerHttpRequest request = updateHeadersForError(exchange, e.getMessage()); + * return chain.filter(exchange.mutate().request(request).build()); + * }); + * } + * } + * @Override protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials(ServerWebExchange exchange, Config config) { + * return super.createRequestCredentials(exchange, config) + * .applId(config.getApplicationName()); + * } + * @Override protected Mono processResponse(ServerWebExchange exchange, GatewayFilterChain chain, MyResponse response) { + * ServerHttpRequest request; + * if (response.getToken() != null) { + * request = exchange.getRequest().mutate().headers(headers -> + * headers.add("mySchemeHeader", response.getToken()) + * ).build(); + * } else { + * request = updateHeadersForError(exchange, "Invalid or missing authentication"); + * } + * exchange = exchange.mutate().request(request).build(); + * return chain.filter(exchange); + * } + * @EqualsAndHashCode(callSuper = true) + * public static class Config extends AbstractAuthSchemeFactory.AbstractConfig { + * } * } - * * @Data class MyResponse { * private String token; * } @@ -112,27 +108,27 @@ public abstract class AbstractAuthSchemeFactory CERTIFICATE_HEADERS_TEST = headerName -> StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[0]) || - StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[1]) || - StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[2]); + StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[1]) || + StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[2]); private static final Predicate CREDENTIALS_COOKIE_INPUT = cookie -> StringUtils.equalsIgnoreCase(cookie.getName(), PAT_COOKIE_AUTH_NAME) || - StringUtils.equalsIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME) || - StringUtils.startsWithIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME + "."); + StringUtils.equalsIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME) || + StringUtils.startsWithIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME + "."); private static final Predicate CREDENTIALS_COOKIE = cookie -> CREDENTIALS_COOKIE_INPUT.test(cookie) || - StringUtils.equalsIgnoreCase(cookie.getName(), "jwtToken") || - StringUtils.equalsIgnoreCase(cookie.getName(), "LtpaToken2"); + StringUtils.equalsIgnoreCase(cookie.getName(), "jwtToken") || + StringUtils.equalsIgnoreCase(cookie.getName(), "LtpaToken2"); private static final Predicate CREDENTIALS_HEADER_INPUT = headerName -> StringUtils.equalsIgnoreCase(headerName, HttpHeaders.AUTHORIZATION) || - StringUtils.equalsIgnoreCase(headerName, PAT_HEADER_NAME); + StringUtils.equalsIgnoreCase(headerName, PAT_HEADER_NAME); private static final Predicate CREDENTIALS_HEADER = headerName -> CREDENTIALS_HEADER_INPUT.test(headerName) || - CERTIFICATE_HEADERS_TEST.test(headerName) || - StringUtils.equalsIgnoreCase(headerName, "X-SAF-Token") || - StringUtils.equalsIgnoreCase(headerName, CLIENT_CERT_HEADER) || - StringUtils.equalsIgnoreCase(headerName, HttpHeaders.COOKIE); + CERTIFICATE_HEADERS_TEST.test(headerName) || + StringUtils.equalsIgnoreCase(headerName, "X-SAF-Token") || + StringUtils.equalsIgnoreCase(headerName, CLIENT_CERT_HEADER) || + StringUtils.equalsIgnoreCase(headerName, HttpHeaders.COOKIE); protected final InstanceInfoService instanceInfoService; protected final MessageService messageService; @@ -209,6 +205,10 @@ protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials( protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, String errorMessage) { var otelContext = OtelRequestContext.of(exchange); otelContext.authenticationFailed(); + otelContext.authErrorMessage(errorMessage); + Optional.ofNullable(exchange.getResponse().getStatusCode()) + .flatMap(httpStatusCode -> Optional.ofNullable(HttpStatus.resolve(httpStatusCode.value()))) + .ifPresent(httpStatus -> otelContext.authErrorType(httpStatus.getReasonPhrase())); Optional.ofNullable(getAuthenticationScheme()).ifPresent(otelContext::authMethod); return exchange.getRequest().mutate().headers(headers -> { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java index 0631263bd9..fb152e9cda 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java @@ -18,6 +18,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; @@ -73,7 +74,7 @@ class ValidResponse { @Test void givenHeaderResponse_whenHandling_thenUpdateTheRequest() { - var request = testRequestMutation(new AbstractAuthSchemeFactory.AuthorizationResponse<>(null,ZaasTokenResponse.builder() + var request = testRequestMutation(new AbstractAuthSchemeFactory.AuthorizationResponse<>(null, ZaasTokenResponse.builder() .headerName("headerName") .token("headerValue") .build() @@ -83,13 +84,13 @@ void givenHeaderResponse_whenHandling_thenUpdateTheRequest() { @Test void givenCookieResponse_whenHandling_thenUpdateTheRequest() { - var request = testRequestMutation(new AbstractAuthSchemeFactory.AuthorizationResponse<>(null,ZaasTokenResponse.builder() + var request = testRequestMutation(new AbstractAuthSchemeFactory.AuthorizationResponse<>(null, ZaasTokenResponse.builder() .cookieName("cookieName") .token("cookieValue") .build() )); assertEquals("cookieName=cookieValue", request.getHeaders().getFirst("cookie")); - assertEquals("Bearer cookieValue" , request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + assertEquals("Bearer cookieValue", request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); } } @@ -126,7 +127,7 @@ void givenEmptyResponseWithError_whenHandling_thenProvideErrorHeader() { @Test void givenCookieAndHeaderInResponse_whenHandling_thenSetBoth() { - var request = testRequestMutation(new AbstractAuthSchemeFactory.AuthorizationResponse<>(null,ZaasTokenResponse.builder() + var request = testRequestMutation(new AbstractAuthSchemeFactory.AuthorizationResponse<>(null, ZaasTokenResponse.builder() .cookieName("cookie") .headerName("header") .token("jwt") @@ -144,20 +145,24 @@ void givenCookieAndHeaderInResponse_whenHandling_thenSetBoth() { class Otel { MockServerHttpRequest request = MockServerHttpRequest.get("/aPath").build(); - MockServerWebExchange exchange = MockServerWebExchange.from(request); + MockServerWebExchange exchange; OtelRequestContext otelRequestContext; @BeforeEach void mockOtelContext() { + exchange = MockServerWebExchange.from(request); otelRequestContext = spy(OtelRequestContext.of(exchange)); exchange.getAttributes().put("otel-context", otelRequestContext); } @Test void givenOtelRequestContext_whenFail_thenCallAuthenticationFailed() { + exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); spy(AbstractAuthSchemeFactory.class).cleanHeadersOnAuthFail(exchange, "test"); verify(otelRequestContext, times(1)).authenticationFailed(); + verify(otelRequestContext, times(1)).authErrorMessage("test"); + verify(otelRequestContext, times(1)).authErrorType(HttpStatus.FORBIDDEN.getReasonPhrase()); } @Test diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java index db29456a3a..0f0a1f7cfc 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java @@ -10,10 +10,11 @@ package org.zowe.apiml.gateway.filters; -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.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.zowe.apiml.auth.AuthenticationScheme; @@ -33,10 +34,11 @@ class RoutingConfigurationErrorFilterFactoryTest { private GatewayFilter filter; private MockServerHttpRequest request = MockServerHttpRequest.get("https://localhost/some/url").build(); - private MockServerWebExchange exchange = MockServerWebExchange.from(request); + private MockServerWebExchange exchange; - @BeforeAll + @BeforeEach void init() { + exchange = MockServerWebExchange.from(request); var config = new RoutingConfigurationErrorFilterFactory.Config(); config.setMessage(MESSAGE); config.setAuthenticationScheme("safIdt"); @@ -47,13 +49,31 @@ void init() { } @Test - void givenConfig_whenApply_thenSetAuthInformation() { + void givenConfig_whenApply_thenSetAuthInformationWithoutErrorType() { var otelContext = spy(OtelRequestContext.of(exchange)); exchange.getAttributes().put(OtelRequestContext.OTEL_CONTEXT, otelContext); StepVerifier.create(filter.filter(exchange, e -> Mono.empty())).verifyComplete(); verify(otelContext).authenticationFailed(); + verify(otelContext).authErrorMessage(MESSAGE); + + verify(otelContext).authMethod(AuthenticationScheme.SAF_IDT); + verify(underTest).cleanHeadersOnAuthFail(exchange, MESSAGE); + } + + @Test + void givenConfig_whenApply_thenSetFailedAuthInformationWithErrorType() { + var otelContext = spy(OtelRequestContext.of(exchange)); + exchange.getAttributes().put(OtelRequestContext.OTEL_CONTEXT, otelContext); + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + + StepVerifier.create(filter.filter(exchange, e -> Mono.empty())).verifyComplete(); + + verify(otelContext).authenticationFailed(); + verify(otelContext).authErrorMessage(MESSAGE); + verify(otelContext).authErrorType(HttpStatus.UNAUTHORIZED.getReasonPhrase()); + verify(otelContext).authMethod(AuthenticationScheme.SAF_IDT); verify(underTest).cleanHeadersOnAuthFail(exchange, MESSAGE); } From 22804b6932a3cdd1d87858c2968dc86c7ea1fa38 Mon Sep 17 00:00:00 2001 From: nxhafa Date: Thu, 2 Apr 2026 18:00:23 +0200 Subject: [PATCH 03/29] revert javadoc alignment Signed-off-by: nxhafa --- .../filters/AbstractAuthSchemeFactory.java | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) 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 528a3980e6..6e561624ce 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 @@ -61,37 +61,41 @@ * Example: * class MyScheme extends AbstractAuthSchemeFactory { * - * @param Class of config class. It should extend {@link AbstractAuthSchemeFactory.AbstractConfig} - * @param Class of expended response from the ZAAS - * @Override public GatewayFilter apply(Config config) { - * try { - * return createGatewayFilter(config); - * } catch (Exception e) { - * return ((exchange, chain) -> { - * ServerHttpRequest request = updateHeadersForError(exchange, e.getMessage()); - * return chain.filter(exchange.mutate().request(request).build()); - * }); - * } - * } - * @Override protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials(ServerWebExchange exchange, Config config) { - * return super.createRequestCredentials(exchange, config) - * .applId(config.getApplicationName()); - * } - * @Override protected Mono processResponse(ServerWebExchange exchange, GatewayFilterChain chain, MyResponse response) { - * ServerHttpRequest request; - * if (response.getToken() != null) { - * request = exchange.getRequest().mutate().headers(headers -> - * headers.add("mySchemeHeader", response.getToken()) - * ).build(); - * } else { - * request = updateHeadersForError(exchange, "Invalid or missing authentication"); - * } - * exchange = exchange.mutate().request(request).build(); - * return chain.filter(exchange); - * } - * @EqualsAndHashCode(callSuper = true) - * public static class Config extends AbstractAuthSchemeFactory.AbstractConfig { - * } + * @param Class of config class. It should extend {@link AbstractAuthSchemeFactory.AbstractConfig} + * @param Class of expended response from the ZAAS + * @Override public GatewayFilter apply(Config config) { + * try { + * return createGatewayFilter(config); + * } catch (Exception e) { + * return ((exchange, chain) -> { + * ServerHttpRequest request = updateHeadersForError(exchange, e.getMessage()); + * return chain.filter(exchange.mutate().request(request).build()); + * }); + * } + * } + * + * @Override + * protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials(ServerWebExchange exchange, Config config) { + * return super.createRequestCredentials(exchange, config) + * .applId(config.getApplicationName()); + * } + * + * @Override protected Mono processResponse(ServerWebExchange exchange, GatewayFilterChain chain, MyResponse response) { + * ServerHttpRequest request; + * if (response.getToken() != null) { + * request = exchange.getRequest().mutate().headers(headers -> + * headers.add("mySchemeHeader", response.getToken()) + * ).build(); + * } else { + * request = updateHeadersForError(exchange, "Invalid or missing authentication"); + * } + * exchange = exchange.mutate().request(request).build(); + * return chain.filter(exchange); + * } + * + * @EqualsAndHashCode(callSuper = true) + * public static class Config extends AbstractAuthSchemeFactory.AbstractConfig { + * } * } * @Data class MyResponse { * private String token; From b91e7fbad877ef05711f2f21d7f23b1adf9b6bfc Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 15 Apr 2026 14:22:59 +0200 Subject: [PATCH 04/29] placeholder tests Signed-off-by: Pablo Carle --- .../AcceptanceTestWithMockServices.java | 10 ++++ ...penTelemetryResourceAttributesZosTest.java | 59 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) 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 ae04bcd43b..99de7c47b2 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTestWithMockServices.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTestWithMockServices.java @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.zowe.apiml.gateway.ApplicationRegistry; import org.zowe.apiml.gateway.MockService; +import org.zowe.apiml.gateway.MockWebSocketService; @AcceptanceTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -78,6 +79,15 @@ protected MockService.MockServiceBuilder mockService(String serviceId) { .serviceId(serviceId); } + protected MockWebSocketService.MockWsServiceBuilder mockServiceWs(String serviceId) { + return MockWebSocketService.wsBuilder() + .statusChangedListener(mockService -> { + applicationRegistry.update(mockService); + updateRoutingRules(); + }) + .serviceId(serviceId); + } + @AfterEach void stopMocksWithTestScope() { applicationRegistry.afterTest(); diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 8881f4ff62..ab613e3dfa 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -34,6 +34,7 @@ import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.zaas.security.mapping.OIDCExternalMapper; +import org.zowe.apiml.zaas.security.mapping.X509NativeMapper; import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import java.net.URI; @@ -132,10 +133,14 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @MockitoBean private OIDCTokenProvider oidcTokenProvider; + @MockitoBean + private X509NativeMapper x509TokenProvider; + private MockService mockServiceZoweJwt; private MockService mockServicePassTicket; private MockService mockServicePassTicketMisconfigured; private MockService mockServiceBypass; + private MockService mockServiceWs; @BeforeAll void startMockServices() throws Exception { @@ -170,6 +175,10 @@ void startMockServices() throws Exception { .addEndpoint("/testservicebp/200") .responseCode(200) .and().start(); + + mockServiceWs = mockServiceWs("testservicews") + .scope(Scope.CLASS) + .start(); } @AfterAll @@ -293,7 +302,7 @@ void givenCatalogEndpoint_thenLog() { } @Test - void givenRouted_withAuthSuccess_thenLog() { + void givenRouted_withAuthJwt_success_thenLog() { given() .cookie("apimlAuthenticationToken", login()) .get(basePath + "/testservice/api/v1/200") @@ -316,6 +325,10 @@ void givenRouted_withAuthSuccess_thenLog() { assertEquals("JWT", getAttribute(logBody, "auth.method")); } + @Test + void givenRouted_withAuthJwt_failure_thenLog() { + } + @Test void givenNoRoute_thenLog() { given() @@ -411,7 +424,7 @@ void givenRouted_withMisconfiguredAuthPassTicket_thenLog() { } @Test - void givenRouted_withOidc_thenLog() { + void givenRouted_withOidc_success_thenLog() { when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(true); when(oidcExternalMapper.mapToMainframeUserId(any())).thenReturn("USER"); @@ -439,7 +452,18 @@ void givenRouted_withOidc_thenLog() { } @Test - void givenRouted_withX509_thenLog() { + void givenRouted_withOidc_failure_thenLog() { + when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(false); + + given() + .header(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_OIDC_TOKEN) + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + } + + @Test + void givenRouted_withX509_success_thenLog() { given() .config(SslContext.clientCertUser) .get(basePath + "/testservice/api/v1/200") @@ -462,6 +486,35 @@ void givenRouted_withX509_thenLog() { assertEquals("CLIENT_CERT", getAttribute(logBody, "auth.method")); } + @Test + void givenRouter_withX509_failure_thenLog() { + given() + .config(SslContext.tlsWithoutCert) + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + + when(x509TokenProvider.isValid(any())).thenReturn(false); + } + + @Test + void givenRouter_withWs_success_thenLog() { + given() + .get(basePath + "/testservicews/api/v1/200") + .then() + .statusCode(200); + } + + @Test + void givenRouter_withPAT_success_thenLog() { + + } + + @Test + void givenRouter_withPAT_failure_thenLog() { + + } + private String login() { var token = given() .contentType(ContentType.JSON) From f1cd8e94d5a535eb242136e59faa6f092cead3d5 Mon Sep 17 00:00:00 2001 From: nxhafa Date: Fri, 17 Apr 2026 14:38:09 +0200 Subject: [PATCH 05/29] handling failed routing with jwt authentication Signed-off-by: nxhafa --- .../security/common/util/JWTTestUtils.java | 21 +++++ apiml/build.gradle | 1 + .../zowe/apiml/ZaasSchemeTransformApi.java | 66 +++++++++------ ...penTelemetryResourceAttributesZosTest.java | 82 +++++++++++++++++-- .../org/zowe/apiml/ticket/TicketResponse.java | 2 + .../zowe/apiml/zaas/ZaasTokenResponse.java | 2 + .../filters/AbstractAuthSchemeFactory.java | 6 +- .../filters/AbstractTokenFilterFactory.java | 5 +- 8 files changed, 148 insertions(+), 37 deletions(-) diff --git a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java index c30fca1605..d5f7767023 100644 --- a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java +++ b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java @@ -34,6 +34,10 @@ public static String createZoweJwtToken(String username, String domain, String l return createToken(username, domain, ltpaToken, config, "APIML"); } + public static String createExpiredZoweJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { + return createExpiredToken(username, domain, ltpaToken, config, "APIML"); + } + public static String createZosmfJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { return createToken(username, domain, ltpaToken, config, "zOSMF"); } @@ -55,6 +59,23 @@ public static String createToken(String username, String domain, String ltpaToke .compact(); } + public static String createExpiredToken(String username, String domain, String ltpaToken, HttpsConfig config, String issuer) { + long now = System.currentTimeMillis(); + long expiration = now - 200_000L; + Key jwtSecret = SecurityUtils.loadKey(config); + + return Jwts.builder() + .subject(username) + .claim("dom", domain) + .claim("ltpa", ltpaToken) + .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(); diff --git a/apiml/build.gradle b/apiml/build.gradle index 5924887586..8612a14db4 100644 --- a/apiml/build.gradle +++ b/apiml/build.gradle @@ -77,6 +77,7 @@ dependencies { implementation libs.opentelemetry.spring.boot.autoconfigure testImplementation(testFixtures(project(":apiml-common"))) + testImplementation(testFixtures(project(":apiml-security-common"))) testImplementation(testFixtures(project(":gateway-service"))) testImplementation libs.spring.boot.starter.test testImplementation libs.spring.mock.mvc diff --git a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java index 0824974131..bfcc38aa9c 100644 --- a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java +++ b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java @@ -10,6 +10,8 @@ package org.zowe.apiml; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ExpiredJWTException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpUpgradeHandler; @@ -19,6 +21,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.ClientResponse; import org.zowe.apiml.constants.ApimlConstants; @@ -39,6 +42,7 @@ import java.io.ByteArrayInputStream; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.text.ParseException; import java.util.*; import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; @@ -59,7 +63,6 @@ *

  • z/OSMF token exchange
  • *
  • Zowe JWT generation
  • * - *

    * *

    * This bean is only active when {@code modulithConfig} is present in the Spring context. @@ -86,41 +89,44 @@ public class ZaasSchemeTransformApi implements ZaasSchemeTransform { @Value("${apiml.service.apimlId:apiml}") private String currentApimlId; - private Mono> createErrorMessage(String errorMessage) { - var headers = new ErrorHeaders(errorMessage); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(headers, null)); + private ErrorHeaders createErrorMessage(String errorMessage) { + return new ErrorHeaders(errorMessage); } - private Mono> createInvalidAuthenticationErrorMessage() { + private ErrorHeaders createInvalidAuthenticationErrorMessage() { String messageKey = "org.zowe.apiml.common.unauthorized"; String logMessage = messageService.createMessage(messageKey).mapToLogMessage(); - var headers = new ErrorHeaders(logMessage); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(headers, null)); + return new ErrorHeaders(logMessage); } - private Mono> createMissingAuthenticationErrorMessage() { + private AbstractAuthSchemeFactory.AuthorizationResponse createMissingAuthenticationErrorMessage() { String messageKey = "org.zowe.apiml.zaas.security.schema.missingAuthentication"; String logMessage = messageService.createMessage(messageKey).mapToLogMessage(); - var headers = new ErrorHeaders(logMessage); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(headers, null)); + return new AbstractAuthSchemeFactory.AuthorizationResponse<>(createErrorMessage(logMessage), InsufficientAuthenticationException.class.getName()); + } + + private Mono> createAuthorizationResponse(ErrorHeaders headers, R response) { + return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(headers, response)); } @Override public Mono> passticket(RequestCredentials requestCredentials) { var applicationName = requestCredentials.getApplId(); if (StringUtils.isBlank(applicationName)) { - return createErrorMessage("ApplicationName not provided."); + // TODO update errorType when passticket ApplId is missing + return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), TicketResponse.builder().errorType("").build()); } try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); + var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), TicketResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), TicketResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -143,7 +149,7 @@ public Mono> pas return Mono.error(new ZaasInternalErrorException(currentApimlId, e.getMessage())); } catch (Exception e) { log.debug("Token has expired", e); - return createInvalidAuthenticationErrorMessage(); + return createAuthorizationResponse(createInvalidAuthenticationErrorMessage(), TicketResponse.builder().errorType(e.getClass().getName()).build()); } } @@ -159,18 +165,20 @@ private void updateServiceId(Optional authSource, RequestCredentials public Mono> safIdt(RequestCredentials requestCredentials) { var applicationName = requestCredentials.getApplId(); if (StringUtils.isBlank(applicationName)) { - return createErrorMessage("ApplicationName not provided."); + // TODO update errorType when ApplId is missing + return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), ZaasTokenResponse.builder().errorType("").build()); } try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); + var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -189,7 +197,7 @@ public Mono> return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot generate SAF IDT", e); - return createErrorMessage(e.getMessage()); + return createAuthorizationResponse(createErrorMessage(e.getMessage()), ZaasTokenResponse.builder().errorType(e.getClass().getName()).build()); } } @@ -198,12 +206,13 @@ public Mono> try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); + var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -218,26 +227,27 @@ public Mono> return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot obtain z/OSMF token", e); - return createErrorMessage(e.getMessage()); + return createAuthorizationResponse(createErrorMessage(e.getMessage()), ZaasTokenResponse.builder().errorType(e.getClass().getName()).build()); } } @Override public Mono> zoweJwt(RequestCredentials requestCredentials) { + var zaasTokenResponseBuilder = ZaasTokenResponse.builder(); try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); + var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), zaasTokenResponseBuilder.errorType(missingAuthenticationErrorResponse.getBody()).build()); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), zaasTokenResponseBuilder.errorType(missingAuthenticationErrorResponse.getBody()).build()); } var authSourceParsed = authSourceService.parse(authSource.get()); var token = authSourceService.getJWT(authSource.get()); - var response = ZaasTokenResponse.builder() - .cookieName(COOKIE_AUTH_NAME) + var response = zaasTokenResponseBuilder.cookieName(COOKIE_AUTH_NAME) .token(token) .userId(authSourceParsed.getUserId()) .distributedIds(authSource.filter(OIDCAuthSource.class::isInstance) @@ -250,7 +260,11 @@ public Mono> return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot obtain Zowe JWT token", e); - return createInvalidAuthenticationErrorMessage(); + if (e.getCause() instanceof BadJWTException || e.getCause() instanceof ParseException || e.getCause() instanceof ExpiredJWTException) { + zaasTokenResponseBuilder.authSourceType(AuthSource.AuthSourceType.JWT.name()); + } + return createAuthorizationResponse(createInvalidAuthenticationErrorMessage(), zaasTokenResponseBuilder.errorType(e.getClass().getName()).build()); + } } diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index ab613e3dfa..170cedfded 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -31,6 +31,7 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.MockService; import org.zowe.apiml.gateway.MockService.Scope; +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.security.mapping.OIDCExternalMapper; @@ -39,9 +40,7 @@ import java.net.URI; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.restassured.RestAssured.given; @@ -49,6 +48,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.zowe.apiml.security.common.util.JWTTestUtils.createExpiredZoweJwtToken; class OpenTelemetryResourceAttributesZosTest { @@ -127,6 +127,9 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @Autowired private LogRecordExporter logExporter; + @Autowired + private HttpConfig httpConfig; + @MockitoBean private OIDCExternalMapper oidcExternalMapper; @@ -152,6 +155,9 @@ void startMockServices() throws Exception { .authenticationScheme(AuthenticationScheme.ZOWE_JWT) .addEndpoint("/testservice/200") .responseCode(200) + .and() + .addEndpoint("/testservice/401") + .responseCode(401) .and().start(); mockServicePassTicket = mockService("testservicept") @@ -326,7 +332,71 @@ void givenRouted_withAuthJwt_success_thenLog() { } @Test - void givenRouted_withAuthJwt_failure_thenLog() { + void givenRouted_withAuthJwt_failure_invalidToken_thenLog() { + given() + .cookie("apimlAuthenticationToken", "invalid.jwt.token") + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported(); + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); + assertEquals("org.zowe.apiml.security.common.token.TokenNotValidException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); + } + + @Test + void givenRouted_withAuthJwt_failure_expiredToken_thenLog() { + given() + .cookie("apimlAuthenticationToken", createExpiredZoweJwtToken("USER", "z/OS", "Ltpa", httpConfig.getHttpsConfig())) + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported(); + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); + assertEquals("org.zowe.apiml.security.common.token.TokenExpireException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); + } + + @Test + void givenRouted_withAuthJwt_noJwt_thenLog() { + given() + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported(); + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); + assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertNull(getAttribute(logBody, "auth.method")); } @Test @@ -486,7 +556,7 @@ void givenRouted_withX509_success_thenLog() { assertEquals("CLIENT_CERT", getAttribute(logBody, "auth.method")); } - @Test +/* @Test void givenRouter_withX509_failure_thenLog() { given() .config(SslContext.tlsWithoutCert) @@ -495,7 +565,7 @@ void givenRouter_withX509_failure_thenLog() { .statusCode(200); when(x509TokenProvider.isValid(any())).thenReturn(false); - } + }*/ @Test void givenRouter_withWs_success_thenLog() { diff --git a/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java b/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java index 62c2fb172d..62a16e7500 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java +++ b/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java @@ -35,5 +35,7 @@ public class TicketResponse { private List distributedIds; @JsonIgnore // to avoid a breaking change, this value is needed only in Otel via API call, not rest private String authSourceType; + @JsonIgnore // to avoid a breaking change, this value is needed only in Otel via API call, not rest + private String errorType; } diff --git a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java index 47faa6f550..db0fc0963c 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java +++ b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java @@ -33,5 +33,7 @@ public class ZaasTokenResponse { private List distributedIds; @JsonIgnore // to avoid a breaking change, this value is needed only in Otel via API call, not rest private String authSourceType; + @JsonIgnore + private String errorType; } 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 6e561624ce..7609b2c8d6 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 @@ -20,7 +20,6 @@ import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; @@ -119,6 +118,7 @@ public abstract class AbstractAuthSchemeFactory CREDENTIALS_COOKIE = cookie -> CREDENTIALS_COOKIE_INPUT.test(cookie) || StringUtils.equalsIgnoreCase(cookie.getName(), "jwtToken") || @@ -127,6 +127,7 @@ public abstract class AbstractAuthSchemeFactory CREDENTIALS_HEADER_INPUT = headerName -> StringUtils.equalsIgnoreCase(headerName, HttpHeaders.AUTHORIZATION) || StringUtils.equalsIgnoreCase(headerName, PAT_HEADER_NAME); + private static final Predicate CREDENTIALS_HEADER = headerName -> CREDENTIALS_HEADER_INPUT.test(headerName) || CERTIFICATE_HEADERS_TEST.test(headerName) || @@ -210,9 +211,6 @@ protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, S var otelContext = OtelRequestContext.of(exchange); otelContext.authenticationFailed(); otelContext.authErrorMessage(errorMessage); - Optional.ofNullable(exchange.getResponse().getStatusCode()) - .flatMap(httpStatusCode -> Optional.ofNullable(HttpStatus.resolve(httpStatusCode.value()))) - .ifPresent(httpStatus -> otelContext.authErrorType(httpStatus.getReasonPhrase())); Optional.ofNullable(getAuthenticationScheme()).ifPresent(otelContext::authMethod); return exchange.getRequest().mutate().headers(headers -> { 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 954d6359ac..e5c64428c5 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 @@ -75,6 +75,7 @@ protected Mono processResponse(ServerWebExchange exchange, GatewayFilterCh Optional.ofNullable(responseBody).map(ZaasTokenResponse::getUserId).ifPresent(otelContext::userId); Optional.ofNullable(responseBody).map(ZaasTokenResponse::getDistributedIds).ifPresent(otelContext::distributedIds); Optional.ofNullable(responseBody).map(ZaasTokenResponse::getAuthSourceType).ifPresent(otelContext::authSourceType); + Optional.ofNullable(responseBody).map(ZaasTokenResponse::getErrorType).ifPresent(otelContext::authErrorType); if (!StringUtils.isEmpty(response.get().getCookieName())) { request = cleanHeadersOnAuthSuccess(exchange); @@ -103,7 +104,9 @@ protected Mono processResponse(ServerWebExchange exchange, GatewayFilterCh request = request.mutate().headers(httpHeaders -> httpHeaders.add(ApimlConstants.AUTH_FAIL_HEADER, failureHeader.get())).build(); exchange = exchange.mutate().request(request).build(); } - exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, failureHeader.get()); + if (exchange.getResponse() != null) { + exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, failureHeader.get()); + } } } if (request == null) { From 24b61ff4edb56c41b96046f81f66e776061cabf8 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 17 Apr 2026 15:18:40 +0200 Subject: [PATCH 06/29] test organization wip Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index ab613e3dfa..3203675282 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -221,6 +221,35 @@ private LogRecordData assertOneLogRecordExported() { return logRecord; } + @Nested + class WhenServiceRequiresJwt { + + @Nested + class WhenAuthPresent { + } + + @Nested + class WhenAuthAbsent { + } + } + + @Nested + class WhenServiceRequiresPassTicket { + } + + @Nested + class WhenServiceRequiresX509 { + } + + @Nested + class WhenServiceRequiresOidc { + } + + @Nested + class WhenServiceRequiresSafIdt { + } + + @Test void givenRouted_whenAuthFail_thenLog() { given() @@ -494,7 +523,7 @@ void givenRouter_withX509_failure_thenLog() { .then() .statusCode(200); - when(x509TokenProvider.isValid(any())).thenReturn(false); + // when(x509TokenProvider.isValid(any())).thenReturn(false); } @Test From a642ed80a84f0ac6d82b96f38ecd0fbc6c3c4b00 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 20 Apr 2026 12:49:59 +0200 Subject: [PATCH 07/29] wip improve tests Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 183 ++++++++++++------ .../ApiCatalogAuthenticationTest.java | 29 ++- 2 files changed, 148 insertions(+), 64 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index c142dedc1a..7d9747a3df 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -140,7 +140,6 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { private X509NativeMapper x509TokenProvider; private MockService mockServiceZoweJwt; - private MockService mockServicePassTicket; private MockService mockServicePassTicketMisconfigured; private MockService mockServiceBypass; private MockService mockServiceWs; @@ -160,14 +159,6 @@ void startMockServices() throws Exception { .responseCode(401) .and().start(); - mockServicePassTicket = mockService("testservicept") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) - .applid("TSTSVRPT") - .addEndpoint("/testservicept/200") - .responseCode(200) - .and().start(); - mockServicePassTicketMisconfigured = mockService("testservicepterror") .scope(Scope.CLASS) .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) @@ -227,6 +218,11 @@ private LogRecordData assertOneLogRecordExported() { return logRecord; } + @Nested + class WhenServiceDoesNotExist { + + } + @Nested class WhenServiceRequiresJwt { @@ -241,18 +237,122 @@ class WhenAuthAbsent { @Nested class WhenServiceRequiresPassTicket { + + private MockService mockServicePassTicket; + + @BeforeEach + void setUp() { + mockServicePassTicket = mockService("testservicept") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) + .applid("TSTSVRPT") + .addEndpoint("/testservicept/200") + .responseCode(200) + .and().start(); + } + + @Nested + class WhenAuthPresent { + + @Test + void givenRouted_withAuthPassTicketSucess_thenLog() { + given() + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicept/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported(); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("USER", getAttribute(logBody, "user.id")); + assertEquals("testservicept", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("OK", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservicept:" + mockServicePassTicket.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicept/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("httpBasicPassTicket", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); + } + + } + + @Nested + class WhenAuthAbsent { + + + + } + } @Nested class WhenServiceRequiresX509 { + + @Nested + class WhenAuthPresent { + + @Test + void givenRouted_withX509_success_thenLog() { + given() + .config(SslContext.clientCertUser) + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported(); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("USER", getAttribute(logBody, "user.id")); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("OK", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("CLIENT_CERT", getAttribute(logBody, "auth.method")); + } + + } + + @Nested + class WhenAuthAbsent { + } + } @Nested class WhenServiceRequiresOidc { + + @Nested + class WhenAuthPresent { + + } + + @Nested + class WhenAuthAbsent { + } + } @Nested class WhenServiceRequiresSafIdt { + + @Nested + class WhenAuthPresent { + + } + + @Nested + class WhenAuthAbsent { + } + } @@ -450,30 +550,6 @@ void givenNoRoute_thenLog() { assertNull(getAttribute(logBody, "auth.method")); } - @Test - void givenRouted_withAuthPassTicketSucess_thenLog() { - given() - .cookie(AUTH_COOKIE, login()) - .get(basePath + "/testservicept/api/v1/200") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported(); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertEquals("USER", getAttribute(logBody, "user.id")); - assertEquals("testservicept", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("OK", getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservicept:" + mockServicePassTicket.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservicept/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("httpBasicPassTicket", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("JWT", getAttribute(logBody, "auth.method")); - } - @Test void givenRouted_withBypass_thenLog() { given() @@ -561,29 +637,28 @@ void givenRouted_withOidc_failure_thenLog() { .statusCode(200); } - @Test - void givenRouted_withX509_success_thenLog() { - given() - .config(SslContext.clientCertUser) - .get(basePath + "/testservice/api/v1/200") + private String login() { + var token = given() + .contentType(ContentType.JSON) + .body(""" + { + "username": "USER", + "password": "validPassword" + } + """) + .log().all() + .when() + .post(URI.create(basePath + LOGIN_ENDPOINT)) .then() - .statusCode(200); + .statusCode(204) + .cookie(AUTH_COOKIE) + .extract() + .cookie(AUTH_COOKIE); - var logRecord = assertOneLogRecordExported(); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertEquals("USER", getAttribute(logBody, "user.id")); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("OK", getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("CLIENT_CERT", getAttribute(logBody, "auth.method")); + setUp(); // clean up log emitted from the login + return token; } + } /* @Test void givenRouter_withX509_failure_thenLog() { @@ -637,5 +712,5 @@ private String login() { } } - +*/ } 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 a64d30af38..4924c41102 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 @@ -16,7 +16,6 @@ import io.restassured.response.Validatable; import io.restassured.specification.RequestSpecification; import org.apache.commons.lang3.StringUtils; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -35,7 +34,6 @@ import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.service.DiscoveryUtils; - import java.util.LinkedList; import java.util.stream.Stream; @@ -145,6 +143,7 @@ void givenValidBasicAuthentication(String endpoint, Request request) { ) .then() .log().all() + .onFailMessage("On Gateway URL: " + endpoint) .statusCode(is(SC_OK)); } @@ -159,7 +158,8 @@ void givenValidBearerAuthentication(String endpoint, Request request) { endpoint ) .then() - .statusCode(is(SC_OK)); + .statusCode(is(SC_OK)) + .onFailMessage("On Gateway URL: " + endpoint); } @ParameterizedTest(name = "givenValidBasicAuthenticationAndCertificate {index} {0} ") @@ -174,7 +174,8 @@ void givenValidBasicAuthenticationAndCertificate(String endpoint, Request reques ) .then() .log().all() - .statusCode(is(SC_OK)); + .statusCode(is(SC_OK)) + .onFailMessage("On Gateway URL: " + endpoint); } } @@ -197,7 +198,8 @@ void givenNoAuthentication(String endpoint, Request request) { .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_AUTHENTICATION_PREFIX) .body( "messages.find { it.messageNumber == '" + UNAUTHENTICATED_ERROR_NUMBER + "' }.messageContent", equalTo(expectedMessage) - ); + ) + .onFailMessage("On Gateway URL: " + endpoint); } @ParameterizedTest(name = "givenInvalidBasicAuthentication {index} {0}") @@ -214,6 +216,7 @@ void givenInvalidBasicAuthentication(String endpoint, Request request) { ) .then() .statusCode(is(SC_UNAUTHORIZED)) + .onFailMessage("On Gateway URL: " + endpoint) .body( "messages.find { it.messageNumber == '" + UNAUTHENTICATED_ERROR_NUMBER + "' }.messageContent", equalTo(expectedMessage) ); @@ -232,6 +235,7 @@ void givenInvalidBearerAuthentication(String endpoint, Request request) { ) .then() .log().ifValidationFails() + .onFailMessage("On Gateway URL: " + endpoint) .body( "messages.find { it.messageNumber == 'ZWEAO402E' }.messageContent", equalTo(expectedMessage) ).statusCode(is(SC_UNAUTHORIZED)); @@ -253,7 +257,8 @@ void givenInvalidTokenInCookie(String endpoint, Request request) { .statusCode(is(SC_UNAUTHORIZED)) .body( "messages.find { it.messageNumber == 'ZWEAO402E' }.messageContent", equalTo(expectedMessage) - ); + ) + .onFailMessage("On Gateway URL: " + endpoint); } } } @@ -277,7 +282,8 @@ void givenValidCertificate(String endpoint, Request request) { ) .then() .log().all() - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.OK.value()) + .onFailMessage("On Gateway URL: " + endpoint);; } @ParameterizedTest(name = "givenValidCertificateAndBasicAuth {index} {0} ") @@ -291,7 +297,8 @@ void givenValidCertificateAndBasicAuth(String endpoint, Request request) { endpoint ) .then() - .statusCode(is(SC_OK)); + .statusCode(is(SC_OK)) + .onFailMessage("On Gateway URL: " + endpoint);; } } @@ -307,7 +314,8 @@ void givenUnTrustedCertificateAndNoBasicAuth_thenReturnUnauthorized(String endpo endpoint ) .then() - .statusCode(HttpStatus.UNAUTHORIZED.value()); + .statusCode(HttpStatus.UNAUTHORIZED.value()) + .onFailMessage("On Gateway URL: " + endpoint); } @ParameterizedTest(name = "givenNoCertificateAndNoBasicAuth_thenReturnUnauthorized {index} {0} ") @@ -319,7 +327,8 @@ void givenNoCertificateAndNoBasicAuth_thenReturnUnauthorized(String endpoint, Re endpoint ) .then() - .statusCode(HttpStatus.UNAUTHORIZED.value()); + .statusCode(HttpStatus.UNAUTHORIZED.value()) + .onFailMessage("On Gateway URL: " + endpoint); } } } From f43ebbece59c64b3ff2bd0e0f756a31620e1cdcd Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 21 Apr 2026 14:27:42 +0200 Subject: [PATCH 08/29] wip reorg Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 335 ++++++++++-------- 1 file changed, 179 insertions(+), 156 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 7d9747a3df..fdce41f33c 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -139,7 +139,6 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @MockitoBean private X509NativeMapper x509TokenProvider; - private MockService mockServiceZoweJwt; private MockService mockServicePassTicketMisconfigured; private MockService mockServiceBypass; private MockService mockServiceWs; @@ -149,16 +148,6 @@ void startMockServices() throws Exception { SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); SslContext.prepareSslAuthentication(configurer); - mockServiceZoweJwt = mockService("testservice") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.ZOWE_JWT) - .addEndpoint("/testservice/200") - .responseCode(200) - .and() - .addEndpoint("/testservice/401") - .responseCode(401) - .and().start(); - mockServicePassTicketMisconfigured = mockService("testservicepterror") .scope(Scope.CLASS) .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) @@ -218,20 +207,199 @@ private LogRecordData assertOneLogRecordExported() { return logRecord; } + // Requests that target API ML (/login, /query, /logout, /services, etc.) + @Nested + class WhenRequestToAPIML { + + } + @Nested class WhenServiceDoesNotExist { + + } @Nested class WhenServiceRequiresJwt { + private MockService mockServiceZoweJwt; + + @BeforeEach + void setUp() { + mockServiceZoweJwt = mockService("testservice") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.ZOWE_JWT) + .addEndpoint("/testservice/200") + .responseCode(200) + .and() + .addEndpoint("/testservice/401") + .responseCode(401) + .and().start(); + } + + @Nested class WhenAuthPresent { + + @Nested + class WhenAuthSuccess { + + @Test + void givenRouted_withX509_success_thenLog() { + given() + .config(SslContext.clientCertUser) + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported(); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("USER", getAttribute(logBody, "user.id")); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("OK", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("CLIENT_CERT", getAttribute(logBody, "auth.method")); + } + + @Test + void givenRouted_withOidc_success_thenLog() { + when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(true); + when(oidcExternalMapper.mapToMainframeUserId(any())).thenReturn("USER"); + + given() + .header(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_OIDC_TOKEN) + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported(); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("USER", getAttribute(logBody, "user.id")); + assertEquals(List.of("oidc.username"), getAttribute(logBody, "user.distributed.id")); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("OK", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("OIDC", getAttribute(logBody, "auth.method")); + } + + } + + @Nested + class WhenAuthFailure { + + @Test + void givenRouted_whenAuthFail_thenLog() { + given() + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported(); + + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertNull(getAttribute(logBody, "auth.method")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + } + + @Test + void givenRouted_withAuthJwt_failure_invalidToken_thenLog() { + given() + .cookie("apimlAuthenticationToken", "invalid.jwt.token") + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported(); + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); + assertEquals("org.zowe.apiml.security.common.token.TokenNotValidException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); + } + + @Test + void givenRouted_withAuthJwt_failure_expiredToken_thenLog() { + given() + .cookie("apimlAuthenticationToken", createExpiredZoweJwtToken("USER", "z/OS", "Ltpa", httpConfig.getHttpsConfig())) + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported(); + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); + assertEquals("org.zowe.apiml.security.common.token.TokenExpireException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); + } + + @Test + void givenRouted_withAuthJwt_noJwt_thenLog() { + given() + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported(); + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); + assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertNull(getAttribute(logBody, "auth.method")); + } + + } + } @Nested class WhenAuthAbsent { + } } @@ -283,8 +451,6 @@ void givenRouted_withAuthPassTicketSucess_thenLog() { @Nested class WhenAuthAbsent { - - } } @@ -295,29 +461,6 @@ class WhenServiceRequiresX509 { @Nested class WhenAuthPresent { - @Test - void givenRouted_withX509_success_thenLog() { - given() - .config(SslContext.clientCertUser) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported(); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertEquals("USER", getAttribute(logBody, "user.id")); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("OK", getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("CLIENT_CERT", getAttribute(logBody, "auth.method")); - } } @@ -355,30 +498,6 @@ class WhenAuthAbsent { } - - @Test - void givenRouted_whenAuthFail_thenLog() { - given() - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported(); - - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertNull(getAttribute(logBody, "auth.method")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - } - private Object getAttribute(String logBody, String attributeName) { var objectMapper = new ObjectMapper(); try { @@ -460,74 +579,6 @@ void givenRouted_withAuthJwt_success_thenLog() { assertEquals("JWT", getAttribute(logBody, "auth.method")); } - @Test - void givenRouted_withAuthJwt_failure_invalidToken_thenLog() { - given() - .cookie("apimlAuthenticationToken", "invalid.jwt.token") - .get(basePath + "/testservice/api/v1/401") - .then() - .statusCode(401); - - var logRecord = assertOneLogRecordExported(); - var logBody = logRecord.getBodyValue().asString(); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); - assertEquals("org.zowe.apiml.security.common.token.TokenNotValidException", getAttribute(logBody, "auth.error.type")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("401", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("JWT", getAttribute(logBody, "auth.method")); - } - - @Test - void givenRouted_withAuthJwt_failure_expiredToken_thenLog() { - given() - .cookie("apimlAuthenticationToken", createExpiredZoweJwtToken("USER", "z/OS", "Ltpa", httpConfig.getHttpsConfig())) - .get(basePath + "/testservice/api/v1/401") - .then() - .statusCode(401); - - var logRecord = assertOneLogRecordExported(); - var logBody = logRecord.getBodyValue().asString(); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); - assertEquals("org.zowe.apiml.security.common.token.TokenExpireException", getAttribute(logBody, "auth.error.type")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("401", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("JWT", getAttribute(logBody, "auth.method")); - } - - @Test - void givenRouted_withAuthJwt_noJwt_thenLog() { - given() - .get(basePath + "/testservice/api/v1/401") - .then() - .statusCode(401); - - var logRecord = assertOneLogRecordExported(); - var logBody = logRecord.getBodyValue().asString(); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); - assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("401", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertNull(getAttribute(logBody, "auth.method")); - } - @Test void givenNoRoute_thenLog() { given() @@ -598,34 +649,6 @@ void givenRouted_withMisconfiguredAuthPassTicket_thenLog() { assertNull(getAttribute(logBody, "auth.method")); } - @Test - void givenRouted_withOidc_success_thenLog() { - when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(true); - when(oidcExternalMapper.mapToMainframeUserId(any())).thenReturn("USER"); - - given() - .header(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_OIDC_TOKEN) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported(); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertEquals("USER", getAttribute(logBody, "user.id")); - assertEquals(List.of("oidc.username"), getAttribute(logBody, "user.distributed.id")); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("OK", getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("OIDC", getAttribute(logBody, "auth.method")); - } - @Test void givenRouted_withOidc_failure_thenLog() { when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(false); From 513f6bbfc1c1c4295e64b78f6b2861bd5f6d6540 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 21 Apr 2026 16:23:12 +0200 Subject: [PATCH 09/29] wip reorg Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 192 ++++++++---------- 1 file changed, 86 insertions(+), 106 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index fdce41f33c..e6073947c3 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -140,7 +140,6 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { private X509NativeMapper x509TokenProvider; private MockService mockServicePassTicketMisconfigured; - private MockService mockServiceBypass; private MockService mockServiceWs; @BeforeAll @@ -155,13 +154,6 @@ void startMockServices() throws Exception { .responseCode(200) .and().start(); - mockServiceBypass = mockService("testservicebp") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.BYPASS) - .addEndpoint("/testservicebp/200") - .responseCode(200) - .and().start(); - mockServiceWs = mockServiceWs("testservicews") .scope(Scope.CLASS) .start(); @@ -216,7 +208,68 @@ class WhenRequestToAPIML { @Nested class WhenServiceDoesNotExist { + @Test + void givenNoRoute_thenLog() { + given() + .get(basePath + "/nonexistant/api/v1/200") + .then() + .statusCode(404); + + var logRecord = assertOneLogRecordExported(); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertNull(getAttribute(logBody, "user.id")); + assertEquals("nonexistant", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertNull(getAttribute(logBody, "auth.status")); + assertNull(getAttribute(logBody, "service.instance.id")); + assertEquals("404", getAttribute(logBody, "service.response_code")); + assertEquals("/nonexistant/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertNull(getAttribute(logBody, "auth.method")); + } + + } + + @Nested + class WhenServiceBypass { + + private MockService mockServiceBypass; + @BeforeAll + void init() { + mockServiceBypass = mockService("testservicebp") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.BYPASS) + .addEndpoint("/testservicebp/200") + .responseCode(200) + .and().start(); + } + + @Test + void givenRouted_withBypass_thenLog() { + given() + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicebp/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported(); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertNull(getAttribute(logBody, "user.id")); + assertEquals("testservicebp", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertNull(getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservicebp:" + mockServiceBypass.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicebp/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertNull(getAttribute(logBody, "auth.method")); + assertEquals("bypass", getAttribute(logBody, "auth.service.auth.method")); + } } @@ -245,6 +298,30 @@ class WhenAuthPresent { @Nested class WhenAuthSuccess { + @Test + void givenRouted_withAuthJwt_success_thenLog() { + given() + .cookie("apimlAuthenticationToken", login()) + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported(); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("USER", getAttribute(logBody, "user.id")); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("OK", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); + } + @Test void givenRouted_withX509_success_thenLog() { given() @@ -408,7 +485,7 @@ class WhenServiceRequiresPassTicket { private MockService mockServicePassTicket; - @BeforeEach + @BeforeAll void setUp() { mockServicePassTicket = mockService("testservicept") .scope(Scope.CLASS) @@ -555,76 +632,6 @@ void givenCatalogEndpoint_thenLog() { assertNull(getAttribute(logBody, "auth.method")); } - @Test - void givenRouted_withAuthJwt_success_thenLog() { - given() - .cookie("apimlAuthenticationToken", login()) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported(); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertEquals("USER", getAttribute(logBody, "user.id")); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("OK", getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("JWT", getAttribute(logBody, "auth.method")); - } - - @Test - void givenNoRoute_thenLog() { - given() - .get(basePath + "/nonexistant/api/v1/200") - .then() - .statusCode(404); - - var logRecord = assertOneLogRecordExported(); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertNull(getAttribute(logBody, "user.id")); - assertEquals("nonexistant", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertNull(getAttribute(logBody, "auth.status")); - assertNull(getAttribute(logBody, "service.instance.id")); - assertEquals("404", getAttribute(logBody, "service.response_code")); - assertEquals("/nonexistant/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertNull(getAttribute(logBody, "auth.method")); - } - - @Test - void givenRouted_withBypass_thenLog() { - given() - .cookie(AUTH_COOKIE, login()) - .get(basePath + "/testservicebp/api/v1/200") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported(); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertNull(getAttribute(logBody, "user.id")); - assertEquals("testservicebp", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertNull(getAttribute(logBody, "auth.status")); - assertEquals("localhost:testservicebp:" + mockServiceBypass.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservicebp/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertNull(getAttribute(logBody, "auth.method")); - assertEquals("bypass", getAttribute(logBody, "auth.service.auth.method")); - } - @Test void givenRouted_withMisconfiguredAuthPassTicket_thenLog() { given() @@ -684,33 +691,6 @@ private String login() { } /* @Test - void givenRouter_withX509_failure_thenLog() { - given() - .config(SslContext.tlsWithoutCert) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); - - // when(x509TokenProvider.isValid(any())).thenReturn(false); - } - - @Test - void givenRouter_withWs_success_thenLog() { - given() - .get(basePath + "/testservicews/api/v1/200") - .then() - .statusCode(200); - } - - @Test - void givenRouter_withPAT_success_thenLog() { - - } - - @Test - void givenRouter_withPAT_failure_thenLog() { - - } private String login() { var token = given() From 997a94258a9784280f70910d1aca1d81d0dfb6bb Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 22 Apr 2026 18:21:01 +0200 Subject: [PATCH 10/29] fix compilation Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 2b19f1c791..7c8ff090b1 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -41,6 +41,7 @@ import java.net.URI; import java.time.Duration; import java.util.*; +import java.util.stream.Collectors; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.restassured.RestAssured.given; @@ -218,7 +219,7 @@ void givenNoRoute_thenLog() { .then() .statusCode(404); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/nonexistant/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); @@ -258,7 +259,7 @@ void givenRouted_withBypass_thenLog() { .then() .statusCode(200); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); @@ -309,7 +310,7 @@ void givenRouted_withAuthJwt_success_thenLog() { .then() .statusCode(200); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); @@ -333,7 +334,7 @@ void givenRouted_withX509_success_thenLog() { .then() .statusCode(200); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); @@ -360,7 +361,7 @@ void givenRouted_withOidc_success_thenLog() { .then() .statusCode(200); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); @@ -389,7 +390,7 @@ void givenRouted_whenAuthFail_thenLog() { .then() .statusCode(200); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") @@ -413,7 +414,7 @@ void givenRouted_withAuthJwt_failure_invalidToken_thenLog() { .then() .statusCode(401); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); var logBody = logRecord.getBodyValue().asString(); assertEquals("testservice", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); @@ -436,7 +437,7 @@ void givenRouted_withAuthJwt_failure_expiredToken_thenLog() { .then() .statusCode(401); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); var logBody = logRecord.getBodyValue().asString(); assertEquals("testservice", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); @@ -458,7 +459,7 @@ void givenRouted_withAuthJwt_noJwt_thenLog() { .then() .statusCode(401); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); var logBody = logRecord.getBodyValue().asString(); assertEquals("testservice", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); @@ -510,7 +511,7 @@ void givenRouted_withAuthPassTicketSucess_thenLog() { .then() .statusCode(200); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/testservicept/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); @@ -597,7 +598,7 @@ void givenLoginEndpoint_thenLog() { .then() .statusCode(401); - var logRecord = assertOneLogRecordExported(); + var logRecord = assertOneLogRecordExported("/gateway/api/v1/auth/login"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); From 1c22a2e45ff35bb2c0c3dde77cfe88303a19b087 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 23 Apr 2026 10:36:32 +0200 Subject: [PATCH 11/29] wip org Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 282 ++++++++---------- 1 file changed, 127 insertions(+), 155 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 7c8ff090b1..2dea9af707 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -19,11 +19,15 @@ import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import io.restassured.http.ContentType; import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterAll; +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.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; -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; @@ -40,13 +44,20 @@ import java.net.URI; import java.time.Duration; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.restassured.RestAssured.given; import static org.awaitility.Awaitility.await; -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.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.zowe.apiml.security.common.util.JWTTestUtils.createExpiredZoweJwtToken; @@ -80,7 +91,6 @@ private boolean assertAttributesBase(Attributes attributes, int port) { "otel.logs.exporter=none" } ) - @DirtiesContext class WhenBasicConfig { @Autowired @@ -121,6 +131,7 @@ void thenLogCustomAttributes() { } ) @ActiveProfiles({"OpenTelemetryTest", "zos"}) + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenOnboardedService extends AcceptanceTestWithMockServices { private static final String VALID_OIDC_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; @@ -140,14 +151,31 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @MockitoBean private X509NativeMapper x509TokenProvider; + private MockService mockServiceBypass; + private MockService mockServicePassTicket; private MockService mockServicePassTicketMisconfigured; - private MockService mockServiceWs; + private MockService mockServiceZoweJwt; @BeforeAll void startMockServices() throws Exception { SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); SslContext.prepareSslAuthentication(configurer); + mockServiceBypass = mockService("testservicebp") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.BYPASS) + .addEndpoint("/testservicebp/200") + .responseCode(200) + .and().start(); + + mockServicePassTicket = mockService("testservicept") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) + .applid("TSTSVRPT") + .addEndpoint("/testservicept/200") + .responseCode(200) + .and().start(); + mockServicePassTicketMisconfigured = mockService("testservicepterror") .scope(Scope.CLASS) .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) @@ -155,9 +183,15 @@ void startMockServices() throws Exception { .responseCode(200) .and().start(); - mockServiceWs = mockServiceWs("testservicews") + mockServiceZoweJwt = mockService("testservice") .scope(Scope.CLASS) - .start(); + .authenticationScheme(AuthenticationScheme.ZOWE_JWT) + .addEndpoint("/testservice/200") + .responseCode(200) + .and() + .addEndpoint("/testservice/401") + .responseCode(401) + .and().start(); } @AfterAll @@ -207,6 +241,53 @@ private LogRecordData assertOneLogRecordExported(String expectedUrl) { @Nested class WhenRequestToAPIML { + @Test + void givenLoginEndpoint_thenLog() { + given() + .auth().preemptive() + .basic("wronguser", "wrongpass") + .post(basePath + "/gateway/api/v1/auth/login") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported("/gateway/api/v1/auth/login"); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("gateway", getAttribute(logBody, "service.id")); + assertEquals("POST", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("EACCES: Permission is denied; the specified password is incorrect", getAttribute(logBody, "auth.error.message")); + assertEquals("Unauthorized", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:gateway:" + port, getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/gateway/api/v1/auth/login", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("basicAuth", getAttribute(logBody, "auth.service.auth.method")); + } + + @Test + void givenCatalogEndpoint_thenLog() { + given() + .get(basePath + "/apicatalog/ui/v1/index.html") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/200"); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertNull(getAttribute(logBody, "user.id")); + assertEquals("apicatalog", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertNull(getAttribute(logBody, "auth.status")); + assertEquals("localhost:apicatalog:" + port, getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/apicatalog/ui/v1/index.html", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertNull(getAttribute(logBody, "auth.method")); + } + } @Nested @@ -239,18 +320,6 @@ void givenNoRoute_thenLog() { @Nested class WhenServiceBypass { - private MockService mockServiceBypass; - - @BeforeAll - void init() { - mockServiceBypass = mockService("testservicebp") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.BYPASS) - .addEndpoint("/testservicebp/200") - .responseCode(200) - .and().start(); - } - @Test void givenRouted_withBypass_thenLog() { given() @@ -280,22 +349,6 @@ void givenRouted_withBypass_thenLog() { @Nested class WhenServiceRequiresJwt { - private MockService mockServiceZoweJwt; - - @BeforeEach - void setUp() { - mockServiceZoweJwt = mockService("testservice") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.ZOWE_JWT) - .addEndpoint("/testservice/200") - .responseCode(200) - .and() - .addEndpoint("/testservice/401") - .responseCode(401) - .and().start(); - } - - @Nested class WhenAuthPresent { @@ -406,6 +459,17 @@ void givenRouted_whenAuthFail_thenLog() { assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); } + @Test + void givenRouted_withOidc_failure_thenLog() { + when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(false); + + given() + .header(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_OIDC_TOKEN) + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + } + @Test void givenRouted_withAuthJwt_failure_invalidToken_thenLog() { given() @@ -487,24 +551,40 @@ class WhenAuthAbsent { @Nested class WhenServiceRequiresPassTicket { - private MockService mockServicePassTicket; + @Nested + class WhenMisconfigured { + + @Test + void thenLog() { + given() + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicepterror/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported("/testservicepterror/api/v1/200"); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertNull(getAttribute(logBody, "user.id")); + assertEquals("testservicepterror", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals(mockServicePassTicketMisconfigured.getInstanceId(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicepterror/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("httpBasicPassTicket", getAttribute(logBody, "auth.service.auth.method")); + assertNull(getAttribute(logBody, "auth.method")); + } - @BeforeAll - void setUp() { - mockServicePassTicket = mockService("testservicept") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) - .applid("TSTSVRPT") - .addEndpoint("/testservicept/200") - .responseCode(200) - .and().start(); } @Nested class WhenAuthPresent { @Test - void givenRouted_withAuthPassTicketSucess_thenLog() { + void whenSucess_thenLog() { given() .cookie(AUTH_COOKIE, login()) .get(basePath + "/testservicept/api/v1/200") @@ -589,88 +669,6 @@ private Object getAttribute(String logBody, String attributeName) { } } - @Test - void givenLoginEndpoint_thenLog() { - given() - .auth().preemptive() - .basic("wronguser", "wrongpass") - .post(basePath + "/gateway/api/v1/auth/login") - .then() - .statusCode(401); - - var logRecord = assertOneLogRecordExported("/gateway/api/v1/auth/login"); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertEquals("gateway", getAttribute(logBody, "service.id")); - assertEquals("POST", getAttribute(logBody, "http.request.method")); - assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("EACCES: Permission is denied; the specified password is incorrect", getAttribute(logBody, "auth.error.message")); - assertEquals("Unauthorized", getAttribute(logBody, "auth.error.type")); - assertEquals("localhost:gateway:" + port, getAttribute(logBody, "service.instance.id")); - assertEquals("401", getAttribute(logBody, "service.response_code")); - assertEquals("/gateway/api/v1/auth/login", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("basicAuth", getAttribute(logBody, "auth.service.auth.method")); - } - - @Test - void givenCatalogEndpoint_thenLog() { - given() - .get(basePath + "/apicatalog/ui/v1/index.html") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/200"); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertNull(getAttribute(logBody, "user.id")); - assertEquals("apicatalog", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertNull(getAttribute(logBody, "auth.status")); - assertEquals("localhost:apicatalog:" + port, getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/apicatalog/ui/v1/index.html", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertNull(getAttribute(logBody, "auth.method")); - } - - @Test - void givenRouted_withMisconfiguredAuthPassTicket_thenLog() { - given() - .cookie(AUTH_COOKIE, login()) - .get(basePath + "/testservicepterror/api/v1/200") - .then() - .statusCode(200); - - var logRecord = assertOneLogRecordExported("/testservicepterror/api/v1/200"); - assertAttributesBase(logRecord.getResource().getAttributes(), port); - @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); - assertNull(getAttribute(logBody, "user.id")); - assertEquals("testservicepterror", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals(mockServicePassTicketMisconfigured.getInstanceId(), getAttribute(logBody, "service.instance.id")); - assertEquals("200", getAttribute(logBody, "service.response_code")); - assertEquals("/testservicepterror/api/v1/200", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("httpBasicPassTicket", getAttribute(logBody, "auth.service.auth.method")); - assertNull(getAttribute(logBody, "auth.method")); - } - - @Test - void givenRouted_withOidc_failure_thenLog() { - when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(false); - - given() - .header(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_OIDC_TOKEN) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); - } - private String login() { var token = given() .contentType(ContentType.JSON) @@ -694,30 +692,4 @@ private String login() { } } -/* @Test - - private String login() { - var token = given() - .contentType(ContentType.JSON) - .body(""" - { - "username": "USER", - "password": "validPassword" - } - """) - .log().all() - .when() - .post(URI.create(basePath + LOGIN_ENDPOINT)) - .then() - .statusCode(204) - .cookie(AUTH_COOKIE) - .extract() - .cookie(AUTH_COOKIE); - - setUp(); // clean up log emitted from the login - return token; - } - - } -*/ } From e1136b4363525c4122a0535b834542a9c2ac49a8 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 23 Apr 2026 12:30:18 +0200 Subject: [PATCH 12/29] tests reorganized passing Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 98 ++++++++++++------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 2dea9af707..4284464dc6 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -29,7 +29,9 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.constants.ApimlConstants; @@ -132,6 +134,7 @@ void thenLogCustomAttributes() { ) @ActiveProfiles({"OpenTelemetryTest", "zos"}) @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) class WhenOnboardedService extends AcceptanceTestWithMockServices { private static final String VALID_OIDC_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; @@ -151,47 +154,10 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @MockitoBean private X509NativeMapper x509TokenProvider; - private MockService mockServiceBypass; - private MockService mockServicePassTicket; - private MockService mockServicePassTicketMisconfigured; - private MockService mockServiceZoweJwt; - @BeforeAll void startMockServices() throws Exception { SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); SslContext.prepareSslAuthentication(configurer); - - mockServiceBypass = mockService("testservicebp") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.BYPASS) - .addEndpoint("/testservicebp/200") - .responseCode(200) - .and().start(); - - mockServicePassTicket = mockService("testservicept") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) - .applid("TSTSVRPT") - .addEndpoint("/testservicept/200") - .responseCode(200) - .and().start(); - - mockServicePassTicketMisconfigured = mockService("testservicepterror") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) - .addEndpoint("/testservicepterror/200") - .responseCode(200) - .and().start(); - - mockServiceZoweJwt = mockService("testservice") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.ZOWE_JWT) - .addEndpoint("/testservice/200") - .responseCode(200) - .and() - .addEndpoint("/testservice/401") - .responseCode(401) - .and().start(); } @AfterAll @@ -239,6 +205,7 @@ private LogRecordData assertOneLogRecordExported(String expectedUrl) { // Requests that target API ML (/login, /query, /logout, /services, etc.) @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenRequestToAPIML { @Test @@ -273,7 +240,7 @@ void givenCatalogEndpoint_thenLog() { .then() .statusCode(200); - var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/200"); + var logRecord = assertOneLogRecordExported("/apicatalog/ui/v1/index.html"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); @@ -291,6 +258,7 @@ void givenCatalogEndpoint_thenLog() { } @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceDoesNotExist { @Test @@ -318,8 +286,21 @@ void givenNoRoute_thenLog() { } @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceBypass { + private MockService mockServiceBypass; + + @BeforeAll + void init() { + mockServiceBypass = mockService("testservicebp") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.BYPASS) + .addEndpoint("/testservicebp/200") + .responseCode(200) + .and().start(); + } + @Test void givenRouted_withBypass_thenLog() { given() @@ -347,8 +328,24 @@ void givenRouted_withBypass_thenLog() { } @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresJwt { + private MockService mockServiceZoweJwt; + + @BeforeAll + void init() { + mockServiceZoweJwt = mockService("testservice") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.ZOWE_JWT) + .addEndpoint("/testservice/200") + .responseCode(200) + .and() + .addEndpoint("/testservice/401") + .responseCode(401) + .and().start(); + } + @Nested class WhenAuthPresent { @@ -549,8 +546,30 @@ class WhenAuthAbsent { } @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresPassTicket { + private MockService mockServicePassTicket; + private MockService mockServicePassTicketMisconfigured; + + @BeforeAll + void init() { + mockServicePassTicket = mockService("testservicept") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) + .applid("TSTSVRPT") + .addEndpoint("/testservicept/200") + .responseCode(200) + .and().start(); + + mockServicePassTicketMisconfigured = mockService("testservicepterror") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET) + .addEndpoint("/testservicepterror/200") + .responseCode(200) + .and().start(); + } + @Nested class WhenMisconfigured { @@ -617,6 +636,7 @@ class WhenAuthAbsent { } @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresX509 { @Nested @@ -632,6 +652,7 @@ class WhenAuthAbsent { } @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresOidc { @Nested @@ -646,6 +667,7 @@ class WhenAuthAbsent { } @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresSafIdt { @Nested From ed16d5e8d1552bbf8938d7750bdf99080c3c5a95 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 23 Apr 2026 14:21:04 +0200 Subject: [PATCH 13/29] wip unit tests Signed-off-by: Pablo Carle --- .../filters/AbstractAuthSchemeFactory.java | 31 +++++++++++-------- ...ngConfigurationErrorFilterFactoryTest.java | 17 ++++++---- 2 files changed, 29 insertions(+), 19 deletions(-) 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 7609b2c8d6..1e2bd7f65a 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 @@ -16,6 +16,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; @@ -34,7 +35,11 @@ import java.net.HttpCookie; import java.security.cert.CertificateEncodingException; -import java.util.*; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -110,30 +115,30 @@ public abstract class AbstractAuthSchemeFactory CERTIFICATE_HEADERS_TEST = headerName -> - StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[0]) || - StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[1]) || - StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[2]); + Strings.CI.equals(headerName, CERTIFICATE_HEADERS[0]) || + Strings.CI.equals(headerName, CERTIFICATE_HEADERS[1]) || + Strings.CI.equals(headerName, CERTIFICATE_HEADERS[2]); private static final Predicate CREDENTIALS_COOKIE_INPUT = cookie -> - StringUtils.equalsIgnoreCase(cookie.getName(), PAT_COOKIE_AUTH_NAME) || - StringUtils.equalsIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME) || + Strings.CI.equals(cookie.getName(), PAT_COOKIE_AUTH_NAME) || + Strings.CI.equals(cookie.getName(), COOKIE_AUTH_NAME) || StringUtils.startsWithIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME + "."); private static final Predicate CREDENTIALS_COOKIE = cookie -> CREDENTIALS_COOKIE_INPUT.test(cookie) || - StringUtils.equalsIgnoreCase(cookie.getName(), "jwtToken") || - StringUtils.equalsIgnoreCase(cookie.getName(), "LtpaToken2"); + Strings.CI.equals(cookie.getName(), "jwtToken") || + Strings.CI.equals(cookie.getName(), "LtpaToken2"); private static final Predicate CREDENTIALS_HEADER_INPUT = headerName -> - StringUtils.equalsIgnoreCase(headerName, HttpHeaders.AUTHORIZATION) || - StringUtils.equalsIgnoreCase(headerName, PAT_HEADER_NAME); + Strings.CI.equals(headerName, HttpHeaders.AUTHORIZATION) || + Strings.CI.equals(headerName, PAT_HEADER_NAME); private static final Predicate CREDENTIALS_HEADER = headerName -> CREDENTIALS_HEADER_INPUT.test(headerName) || CERTIFICATE_HEADERS_TEST.test(headerName) || - StringUtils.equalsIgnoreCase(headerName, "X-SAF-Token") || - StringUtils.equalsIgnoreCase(headerName, CLIENT_CERT_HEADER) || - StringUtils.equalsIgnoreCase(headerName, HttpHeaders.COOKIE); + Strings.CI.equals(headerName, "X-SAF-Token") || + Strings.CI.equals(headerName, CLIENT_CERT_HEADER) || + Strings.CI.equals(headerName, HttpHeaders.COOKIE); protected final InstanceInfoService instanceInfoService; protected final MessageService messageService; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java index 0f0a1f7cfc..2b4bbfbf2f 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java @@ -13,6 +13,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; @@ -22,19 +25,24 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ExtendWith(MockitoExtension.class) class RoutingConfigurationErrorFilterFactoryTest { private static final String MESSAGE = "test message"; - private RoutingConfigurationErrorFilterFactory underTest; + private GatewayFilter filter; private MockServerHttpRequest request = MockServerHttpRequest.get("https://localhost/some/url").build(); - private MockServerWebExchange exchange; + private MockServerWebExchange exchange = MockServerWebExchange.from(request); + + @Spy + private RoutingConfigurationErrorFilterFactory underTest = new RoutingConfigurationErrorFilterFactory(null, null); + @Spy + private OtelRequestContext otelContext = OtelRequestContext.of(exchange); @BeforeEach void init() { @@ -44,13 +52,11 @@ void init() { config.setAuthenticationScheme("safIdt"); config.setServiceId("serviceId"); - underTest = spy(new RoutingConfigurationErrorFilterFactory(null, null)); filter = underTest.apply(config); } @Test void givenConfig_whenApply_thenSetAuthInformationWithoutErrorType() { - var otelContext = spy(OtelRequestContext.of(exchange)); exchange.getAttributes().put(OtelRequestContext.OTEL_CONTEXT, otelContext); StepVerifier.create(filter.filter(exchange, e -> Mono.empty())).verifyComplete(); @@ -64,7 +70,6 @@ void givenConfig_whenApply_thenSetAuthInformationWithoutErrorType() { @Test void givenConfig_whenApply_thenSetFailedAuthInformationWithErrorType() { - var otelContext = spy(OtelRequestContext.of(exchange)); exchange.getAttributes().put(OtelRequestContext.OTEL_CONTEXT, otelContext); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); From 9ecdbd1e93d8d1c5e916ed8e7bc44eaab52af9ce Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 23 Apr 2026 16:45:54 +0200 Subject: [PATCH 14/29] wip new tests Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 153 +++++++++++++----- 1 file changed, 116 insertions(+), 37 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 4284464dc6..db21350160 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -42,6 +42,7 @@ import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.zaas.security.mapping.OIDCExternalMapper; import org.zowe.apiml.zaas.security.mapping.X509NativeMapper; +import org.zowe.apiml.zaas.security.service.saf.SafIdtProvider; import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import java.net.URI; @@ -302,7 +303,7 @@ void init() { } @Test - void givenRouted_withBypass_thenLog() { + void thenLog() { given() .cookie(AUTH_COOKIE, login()) .get(basePath + "/testservicebp/api/v1/200") @@ -433,6 +434,7 @@ void givenRouted_withOidc_success_thenLog() { @Nested class WhenAuthFailure { + // Is this test the same as whenNoJwtProvided_thenLog? @Test void givenRouted_whenAuthFail_thenLog() { given() @@ -457,7 +459,7 @@ void givenRouted_whenAuthFail_thenLog() { } @Test - void givenRouted_withOidc_failure_thenLog() { + void whenOidcTokenInvalid_thenLog() { when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(false); given() @@ -465,10 +467,15 @@ void givenRouted_withOidc_failure_thenLog() { .get(basePath + "/testservice/api/v1/200") .then() .statusCode(200); + + // TODO assertions + var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); + + assertAttributesBase(logRecord.getResource().getAttributes(), port); } @Test - void givenRouted_withAuthJwt_failure_invalidToken_thenLog() { + void whenInvalidToken_thenLog() { given() .cookie("apimlAuthenticationToken", "invalid.jwt.token") .get(basePath + "/testservice/api/v1/401") @@ -491,7 +498,7 @@ void givenRouted_withAuthJwt_failure_invalidToken_thenLog() { } @Test - void givenRouted_withAuthJwt_failure_expiredToken_thenLog() { + void whenExpiredToken_thenLog() { given() .cookie("apimlAuthenticationToken", createExpiredZoweJwtToken("USER", "z/OS", "Ltpa", httpConfig.getHttpsConfig())) .get(basePath + "/testservice/api/v1/401") @@ -513,28 +520,6 @@ void givenRouted_withAuthJwt_failure_expiredToken_thenLog() { assertEquals("JWT", getAttribute(logBody, "auth.method")); } - @Test - void givenRouted_withAuthJwt_noJwt_thenLog() { - given() - .get(basePath + "/testservice/api/v1/401") - .then() - .statusCode(401); - - var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); - var logBody = logRecord.getBodyValue().asString(); - assertEquals("testservice", getAttribute(logBody, "service.id")); - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); - assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); - assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("401", getAttribute(logBody, "service.response_code")); - assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertNull(getAttribute(logBody, "auth.method")); - } - } } @@ -542,7 +527,30 @@ void givenRouted_withAuthJwt_noJwt_thenLog() { @Nested class WhenAuthAbsent { + @Test + void whenNoJwtProvided_thenLog() { + given() + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); + assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertNull(getAttribute(logBody, "auth.method")); + } + } + } @Nested @@ -631,6 +639,18 @@ void whenSucess_thenLog() { @Nested class WhenAuthAbsent { + @Test + void whenNoPassTicketProvided_thenLog() { + given() + .get(basePath + "/testservicept/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported("/testservicept/api/v1/401"); + + // TODO Complete + } + } } @@ -639,6 +659,21 @@ class WhenAuthAbsent { @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresX509 { + @MockitoBean + private X509NativeMapper x509TokenProvider; + + private MockService mockServiceX509; + + @BeforeAll + void init() { + mockServiceX509 = mockService("testservicex509") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.X509) + .addEndpoint("/testservicex509/200") + .responseCode(200) + .and().start(); + } + @Nested class WhenAuthPresent { @@ -647,36 +682,79 @@ class WhenAuthPresent { @Nested class WhenAuthAbsent { + } } @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class WhenServiceRequiresOidc { + class WhenServiceRequiresSafIdt { - @Nested - class WhenAuthPresent { + @MockitoBean + private SafIdtProvider safIdtProvider; + + private MockService mockServiceSafIdt; + @BeforeAll + void init() { + mockServiceSafIdt = mockService("testservicesafidt") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.SAF_IDT) + .addEndpoint("/testservicesafidt/200") + .responseCode(200) + .and().start(); } @Nested - class WhenAuthAbsent { - } + class WhenAuthPresent { - } + @Test + void whenSuccess_thenLog() { + given() + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicesafidt/api/v1/200") + .then() + .statusCode(200); - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class WhenServiceRequiresSafIdt { + var logRecord = assertOneLogRecordExported("/testservicesafidt/api/v1/200"); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("USER", getAttribute(logBody, "user.id")); + assertEquals("testservicesafidt", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("SUCCESS", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservicesafidt:" + mockServiceSafIdt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicesafidt/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("safIdt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); + } - @Nested - class WhenAuthPresent { + @Test + void whenFailure_thenLog() { + + } } @Nested class WhenAuthAbsent { + + @Test + void whenNoSafIdtProvided_thenLog() { + given() + .get(basePath + "/testservice/api/v1/401") + .then() + .statusCode(401); + + var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); + + // TODO Complete + } + } } @@ -712,6 +790,7 @@ private String login() { setUp(); // clean up log emitted from the login return token; } + } } From ed65774e37e35a365c6c88c253e8b929595a028c Mon Sep 17 00:00:00 2001 From: nxhafa Date: Fri, 24 Apr 2026 12:51:25 +0200 Subject: [PATCH 15/29] change approach of setting error type in otelContext Signed-off-by: nxhafa --- .../zowe/apiml/ZaasSchemeTransformApi.java | 70 ++++++++++++------- .../FailedAuthenticationWebHandler.java | 2 +- .../apiml/ZaasSchemeTransformApiTest.java | 43 ++++++------ .../org/zowe/apiml/ticket/TicketResponse.java | 2 - .../zowe/apiml/zaas/ZaasTokenResponse.java | 2 - .../filters/AbstractAuthSchemeFactory.java | 4 +- .../filters/AbstractTokenFilterFactory.java | 5 +- .../filters/PassticketFilterFactory.java | 4 +- ...outingConfigurationErrorFilterFactory.java | 2 +- .../gateway/filters/SafIdtFilterFactory.java | 4 +- .../gateway/filters/ZaasSchemeTransform.java | 9 +-- .../filters/ZaasSchemeTransformRest.java | 9 +-- .../gateway/filters/ZosmfFilterFactory.java | 5 +- .../gateway/filters/ZoweFilterFactory.java | 4 +- .../AbstractTokenFilterFactoryTest.java | 2 +- 15 files changed, 91 insertions(+), 76 deletions(-) diff --git a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java index bfcc38aa9c..f6d5177fc8 100644 --- a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java +++ b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java @@ -24,11 +24,14 @@ import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.server.ServerWebExchange; import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.filters.*; import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.passticket.ApplicationNameNotProvidedException; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.product.opentelemetry.OtelRequestContext; import org.zowe.apiml.ticket.TicketResponse; import org.zowe.apiml.zaas.ZaasTokenResponse; import org.zowe.apiml.zaas.security.service.TokenCreationService; @@ -93,10 +96,11 @@ private ErrorHeaders createErrorMessage(String errorMessage) { return new ErrorHeaders(errorMessage); } - private ErrorHeaders createInvalidAuthenticationErrorMessage() { + private Mono> createInvalidAuthenticationErrorMessage() { String messageKey = "org.zowe.apiml.common.unauthorized"; String logMessage = messageService.createMessage(messageKey).mapToLogMessage(); - return new ErrorHeaders(logMessage); + var headers = new ErrorHeaders(logMessage); + return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(headers, null)); } private AbstractAuthSchemeFactory.AuthorizationResponse createMissingAuthenticationErrorMessage() { @@ -110,11 +114,12 @@ private Mono> createAutho } @Override - public Mono> passticket(RequestCredentials requestCredentials) { + public Mono> passticket(RequestCredentials requestCredentials, ServerWebExchange exchange) { var applicationName = requestCredentials.getApplId(); + var otelRequestContext = OtelRequestContext.of(exchange); if (StringUtils.isBlank(applicationName)) { - // TODO update errorType when passticket ApplId is missing - return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), TicketResponse.builder().errorType("").build()); + otelRequestContext.authErrorType(ApplicationNameNotProvidedException.class.getName()); + return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."),null); } try { @@ -122,11 +127,13 @@ public Mono> pas Optional authSource = authSourceService.getAuthSourceFromRequest(request); var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), TicketResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), TicketResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -149,7 +156,8 @@ public Mono> pas return Mono.error(new ZaasInternalErrorException(currentApimlId, e.getMessage())); } catch (Exception e) { log.debug("Token has expired", e); - return createAuthorizationResponse(createInvalidAuthenticationErrorMessage(), TicketResponse.builder().errorType(e.getClass().getName()).build()); + otelRequestContext.authErrorType(e.getClass().getName()); + return createInvalidAuthenticationErrorMessage(); } } @@ -162,11 +170,12 @@ private void updateServiceId(Optional authSource, RequestCredentials } @Override - public Mono> safIdt(RequestCredentials requestCredentials) { + public Mono> safIdt(RequestCredentials requestCredentials, ServerWebExchange exchange) { var applicationName = requestCredentials.getApplId(); + var otelRequestContext = OtelRequestContext.of(exchange); if (StringUtils.isBlank(applicationName)) { - // TODO update errorType when ApplId is missing - return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), ZaasTokenResponse.builder().errorType("").build()); + otelRequestContext.authErrorType(ApplicationNameNotProvidedException.class.getName()); + return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), null); } try { @@ -174,11 +183,13 @@ public Mono> Optional authSource = authSourceService.getAuthSourceFromRequest(request); var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -197,22 +208,26 @@ public Mono> return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot generate SAF IDT", e); - return createAuthorizationResponse(createErrorMessage(e.getMessage()), ZaasTokenResponse.builder().errorType(e.getClass().getName()).build()); + otelRequestContext.authErrorType(e.getClass().getName()); + return createAuthorizationResponse(createErrorMessage(e.getMessage()), null); } } @Override - public Mono> zosmf(RequestCredentials requestCredentials) { + public Mono> zosmf(RequestCredentials requestCredentials, ServerWebExchange exchange) { + var otelRequestContext = OtelRequestContext.of(exchange); try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), ZaasTokenResponse.builder().errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -227,27 +242,30 @@ public Mono> return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot obtain z/OSMF token", e); - return createAuthorizationResponse(createErrorMessage(e.getMessage()), ZaasTokenResponse.builder().errorType(e.getClass().getName()).build()); + otelRequestContext.authErrorType(e.getClass().getName()); + return createAuthorizationResponse(createErrorMessage(e.getMessage()), null); } } @Override - public Mono> zoweJwt(RequestCredentials requestCredentials) { - var zaasTokenResponseBuilder = ZaasTokenResponse.builder(); + public Mono> zoweJwt(RequestCredentials requestCredentials, ServerWebExchange exchange) { + var otelRequestContext = OtelRequestContext.of(exchange); try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); if (authSource.isEmpty()) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), zaasTokenResponseBuilder.errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), zaasTokenResponseBuilder.errorType(missingAuthenticationErrorResponse.getBody()).build()); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); var token = authSourceService.getJWT(authSource.get()); - var response = zaasTokenResponseBuilder.cookieName(COOKIE_AUTH_NAME) + var response = ZaasTokenResponse.builder().cookieName(COOKIE_AUTH_NAME) .token(token) .userId(authSourceParsed.getUserId()) .distributedIds(authSource.filter(OIDCAuthSource.class::isInstance) @@ -261,10 +279,10 @@ public Mono> } catch (Exception e) { log.debug("Cannot obtain Zowe JWT token", e); if (e.getCause() instanceof BadJWTException || e.getCause() instanceof ParseException || e.getCause() instanceof ExpiredJWTException) { - zaasTokenResponseBuilder.authSourceType(AuthSource.AuthSourceType.JWT.name()); + otelRequestContext.authSourceType(AuthSource.AuthSourceType.JWT.name()); } - return createAuthorizationResponse(createInvalidAuthenticationErrorMessage(), zaasTokenResponseBuilder.errorType(e.getClass().getName()).build()); - + otelRequestContext.authErrorType(e.getClass().getName()); + return createInvalidAuthenticationErrorMessage(); } } diff --git a/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java b/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java index 28ab96a943..cf36697ffd 100644 --- a/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java +++ b/apiml/src/main/java/org/zowe/apiml/handler/FailedAuthenticationWebHandler.java @@ -53,11 +53,11 @@ public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, A log.debug("Unauthorized access to '{}' endpoint", requestUri); otelContext.authenticationFailed(); otelContext.authErrorMessage(exception.getMessage()); + otelContext.authErrorType(exception.getClass().getName()); var bufferFactory = new DefaultDataBufferFactory(); AtomicReference buffer = new AtomicReference<>(); BiConsumer consumer = (message, status) -> { exchange.getResponse().setStatusCode(status); - otelContext.authErrorType(status.getReasonPhrase()); if (message != null) { exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); try { diff --git a/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java b/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java index 73345b32ae..609a612383 100644 --- a/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java +++ b/apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.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.springframework.web.server.ServerWebExchange; import org.zowe.apiml.gateway.filters.RequestCredentials; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; @@ -97,7 +98,7 @@ void thenReturnsExpectedTicket() throws PassTicketException { when(passTicketService.generate("USER1", "app1")).thenReturn("ticket123"); - StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertNotNull(result); TicketResponse response = result.getBody(); assertNotNull(response); @@ -112,7 +113,7 @@ void whenTicketGenerationFails_writeErrorHeader() throws PassTicketException { when(passTicketService.generate("USER1", "app1")).thenThrow(new RuntimeException("boom")); - StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); }).verifyComplete(); } @@ -126,7 +127,7 @@ void whenAuthSourceMissing_returnsMissingAuthError() { RequestCredentials credentials = mockCredentials(); when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); - StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertNotNull(result); assertNull(result.getBody()); assertTrue(result.getHeaders().header("x-zowe-error").isEmpty()); @@ -141,7 +142,7 @@ void whenAuthSourceInvalid_writeErrorHeader() throws PassTicketException { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); when(authSourceService.isValid(authSource)).thenReturn(false); - StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -150,7 +151,7 @@ void whenAuthSourceInvalid_writeErrorHeader() throws PassTicketException { @Test void whenApplicationNameIsMissing_inPassticket_thenReturnsError() { when(credentials.getApplId()).thenReturn(null); - StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -168,7 +169,7 @@ class GivenSafIdtScheme { void whenMissingAppId_returnsError() { when(credentials.getApplId()).thenReturn(null); - StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -191,7 +192,7 @@ void whenValidUser_returnsToken() throws PassTicketException { when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) .thenReturn("saf-idt"); - StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertNotNull(result); assertEquals("saf-idt", result.getBody().getToken()); }).verifyComplete(); @@ -209,7 +210,7 @@ void whenOidc_returnsDistributedId() throws PassTicketException { when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) .thenReturn("saf-idt"); - StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertNotNull(result); assertEquals("saf-idt", result.getBody().getToken()); assertNotNull(result.getBody().getDistributedIds()); @@ -224,7 +225,7 @@ void whenSafIdTokenCreationFails_returnsError() { when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1")) .thenThrow(new RuntimeException("Simulated SAF IDT failure")); - StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals("Simulated SAF IDT failure", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -241,7 +242,7 @@ void whenAuthSourceInvalid_returnsError() { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource)); when(authSourceService.isValid(authSource)).thenReturn(false); - StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -251,7 +252,7 @@ void whenAuthSourceInvalid_returnsError() { void whenApplicationNameIsMissing_inSafIdt_thenReturnsError() { RequestCredentials credentials = mockCredentialsWithAppId(" "); // blank - StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -283,7 +284,7 @@ void thenReturnsJwt() { when(authSourceService.getJWT(authSource)).thenReturn("jwt-token"); - StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.zoweJwt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertNotNull(result); ZaasTokenResponse response = result.getBody(); assertNotNull(response); @@ -295,7 +296,7 @@ void thenReturnsJwt() { void whenJwtRetrievalFails_returnsErrorResponse() { when(authSourceService.getJWT(authSource)).thenThrow(new RuntimeException("boom")); - StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.zoweJwt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -307,7 +308,7 @@ void whenMissingAuthSource_returnsError() { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); - StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.zoweJwt(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -353,7 +354,7 @@ void whenValidAuthSource_returnsTokenResponse() throws ServiceNotFoundException when(zosmfService.exchangeAuthenticationForZosmfToken(anyString(), eq(parsed))) .thenReturn(mockResponse); - StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.zosmf(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertNotNull(result); assertEquals("zosmf-token", result.getBody().getToken()); @@ -365,7 +366,7 @@ void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundExcep when(zosmfService.exchangeAuthenticationForZosmfToken(any(), any())) .thenThrow(new RuntimeException("Error returned from zosmf")); - StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.zosmf(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals("Error returned from zosmf", result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -376,7 +377,7 @@ void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundExcep void whenAuthSourceMissing_returnsMissingAuthError() { when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty()); - StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> { + StepVerifier.create(transformApi.zosmf(credentials, mock(ServerWebExchange.class))).assertNext(result -> { assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0)); assertNull(result.getBody()); }).verifyComplete(); @@ -415,7 +416,7 @@ void setup() { void giveOidcToken_whenPassticket_thenReturnUserIds() { var requestCredentials = RequestCredentials.builder().applId("APPLID").build(); - StepVerifier.create(transformApi.passticket(requestCredentials)) + StepVerifier.create(transformApi.passticket(requestCredentials, mock(ServerWebExchange.class))) .assertNext(response -> { assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds()); assertSame(USER_ID, response.getBody().getUserId()); @@ -426,7 +427,7 @@ void giveOidcToken_whenPassticket_thenReturnUserIds() { void giveOidcToken_whenSafIdt_thenReturnUserIds() { var requestCredentials = RequestCredentials.builder().applId("APPLID").build(); - StepVerifier.create(transformApi.safIdt(requestCredentials)) + StepVerifier.create(transformApi.safIdt(requestCredentials, mock(ServerWebExchange.class))) .assertNext(response -> { assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds()); assertSame(USER_ID, response.getBody().getUserId()); @@ -438,7 +439,7 @@ void giveOidcToken_whenZosmf_thenReturnUserIds() throws ServiceNotFoundException var requestCredentials = RequestCredentials.builder().build(); doReturn(ZaasTokenResponse.builder().distributedIds(DISTRIBUTED_IDS).userId(USER_ID).build()).when(zosmfService).exchangeAuthenticationForZosmfToken(any(), any()); - StepVerifier.create(transformApi.zosmf(requestCredentials)) + StepVerifier.create(transformApi.zosmf(requestCredentials, mock(ServerWebExchange.class))) .assertNext(response -> { assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds()); assertSame(USER_ID, response.getBody().getUserId()); @@ -449,7 +450,7 @@ void giveOidcToken_whenZosmf_thenReturnUserIds() throws ServiceNotFoundException void giveOidcToken_whenZoweJwt_thenReturnUserIds() { var requestCredentials = RequestCredentials.builder().build(); - StepVerifier.create(transformApi.zoweJwt(requestCredentials)) + StepVerifier.create(transformApi.zoweJwt(requestCredentials, mock(ServerWebExchange.class))) .assertNext(response -> { assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds()); assertSame(USER_ID, response.getBody().getUserId()); diff --git a/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java b/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java index 62a16e7500..62c2fb172d 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java +++ b/common-service-core/src/main/java/org/zowe/apiml/ticket/TicketResponse.java @@ -35,7 +35,5 @@ public class TicketResponse { private List distributedIds; @JsonIgnore // to avoid a breaking change, this value is needed only in Otel via API call, not rest private String authSourceType; - @JsonIgnore // to avoid a breaking change, this value is needed only in Otel via API call, not rest - private String errorType; } diff --git a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java index db0fc0963c..47faa6f550 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java +++ b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java @@ -33,7 +33,5 @@ public class ZaasTokenResponse { private List distributedIds; @JsonIgnore // to avoid a breaking change, this value is needed only in Otel via API call, not rest private String authSourceType; - @JsonIgnore - private String errorType; } 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 1e2bd7f65a..fd785d41b0 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 @@ -156,7 +156,7 @@ protected AbstractAuthSchemeFactory(Class configClazz, InstanceInfoService in protected abstract AuthenticationScheme getAuthenticationScheme(); - protected abstract Function>> getAuthorizationResponseTransformer(); + protected abstract Function>> getAuthorizationResponseTransformer(ServerWebExchange exchange); /** * The method responsible for reading a response from a ZAAS component and decorating of user request (i.e. set @@ -260,7 +260,7 @@ protected ServerHttpRequest cleanHeadersOnAuthSuccess(ServerWebExchange exchange } protected GatewayFilter createGatewayFilter(T config) { - return (exchange, chain) -> getAuthorizationResponseTransformer() + return (exchange, chain) -> getAuthorizationResponseTransformer(exchange) .apply(createRequestCredentials(exchange, config).build()) .flatMap(response -> processResponse(exchange, chain, response)); } 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 e5c64428c5..954d6359ac 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 @@ -75,7 +75,6 @@ protected Mono processResponse(ServerWebExchange exchange, GatewayFilterCh Optional.ofNullable(responseBody).map(ZaasTokenResponse::getUserId).ifPresent(otelContext::userId); Optional.ofNullable(responseBody).map(ZaasTokenResponse::getDistributedIds).ifPresent(otelContext::distributedIds); Optional.ofNullable(responseBody).map(ZaasTokenResponse::getAuthSourceType).ifPresent(otelContext::authSourceType); - Optional.ofNullable(responseBody).map(ZaasTokenResponse::getErrorType).ifPresent(otelContext::authErrorType); if (!StringUtils.isEmpty(response.get().getCookieName())) { request = cleanHeadersOnAuthSuccess(exchange); @@ -104,9 +103,7 @@ protected Mono processResponse(ServerWebExchange exchange, GatewayFilterCh request = request.mutate().headers(httpHeaders -> httpHeaders.add(ApimlConstants.AUTH_FAIL_HEADER, failureHeader.get())).build(); exchange = exchange.mutate().request(request).build(); } - if (exchange.getResponse() != null) { - exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, failureHeader.get()); - } + exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, failureHeader.get()); } } if (request == null) { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PassticketFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PassticketFilterFactory.java index 43ef3400c5..e3513cc6a1 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PassticketFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PassticketFilterFactory.java @@ -56,8 +56,8 @@ protected AuthenticationScheme getAuthenticationScheme() { } @Override - protected Function>> getAuthorizationResponseTransformer() { - return zaasSchemeTransform::passticket; + protected Function>> getAuthorizationResponseTransformer(ServerWebExchange exchange) { + return requestCredentials -> zaasSchemeTransform.passticket(requestCredentials, exchange); } @Override diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactory.java index 906f1bb56d..6a2fad028f 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactory.java @@ -37,7 +37,7 @@ protected AuthenticationScheme getAuthenticationScheme() { } @Override - protected Function>> getAuthorizationResponseTransformer() { + protected Function>> getAuthorizationResponseTransformer(ServerWebExchange exchange) { throw new IllegalStateException("not implemented"); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/SafIdtFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/SafIdtFilterFactory.java index ec3c145aa9..9c2b380dc5 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/SafIdtFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/SafIdtFilterFactory.java @@ -39,8 +39,8 @@ protected AuthenticationScheme getAuthenticationScheme() { } @Override - protected Function>> getAuthorizationResponseTransformer() { - return zaasSchemeTransform::safIdt; + protected Function>> getAuthorizationResponseTransformer(ServerWebExchange exchange) { + return requestCredentials -> zaasSchemeTransform.safIdt(requestCredentials, exchange); } @Override diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransform.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransform.java index ff104bd3ba..2263e04784 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransform.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransform.java @@ -10,18 +10,19 @@ package org.zowe.apiml.gateway.filters; +import org.springframework.web.server.ServerWebExchange; import org.zowe.apiml.ticket.TicketResponse; import org.zowe.apiml.zaas.ZaasTokenResponse; import reactor.core.publisher.Mono; public interface ZaasSchemeTransform { - Mono> passticket(RequestCredentials requestCredentials); + Mono> passticket(RequestCredentials requestCredentials, ServerWebExchange exchange); - Mono> safIdt(RequestCredentials requestCredentials); + Mono> safIdt(RequestCredentials requestCredentials, ServerWebExchange exchange); - Mono> zosmf(RequestCredentials requestCredentials); + Mono> zosmf(RequestCredentials requestCredentials, ServerWebExchange exchange); - Mono> zoweJwt(RequestCredentials requestCredentials); + Mono> zoweJwt(RequestCredentials requestCredentials, ServerWebExchange exchange); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java index c75e4256d7..263bf2d6f3 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZaasSchemeTransformRest.java @@ -24,6 +24,7 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.server.ServerWebExchange; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.ticket.TicketRequest; @@ -133,7 +134,7 @@ private String getUrl(String pattern, ServiceInstance instance) { } @Override - public Mono> passticket(RequestCredentials requestCredentials) { + public Mono> passticket(RequestCredentials requestCredentials, ServerWebExchange exchange) { try { var jsonBody = WRITER.writeValueAsString(new TicketRequest(requestCredentials.getApplId())); return call( @@ -152,7 +153,7 @@ public Mono> pas } @Override - public Mono> safIdt(RequestCredentials requestCredentials) { + public Mono> safIdt(RequestCredentials requestCredentials, ServerWebExchange exchange) { try { String jsonBody = WRITER.writeValueAsString(new TicketRequest(requestCredentials.getApplId())); return call( @@ -172,7 +173,7 @@ public Mono> } @Override - public Mono> zosmf(RequestCredentials requestCredentials) { + public Mono> zosmf(RequestCredentials requestCredentials, ServerWebExchange exchange) { return call( ZaasTokenResponse.class, instance -> createRequest( @@ -184,7 +185,7 @@ public Mono> } @Override - public Mono> zoweJwt(RequestCredentials requestCredentials) { + public Mono> zoweJwt(RequestCredentials requestCredentials, ServerWebExchange exchange) { return call( ZaasTokenResponse.class, instance -> createRequest( diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZosmfFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZosmfFilterFactory.java index 95ade7a225..d93034ef57 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZosmfFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZosmfFilterFactory.java @@ -11,6 +11,7 @@ package org.zowe.apiml.gateway.filters; import org.springframework.stereotype.Service; +import org.springframework.web.server.ServerWebExchange; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.gateway.service.InstanceInfoService; import org.zowe.apiml.message.core.MessageService; @@ -36,8 +37,8 @@ protected AuthenticationScheme getAuthenticationScheme() { } @Override - protected Function>> getAuthorizationResponseTransformer() { - return zaasSchemeTransform::zosmf; + protected Function>> getAuthorizationResponseTransformer(ServerWebExchange exchange) { + return requestCredentials -> zaasSchemeTransform.zosmf(requestCredentials, exchange); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZoweFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZoweFilterFactory.java index 10e406a8dc..767a443c7f 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZoweFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ZoweFilterFactory.java @@ -43,8 +43,8 @@ protected AuthenticationScheme getAuthenticationScheme() { } @Override - protected Function>> getAuthorizationResponseTransformer() { - return zaasSchemeTransform::zoweJwt; + protected Function>> getAuthorizationResponseTransformer(ServerWebExchange exchange) { + return requestCredentials -> zaasSchemeTransform.zoweJwt(requestCredentials, exchange); } @Override diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java index fb152e9cda..07bb8358c9 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java @@ -53,7 +53,7 @@ private ServerHttpRequest testRequestMutation(AbstractAuthSchemeFactory.Authoriz new AbstractTokenFilterFactory<>(AbstractTokenFilterFactory.Config.class, null, null) { @Override - protected Function>> getAuthorizationResponseTransformer() { + protected Function>> getAuthorizationResponseTransformer(ServerWebExchange exchange) { return null; } From 291a2363487211685dc5f4f4284a6131b17d808e Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Apr 2026 13:04:08 +0200 Subject: [PATCH 16/29] wip exception name Signed-off-by: Pablo Carle --- apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java | 3 ++- .../zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java index bfcc38aa9c..7f7b5fb33f 100644 --- a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java +++ b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java @@ -27,6 +27,7 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.filters.*; import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.passticket.ApplicationNameNotProvidedException; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; import org.zowe.apiml.ticket.TicketResponse; @@ -114,7 +115,7 @@ public Mono> pas var applicationName = requestCredentials.getApplId(); if (StringUtils.isBlank(applicationName)) { // TODO update errorType when passticket ApplId is missing - return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), TicketResponse.builder().errorType("").build()); + return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), TicketResponse.builder().errorType(ApplicationNameNotProvidedException.class.getName()).build()); } try { 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 1e2bd7f65a..9c1bc69dd2 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 @@ -216,6 +216,7 @@ protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, S var otelContext = OtelRequestContext.of(exchange); otelContext.authenticationFailed(); otelContext.authErrorMessage(errorMessage); + // missing error type ? Optional.ofNullable(getAuthenticationScheme()).ifPresent(otelContext::authMethod); return exchange.getRequest().mutate().headers(headers -> { From 25e358b048436e0f3b43d7a35b8e3da7ba5d9130 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Apr 2026 14:14:35 +0200 Subject: [PATCH 17/29] fixes for unit tests Signed-off-by: Pablo Carle --- .../zowe/apiml/ZaasSchemeTransformApi.java | 64 ++++++++++++------- .../filters/AbstractAuthSchemeFactory.java | 2 +- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java index f6d5177fc8..4ab3066ab6 100644 --- a/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java +++ b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java @@ -26,7 +26,11 @@ import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; import org.zowe.apiml.constants.ApimlConstants; -import org.zowe.apiml.gateway.filters.*; +import org.zowe.apiml.gateway.filters.AbstractAuthSchemeFactory.AuthorizationResponse; +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.ApplicationNameNotProvidedException; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; @@ -46,7 +50,11 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.text.ParseException; -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; @@ -96,25 +104,25 @@ private ErrorHeaders createErrorMessage(String errorMessage) { return new ErrorHeaders(errorMessage); } - private Mono> createInvalidAuthenticationErrorMessage() { - String messageKey = "org.zowe.apiml.common.unauthorized"; - String logMessage = messageService.createMessage(messageKey).mapToLogMessage(); + private Mono> createInvalidAuthenticationErrorMessage() { + var messageKey = "org.zowe.apiml.common.unauthorized"; + var logMessage = messageService.createMessage(messageKey).mapToLogMessage(); var headers = new ErrorHeaders(logMessage); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(headers, null)); + return Mono.just(new AuthorizationResponse<>(headers, null)); } - private AbstractAuthSchemeFactory.AuthorizationResponse createMissingAuthenticationErrorMessage() { - String messageKey = "org.zowe.apiml.zaas.security.schema.missingAuthentication"; - String logMessage = messageService.createMessage(messageKey).mapToLogMessage(); - return new AbstractAuthSchemeFactory.AuthorizationResponse<>(createErrorMessage(logMessage), InsufficientAuthenticationException.class.getName()); + private AuthorizationResponse createMissingAuthenticationErrorMessage() { + var messageKey = "org.zowe.apiml.zaas.security.schema.missingAuthentication"; + var logMessage = messageService.createMessage(messageKey).mapToLogMessage(); + return new AuthorizationResponse<>(createErrorMessage(logMessage), InsufficientAuthenticationException.class.getName()); } - private Mono> createAuthorizationResponse(ErrorHeaders headers, R response) { - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(headers, response)); + private Mono> createAuthorizationResponse(ErrorHeaders headers, R response) { + return Mono.just(new AuthorizationResponse<>(headers, response)); } @Override - public Mono> passticket(RequestCredentials requestCredentials, ServerWebExchange exchange) { + public Mono> passticket(RequestCredentials requestCredentials, ServerWebExchange exchange) { var applicationName = requestCredentials.getApplId(); var otelRequestContext = OtelRequestContext.of(exchange); if (StringUtils.isBlank(applicationName)) { @@ -125,13 +133,15 @@ public Mono> pas try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); - var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } @@ -150,7 +160,7 @@ public Mono> pas .authSourceType(authSource.map(AuthSource::getType).map(Enum::name).orElse(null)) .build(); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); + return Mono.just(new AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (IRRPassTicketGenerationException e) { log.debug("Cannot generate ticket", e); return Mono.error(new ZaasInternalErrorException(currentApimlId, e.getMessage())); @@ -170,7 +180,7 @@ private void updateServiceId(Optional authSource, RequestCredentials } @Override - public Mono> safIdt(RequestCredentials requestCredentials, ServerWebExchange exchange) { + public Mono> safIdt(RequestCredentials requestCredentials, ServerWebExchange exchange) { var applicationName = requestCredentials.getApplId(); var otelRequestContext = OtelRequestContext.of(exchange); if (StringUtils.isBlank(applicationName)) { @@ -181,13 +191,15 @@ public Mono> try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); - var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } @@ -205,7 +217,7 @@ public Mono> ) .authSourceType(authSource.map(AuthSource::getType).map(Enum::name).orElse(null)) .build(); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); + return Mono.just(new AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot generate SAF IDT", e); otelRequestContext.authErrorType(e.getClass().getName()); @@ -214,18 +226,20 @@ public Mono> } @Override - public Mono> zosmf(RequestCredentials requestCredentials, ServerWebExchange exchange) { + public Mono> zosmf(RequestCredentials requestCredentials, ServerWebExchange exchange) { var otelRequestContext = OtelRequestContext.of(exchange); try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); - var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } @@ -239,7 +253,7 @@ public Mono> .ifPresent(response::setDistributedIds); authSource.map(AuthSource::getType).map(Enum::name).ifPresent(response::setAuthSourceType); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); + return Mono.just(new AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot obtain z/OSMF token", e); otelRequestContext.authErrorType(e.getClass().getName()); @@ -248,18 +262,20 @@ public Mono> } @Override - public Mono> zoweJwt(RequestCredentials requestCredentials, ServerWebExchange exchange) { + public Mono> zoweJwt(RequestCredentials requestCredentials, ServerWebExchange exchange) { var otelRequestContext = OtelRequestContext.of(exchange); try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); - var missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } @@ -275,7 +291,7 @@ public Mono> ) .authSourceType(authSource.map(AuthSource::getType).map(Enum::name).orElse(null)) .build(); - return Mono.just(new AbstractAuthSchemeFactory.AuthorizationResponse<>(EMPTY_HEADERS, response)); + return Mono.just(new AuthorizationResponse<>(EMPTY_HEADERS, response)); } catch (Exception e) { log.debug("Cannot obtain Zowe JWT token", e); if (e.getCause() instanceof BadJWTException || e.getCause() instanceof ParseException || e.getCause() instanceof ExpiredJWTException) { 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 9807ce2176..22ec1fb764 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 @@ -202,7 +202,7 @@ protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials( } /** - * This method remove a necessary subset of credentials in case of authentication fail. If ZAAS cannot generate a + * This method removes a necessary subset of credentials in case of authentication fail. If ZAAS cannot generate a * new credentials (i.e. because of basic authentication, expired token, etc.) the Gateway should provide the original * credentials passed by a user. But there are headers that could be removed to avoid misusing (see forwarding * certificate - user cannot provide a public certificate to take foreign privileges). From ad0a65b2ab9b762ab59cd60153ece598b8bbd6f4 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Apr 2026 15:29:30 +0200 Subject: [PATCH 18/29] fix sslcontext (test) initialization Signed-off-by: Pablo Carle --- .../java/org/zowe/apiml/util/config/SslContext.java | 8 ++++++-- .../OpenTelemetryResourceAttributesZosTest.java | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) 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 035338719c..fb3942831d 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 @@ -56,6 +56,10 @@ public synchronized static void reset() { isInitialized.set(false); } + public static boolean isInitialized() { + return isInitialized.get(); + } + public synchronized static void prepareSslAuthentication(SslContextConfigurer providedConfigurer) throws Exception { if (configurer.get() != null && !configurer.get().equals(providedConfigurer)) { @@ -174,7 +178,7 @@ public synchronized static void prepareSslAuthentication(SslContextConfigurer pr isInitialized.set(true); } - } -} + } +} diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index db21350160..21a0e38f18 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -157,8 +157,10 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @BeforeAll void startMockServices() throws Exception { - SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); - SslContext.prepareSslAuthentication(configurer); + if (!SslContext.isInitialized()) { + SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); + SslContext.prepareSslAuthentication(configurer); + } } @AfterAll From 78d933b7911346130e64fd1d22244d3d665490aa Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Apr 2026 16:53:16 +0200 Subject: [PATCH 19/29] add / fix passticket test Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 21a0e38f18..5d929ed0b2 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -212,7 +212,7 @@ private LogRecordData assertOneLogRecordExported(String expectedUrl) { class WhenRequestToAPIML { @Test - void givenLoginEndpoint_thenLog() { + void givenLoginEndpoint_failure_thenLog() { given() .auth().preemptive() .basic("wronguser", "wrongpass") @@ -228,7 +228,7 @@ void givenLoginEndpoint_thenLog() { assertEquals("POST", getAttribute(logBody, "http.request.method")); assertEquals("ERROR", getAttribute(logBody, "auth.status")); assertEquals("EACCES: Permission is denied; the specified password is incorrect", getAttribute(logBody, "auth.error.message")); - assertEquals("Unauthorized", getAttribute(logBody, "auth.error.type")); + assertEquals("org.zowe.apiml.security.common.error.ZosAuthenticationException", getAttribute(logBody, "auth.error.type")); assertEquals("localhost:gateway:" + port, getAttribute(logBody, "service.instance.id")); assertEquals("401", getAttribute(logBody, "service.response_code")); assertEquals("/gateway/api/v1/auth/login", getAttribute(logBody, "url.path")); @@ -642,15 +642,28 @@ void whenSucess_thenLog() { class WhenAuthAbsent { @Test - void whenNoPassTicketProvided_thenLog() { + void whenNoTokenProvided_thenLog() { given() - .get(basePath + "/testservicept/api/v1/401") + .get(basePath + "/testservicept/api/v1/200") .then() - .statusCode(401); - - var logRecord = assertOneLogRecordExported("/testservicept/api/v1/401"); + .statusCode(200); - // TODO Complete + var logRecord = assertOneLogRecordExported("/testservicept/api/v1/200"); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertNull(getAttribute(logBody, "user.id")); + assertEquals("testservicept", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservicept:" + mockServicePassTicket.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicept/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("httpBasicPassTicket", getAttribute(logBody, "auth.service.auth.method")); + assertNull(getAttribute(logBody, "auth.method")); + assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); + assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); } } @@ -737,7 +750,27 @@ void whenSuccess_thenLog() { @Test void whenFailure_thenLog() { + given() + .cookie("apimlAuthenticationToken", "invalid.jwt.token") + .get(basePath + "/testservicesafidt/api/v1/401") + .then() + .statusCode(401); + var logRecord = assertOneLogRecordExported("/testservicesafidt/api/v1/401"); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservicesafidt", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); + assertEquals("org.zowe.apiml.security.common.token.TokenNotValidException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservicesafidt:" + mockServiceSafIdt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicesafidt/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("safIdt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); } } From 6887de01167dc3423377e3c9db9cbd84219bc8de Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Apr 2026 17:33:05 +0200 Subject: [PATCH 20/29] safidt mock works, check other tests now it's mocking the TokenCreationService... Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 5d929ed0b2..fa481c1ef2 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -42,7 +42,7 @@ import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.zaas.security.mapping.OIDCExternalMapper; import org.zowe.apiml.zaas.security.mapping.X509NativeMapper; -import org.zowe.apiml.zaas.security.service.saf.SafIdtProvider; +import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import java.net.URI; @@ -155,6 +155,9 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @MockitoBean private X509NativeMapper x509TokenProvider; + @MockitoBean + private TokenCreationService tokenCreationService; + @BeforeAll void startMockServices() throws Exception { if (!SslContext.isInitialized()) { @@ -706,9 +709,6 @@ class WhenAuthAbsent { @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresSafIdt { - @MockitoBean - private SafIdtProvider safIdtProvider; - private MockService mockServiceSafIdt; @BeforeAll @@ -716,6 +716,7 @@ void init() { mockServiceSafIdt = mockService("testservicesafidt") .scope(Scope.CLASS) .authenticationScheme(AuthenticationScheme.SAF_IDT) + .applid("TSTSVRID") .addEndpoint("/testservicesafidt/200") .responseCode(200) .and().start(); @@ -726,6 +727,7 @@ class WhenAuthPresent { @Test void whenSuccess_thenLog() { + when(tokenCreationService.createSafIdTokenWithoutCredentials("USER", "TSTSVRID")).thenReturn("validsafidt"); given() .cookie(AUTH_COOKIE, login()) .get(basePath + "/testservicesafidt/api/v1/200") @@ -786,8 +788,20 @@ void whenNoSafIdtProvided_thenLog() { .statusCode(401); var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); - - // TODO Complete + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservicesafidt", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); + assertEquals("org.zowe.apiml.security.common.token.TokenNotValidException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservicesafidt:" + mockServiceSafIdt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("401", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicesafidt/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("safIdt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("JWT", getAttribute(logBody, "auth.method")); } } From 8e771d40b7e023e3e8b3d31838682402a5165ec7 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 27 Apr 2026 11:13:38 +0200 Subject: [PATCH 21/29] wip acceptance tests (fix safidt tests) Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 79 ++++++++++++++----- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index fa481c1ef2..1b90614009 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -25,6 +25,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.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; @@ -37,6 +38,7 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.MockService; import org.zowe.apiml.gateway.MockService.Scope; +import org.zowe.apiml.passticket.PassTicketException; import org.zowe.apiml.product.web.HttpConfig; import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.config.SslContextConfigurer; @@ -139,6 +141,9 @@ void thenLogCustomAttributes() { class WhenOnboardedService extends AcceptanceTestWithMockServices { private static final String VALID_OIDC_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; + private static final String VALID_PAT = "validpat"; + + private static final List CUSTOM_APIML_ATTRIBUTES = List.of("user.distributed.id", "auth.service.auth.method"); @Autowired private LogRecordExporter logExporter; @@ -434,6 +439,31 @@ void givenRouted_withOidc_success_thenLog() { assertEquals("OIDC", getAttribute(logBody, "auth.method")); } + @Test + void givenRouted_withPAT_success_thenLog() { + given() + .header(ApimlConstants.PAT_HEADER_NAME, "validpat") + .get(basePath + "/testservice/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); + assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("USER", getAttribute(logBody, "user.id")); + assertEquals(List.of("oidc.username"), getAttribute(logBody, "user.distributed.id")); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("OK", getAttribute(logBody, "auth.status")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("OIDC", getAttribute(logBody, "auth.method")); + } + } @Nested @@ -722,6 +752,11 @@ void init() { .and().start(); } + @BeforeEach + void setUp() { + Mockito.reset(tokenCreationService); + } + @Nested class WhenAuthPresent { @@ -741,38 +776,43 @@ void whenSuccess_thenLog() { assertEquals("USER", getAttribute(logBody, "user.id")); assertEquals("testservicesafidt", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("SUCCESS", getAttribute(logBody, "auth.status")); + assertEquals("OK", getAttribute(logBody, "auth.status")); assertEquals("localhost:testservicesafidt:" + mockServiceSafIdt.getPort(), getAttribute(logBody, "service.instance.id")); assertEquals("200", getAttribute(logBody, "service.response_code")); assertEquals("/testservicesafidt/api/v1/200", getAttribute(logBody, "url.path")); assertEquals("https", getAttribute(logBody, "url.scheme")); assertEquals("safIdt", getAttribute(logBody, "auth.service.auth.method")); assertEquals("JWT", getAttribute(logBody, "auth.method")); + assertNull(getAttribute(logBody, "auth.error.type")); + assertNull(getAttribute(logBody, "auth.error.message")); } @Test void whenFailure_thenLog() { + when(tokenCreationService.createSafIdTokenWithoutCredentials("USER", "TSTSVRID")).thenThrow(new PassTicketException("Test exception")); given() - .cookie("apimlAuthenticationToken", "invalid.jwt.token") - .get(basePath + "/testservicesafidt/api/v1/401") + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicesafidt/api/v1/200") .then() - .statusCode(401); + .statusCode(200); - var logRecord = assertOneLogRecordExported("/testservicesafidt/api/v1/401"); + var logRecord = assertOneLogRecordExported("/testservicesafidt/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); assertEquals("testservicesafidt", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); - assertEquals("org.zowe.apiml.security.common.token.TokenNotValidException", getAttribute(logBody, "auth.error.type")); + // assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); + assertEquals("Test exception", getAttribute(logBody, "auth.error.message")); + assertEquals("org.zowe.apiml.passticket.PassTicketException", getAttribute(logBody, "auth.error.type")); assertEquals("localhost:testservicesafidt:" + mockServiceSafIdt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("401", getAttribute(logBody, "service.response_code")); - assertEquals("/testservicesafidt/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicesafidt/api/v1/200", getAttribute(logBody, "url.path")); assertEquals("https", getAttribute(logBody, "url.scheme")); assertEquals("safIdt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("JWT", getAttribute(logBody, "auth.method")); + // assertEquals("JWT", getAttribute(logBody, "auth.method")); + assertNull(getAttribute(logBody, "auth.method")); } } @@ -781,27 +821,28 @@ void whenFailure_thenLog() { class WhenAuthAbsent { @Test - void whenNoSafIdtProvided_thenLog() { + void thenLog() { given() - .get(basePath + "/testservice/api/v1/401") + .get(basePath + "/testservicesafidt/api/v1/200") .then() - .statusCode(401); + .statusCode(200); - var logRecord = assertOneLogRecordExported("/testservice/api/v1/401"); + var logRecord = assertOneLogRecordExported("/testservicesafidt/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); assertEquals("testservicesafidt", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); assertEquals("ERROR", getAttribute(logBody, "auth.status")); - assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); - assertEquals("org.zowe.apiml.security.common.token.TokenNotValidException", getAttribute(logBody, "auth.error.type")); + assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); + assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); assertEquals("localhost:testservicesafidt:" + mockServiceSafIdt.getPort(), getAttribute(logBody, "service.instance.id")); - assertEquals("401", getAttribute(logBody, "service.response_code")); - assertEquals("/testservicesafidt/api/v1/401", getAttribute(logBody, "url.path")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservicesafidt/api/v1/200", getAttribute(logBody, "url.path")); assertEquals("https", getAttribute(logBody, "url.scheme")); assertEquals("safIdt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("JWT", getAttribute(logBody, "auth.method")); + // assertEquals("JWT", getAttribute(logBody, "auth.method")); + assertNull(getAttribute(logBody, "auth.method")); } } From 1d8ad3e34cc72930f2e902a157f7c41789629535 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Tue, 28 Apr 2026 10:47:57 +0200 Subject: [PATCH 22/29] wip Signed-off-by: Pablo Carle --- ...penTelemetryResourceAttributesZosTest.java | 42 ++++--------------- .../StartupMessageAcceptanceTest.java | 9 ++-- .../AbstractTokenFilterFactoryTest.java | 2 +- ...ngConfigurationErrorFilterFactoryTest.java | 3 +- 4 files changed, 16 insertions(+), 40 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 1b90614009..9396766934 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -44,6 +44,7 @@ import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.zaas.security.mapping.OIDCExternalMapper; import org.zowe.apiml.zaas.security.mapping.X509NativeMapper; +import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; @@ -65,6 +66,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.zowe.apiml.constants.ApimlConstants.PAT_HEADER_NAME; import static org.zowe.apiml.security.common.util.JWTTestUtils.createExpiredZoweJwtToken; class OpenTelemetryResourceAttributesZosTest { @@ -132,7 +134,8 @@ void thenLogCustomAttributes() { "apiml.security.oidc.validationType=endpoint", "apiml.security.oidc.enabled=true", "apiml.security.oidc.userInfo.uri=https://oidc.provider.com/user/info", - "apiml.security.filterChainConfiguration=new" + "apiml.security.filterChainConfiguration=new", + "apiml.security.personalAccessToken.enabled=true" } ) @ActiveProfiles({"OpenTelemetryTest", "zos"}) @@ -163,6 +166,9 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @MockitoBean private TokenCreationService tokenCreationService; + @MockitoBean + private AuthenticationService authenticationService; + @BeforeAll void startMockServices() throws Exception { if (!SslContext.isInitialized()) { @@ -442,7 +448,7 @@ void givenRouted_withOidc_success_thenLog() { @Test void givenRouted_withPAT_success_thenLog() { given() - .header(ApimlConstants.PAT_HEADER_NAME, "validpat") + .header(PAT_HEADER_NAME, "validpat") .get(basePath + "/testservice/api/v1/200") .then() .statusCode(200); @@ -703,38 +709,6 @@ void whenNoTokenProvided_thenLog() { } - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class WhenServiceRequiresX509 { - - @MockitoBean - private X509NativeMapper x509TokenProvider; - - private MockService mockServiceX509; - - @BeforeAll - void init() { - mockServiceX509 = mockService("testservicex509") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.X509) - .addEndpoint("/testservicex509/200") - .responseCode(200) - .and().start(); - } - - @Nested - class WhenAuthPresent { - - - } - - @Nested - class WhenAuthAbsent { - - } - - } - @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenServiceRequiresSafIdt { diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java index 8b52345d66..91355c9ebd 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java @@ -24,9 +24,11 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.product.web.ApimlTomcatCustomizer; @@ -48,6 +50,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) class StartupMessageAcceptanceTest { abstract static class BaseStartupTest extends AcceptanceTestWithMockServices { @@ -81,7 +84,6 @@ void verifyStartupMessage(CapturedOutput output) { @Nested @AcceptanceTest @TestInstance(Lifecycle.PER_CLASS) - @DirtiesContext @ExtendWith({MockitoExtension.class, OutputCaptureExtension.class}) @ActiveProfiles({"default"}) class GivenDefaultProfile extends BaseStartupTest { @@ -96,7 +98,6 @@ void whenFullyStartedUp_thenEmitMessage(CapturedOutput output) { @Nested @AcceptanceTest @TestInstance(Lifecycle.PER_CLASS) - @DirtiesContext @ExtendWith({MockitoExtension.class, OutputCaptureExtension.class}) @ActiveProfiles({"attlsClient", "attlsServer"}) class GivenAttlsProfile extends BaseStartupTest { @@ -125,7 +126,7 @@ class GivenAttlsProfile extends BaseStartupTest { @MockitoBean private ApimlTomcatCustomizer apimlTomcatCustomizer; - @MockitoBean + @MockitoSpyBean private ApimlInstanceRegistry apimlInstanceRegistry; @Mock private AttlsContext attlsContext; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java index 07bb8358c9..3635c4bee4 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java @@ -162,7 +162,7 @@ void givenOtelRequestContext_whenFail_thenCallAuthenticationFailed() { verify(otelRequestContext, times(1)).authenticationFailed(); verify(otelRequestContext, times(1)).authErrorMessage("test"); - verify(otelRequestContext, times(1)).authErrorType(HttpStatus.FORBIDDEN.getReasonPhrase()); + // TODO ? verify(otelRequestContext, times(1)).authErrorType(HttpStatus.FORBIDDEN.getReasonPhrase()); } @Test diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java index 2b4bbfbf2f..3052226638 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java @@ -11,6 +11,7 @@ package org.zowe.apiml.gateway.filters; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,7 +34,6 @@ class RoutingConfigurationErrorFilterFactoryTest { private static final String MESSAGE = "test message"; - private GatewayFilter filter; private MockServerHttpRequest request = MockServerHttpRequest.get("https://localhost/some/url").build(); @@ -69,6 +69,7 @@ void givenConfig_whenApply_thenSetAuthInformationWithoutErrorType() { } @Test + @Disabled("TODO: fix this test") // TODO void givenConfig_whenApply_thenSetFailedAuthInformationWithErrorType() { exchange.getAttributes().put(OtelRequestContext.OTEL_CONTEXT, otelContext); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); From fc2818c9e90babc2c7aa96ddeb6e1863b86e9dde Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 29 Apr 2026 14:09:21 +0200 Subject: [PATCH 23/29] PAT success test Signed-off-by: Pablo Carle --- .../security/common/util/JWTTestUtils.java | 48 ++++++++++++++----- apiml/build.gradle | 1 + ...penTelemetryResourceAttributesZosTest.java | 19 ++++---- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java index 29923bf2cf..279405fbde 100644 --- a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java +++ b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java @@ -12,6 +12,7 @@ import io.jsonwebtoken.Jwts; import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKeySet; import org.zowe.apiml.security.HttpsConfig; @@ -31,49 +32,74 @@ public class JWTTestUtils { public static String createZoweJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { - return createToken(username, domain, ltpaToken, config, "APIML"); + return createToken(username, domain, ltpaToken, null, config, "APIML"); } public static String createExpiredZoweJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { - return createExpiredToken(username, domain, ltpaToken, config, "APIML"); + return createExpiredToken(username, domain, ltpaToken, null, config, "APIML"); } public static String createZosmfJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { - return createToken(username, domain, ltpaToken, config, "zOSMF"); + return createToken(username, domain, ltpaToken, null, config, "zOSMF"); } - public static String createToken(String username, String domain, String ltpaToken, HttpsConfig config, String issuer) { + public static String createZowePatJwtToken(String username, String domain, List scopes, HttpsConfig config) { + return createToken(username, domain, null, scopes, config, "APIML_PAT"); + } + + public static String createToken(String username, String domain, String ltpaToken, List scopes, HttpsConfig config, String issuer) { long now = System.currentTimeMillis(); long expiration = now + 100_000L; Key jwtSecret = SecurityUtils.loadKey(config); - return Jwts.builder() + var builder = Jwts.builder(); + + builder .subject(username) .claim("dom", domain) - .claim("ltpa", ltpaToken) .issuedAt(new Date(now)) .expiration(new Date(expiration)) .issuer(issuer) .id(UUID.randomUUID().toString()) - .signWith(jwtSecret) - .compact(); + .signWith(jwtSecret); + + if (!StringUtils.isEmpty(ltpaToken)) { + builder.claim("ltpa", ltpaToken); + } + + if (scopes != null && scopes.size() > 0) { + builder.claim("scopes", scopes); + } + + return builder.compact(); } - public static String createExpiredToken(String username, String domain, String ltpaToken, HttpsConfig config, String issuer) { + public static String createExpiredToken(String username, String domain, String ltpaToken, List scopes, HttpsConfig config, String issuer) { long now = System.currentTimeMillis(); long expiration = now - 200_000L; Key jwtSecret = SecurityUtils.loadKey(config); - return Jwts.builder() + var builder = Jwts.builder(); + + builder .subject(username) .claim("dom", domain) - .claim("ltpa", ltpaToken) .issuedAt(new Date(now)) .expiration(new Date(expiration)) .issuer(issuer) .id(UUID.randomUUID().toString()) .signWith(jwtSecret) .compact(); + + if (!StringUtils.isEmpty(ltpaToken)) { + builder.claim("ltpa", ltpaToken); + } + + if (scopes != null && scopes.size() > 0) { + builder.claim("scopes", scopes); + } + + return builder.compact(); } public static String createDummyJwtToken(String username, String issuer, long expiration) { diff --git a/apiml/build.gradle b/apiml/build.gradle index df18bbf8d7..fa0b16f2ae 100644 --- a/apiml/build.gradle +++ b/apiml/build.gradle @@ -77,6 +77,7 @@ dependencies { implementation libs.opentelemetry.spring.boot.autoconfigure testImplementation(testFixtures(project(":apiml-common"))) + testImplementation(project(":zaas-service")) testImplementation(testFixtures(project(":apiml-security-common"))) testImplementation(testFixtures(project(":gateway-service"))) testImplementation libs.spring.boot.starter.test diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 9396766934..a696dd06f2 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -31,8 +31,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.NestedTestConfiguration; -import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.constants.ApimlConstants; @@ -44,8 +44,8 @@ import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.zaas.security.mapping.OIDCExternalMapper; import org.zowe.apiml.zaas.security.mapping.X509NativeMapper; -import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.TokenCreationService; +import org.zowe.apiml.zaas.security.service.token.ApimlAccessTokenProvider; import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider; import java.net.URI; @@ -68,6 +68,7 @@ import static org.mockito.Mockito.when; import static org.zowe.apiml.constants.ApimlConstants.PAT_HEADER_NAME; import static org.zowe.apiml.security.common.util.JWTTestUtils.createExpiredZoweJwtToken; +import static org.zowe.apiml.security.common.util.JWTTestUtils.createZowePatJwtToken; class OpenTelemetryResourceAttributesZosTest { @@ -144,9 +145,6 @@ void thenLogCustomAttributes() { class WhenOnboardedService extends AcceptanceTestWithMockServices { private static final String VALID_OIDC_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl"; - private static final String VALID_PAT = "validpat"; - - private static final List CUSTOM_APIML_ATTRIBUTES = List.of("user.distributed.id", "auth.service.auth.method"); @Autowired private LogRecordExporter logExporter; @@ -167,7 +165,7 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { private TokenCreationService tokenCreationService; @MockitoBean - private AuthenticationService authenticationService; + private ApimlAccessTokenProvider apimlAccessTokenProvider; @BeforeAll void startMockServices() throws Exception { @@ -447,8 +445,11 @@ void givenRouted_withOidc_success_thenLog() { @Test void givenRouted_withPAT_success_thenLog() { + var pat = createZowePatJwtToken("USER", "z/OS", List.of("testservice"), httpConfig.getHttpsConfig()); + when(apimlAccessTokenProvider.isValidForScopes(pat, "testservice")).thenReturn(true); + when(apimlAccessTokenProvider.isInvalidated(pat)).thenReturn(false); given() - .header(PAT_HEADER_NAME, "validpat") + .header(PAT_HEADER_NAME, pat) .get(basePath + "/testservice/api/v1/200") .then() .statusCode(200); @@ -458,7 +459,6 @@ void givenRouted_withPAT_success_thenLog() { @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); assertEquals("USER", getAttribute(logBody, "user.id")); - assertEquals(List.of("oidc.username"), getAttribute(logBody, "user.distributed.id")); assertEquals("testservice", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); assertEquals("OK", getAttribute(logBody, "auth.status")); @@ -467,7 +467,7 @@ void givenRouted_withPAT_success_thenLog() { assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); assertEquals("https", getAttribute(logBody, "url.scheme")); assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("OIDC", getAttribute(logBody, "auth.method")); + assertEquals("PAT", getAttribute(logBody, "auth.method")); } } @@ -475,7 +475,6 @@ void givenRouted_withPAT_success_thenLog() { @Nested class WhenAuthFailure { - // Is this test the same as whenNoJwtProvided_thenLog? @Test void givenRouted_whenAuthFail_thenLog() { given() From ce71135aa581bde4a32a7d7edf5e6b89e78ef583 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 29 Apr 2026 14:33:32 +0200 Subject: [PATCH 24/29] rollback Signed-off-by: Pablo Carle --- .../org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java index 91355c9ebd..b403b0344b 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java @@ -126,7 +126,7 @@ class GivenAttlsProfile extends BaseStartupTest { @MockitoBean private ApimlTomcatCustomizer apimlTomcatCustomizer; - @MockitoSpyBean + @MockitoBean private ApimlInstanceRegistry apimlInstanceRegistry; @Mock private AttlsContext attlsContext; From 340a1ec558119cc4fe78c29db0f0d823b2acdd4e Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 29 Apr 2026 14:54:03 +0200 Subject: [PATCH 25/29] cleanup Signed-off-by: Pablo Carle --- .../org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java | 1 - .../zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java | 1 - .../apiml/gateway/filters/AbstractTokenFilterFactoryTest.java | 1 - 3 files changed, 3 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java index b403b0344b..d835a53ccc 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java @@ -28,7 +28,6 @@ import org.springframework.test.context.NestedTestConfiguration; import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.discovery.ApimlInstanceRegistry; import org.zowe.apiml.product.web.ApimlTomcatCustomizer; 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 22ec1fb764..75957e09ad 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 @@ -216,7 +216,6 @@ protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, S var otelContext = OtelRequestContext.of(exchange); otelContext.authenticationFailed(); otelContext.authErrorMessage(errorMessage); - // missing error type ? Optional.ofNullable(getAuthenticationScheme()).ifPresent(otelContext::authMethod); return exchange.getRequest().mutate().headers(headers -> { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java index 3635c4bee4..20218fc0cc 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java @@ -162,7 +162,6 @@ void givenOtelRequestContext_whenFail_thenCallAuthenticationFailed() { verify(otelRequestContext, times(1)).authenticationFailed(); verify(otelRequestContext, times(1)).authErrorMessage("test"); - // TODO ? verify(otelRequestContext, times(1)).authErrorType(HttpStatus.FORBIDDEN.getReasonPhrase()); } @Test From 983cbc5759234cb11592676fd10755c14764f45c Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 29 Apr 2026 15:13:05 +0200 Subject: [PATCH 26/29] remove double semicolon Signed-off-by: Pablo Carle --- .../functional/apicatalog/ApiCatalogAuthenticationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4924c41102..35447d77c2 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 @@ -283,7 +283,7 @@ void givenValidCertificate(String endpoint, Request request) { .then() .log().all() .statusCode(HttpStatus.OK.value()) - .onFailMessage("On Gateway URL: " + endpoint);; + .onFailMessage("On Gateway URL: " + endpoint); } @ParameterizedTest(name = "givenValidCertificateAndBasicAuth {index} {0} ") From ef65b860c828106448431e67e005cd55bddea67a Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 29 Apr 2026 16:12:53 +0200 Subject: [PATCH 27/29] pr review 1 Signed-off-by: Pablo Carle --- .../apiml/security/common/util/JWTTestUtils.java | 3 +-- .../OpenTelemetryResourceAttributesZosTest.java | 14 +++++++++++++- .../apicatalog/ApiCatalogAuthenticationTest.java | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java index 279405fbde..1d85d8269c 100644 --- a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java +++ b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java @@ -88,8 +88,7 @@ public static String createExpiredToken(String username, String domain, String l .expiration(new Date(expiration)) .issuer(issuer) .id(UUID.randomUUID().toString()) - .signWith(jwtSecret) - .compact(); + .signWith(jwtSecret); if (!StringUtils.isEmpty(ltpaToken)) { builder.claim("ltpa", ltpaToken); diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index a696dd06f2..8e73287148 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -508,10 +508,22 @@ void whenOidcTokenInvalid_thenLog() { .then() .statusCode(200); - // TODO assertions var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); assertAttributesBase(logRecord.getResource().getAttributes(), port); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + assertEquals("testservice", getAttribute(logBody, "service.id")); + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("ERROR", getAttribute(logBody, "auth.status")); + assertEquals("ZWEAG160E No authentication provided in the request", getAttribute(logBody, "auth.error.message")); + assertEquals("org.springframework.security.authentication.InsufficientAuthenticationException", getAttribute(logBody, "auth.error.type")); + assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id")); + assertEquals("200", getAttribute(logBody, "service.response_code")); + assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertNull(getAttribute(logBody, "auth.method")); + assertEquals("zoweJwt", getAttribute(logBody, "auth.service.auth.method")); } @Test 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 35447d77c2..a5eae63a9a 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 @@ -298,7 +298,7 @@ void givenValidCertificateAndBasicAuth(String endpoint, Request request) { ) .then() .statusCode(is(SC_OK)) - .onFailMessage("On Gateway URL: " + endpoint);; + .onFailMessage("On Gateway URL: " + endpoint); } } From d2f2d664c73aa63c2857a72ab26ddaa993d94674 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 29 Apr 2026 16:43:12 +0200 Subject: [PATCH 28/29] cleanup Signed-off-by: Pablo Carle --- .../acceptance/OpenTelemetryResourceAttributesZosTest.java | 2 -- .../filters/RoutingConfigurationErrorFilterFactoryTest.java | 3 --- 2 files changed, 5 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index 8e73287148..d99bad2b5e 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -788,7 +788,6 @@ void whenFailure_thenLog() { assertEquals("testservicesafidt", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); assertEquals("ERROR", getAttribute(logBody, "auth.status")); - // assertEquals("ZWEAO402E The request has not been applied because it lacks valid authentication credentials.", getAttribute(logBody, "auth.error.message")); assertEquals("Test exception", getAttribute(logBody, "auth.error.message")); assertEquals("org.zowe.apiml.passticket.PassTicketException", getAttribute(logBody, "auth.error.type")); assertEquals("localhost:testservicesafidt:" + mockServiceSafIdt.getPort(), getAttribute(logBody, "service.instance.id")); @@ -796,7 +795,6 @@ void whenFailure_thenLog() { assertEquals("/testservicesafidt/api/v1/200", getAttribute(logBody, "url.path")); assertEquals("https", getAttribute(logBody, "url.scheme")); assertEquals("safIdt", getAttribute(logBody, "auth.service.auth.method")); - // assertEquals("JWT", getAttribute(logBody, "auth.method")); assertNull(getAttribute(logBody, "auth.method")); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java index 3052226638..c875c7552c 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java @@ -69,16 +69,13 @@ void givenConfig_whenApply_thenSetAuthInformationWithoutErrorType() { } @Test - @Disabled("TODO: fix this test") // TODO void givenConfig_whenApply_thenSetFailedAuthInformationWithErrorType() { exchange.getAttributes().put(OtelRequestContext.OTEL_CONTEXT, otelContext); - exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); StepVerifier.create(filter.filter(exchange, e -> Mono.empty())).verifyComplete(); verify(otelContext).authenticationFailed(); verify(otelContext).authErrorMessage(MESSAGE); - verify(otelContext).authErrorType(HttpStatus.UNAUTHORIZED.getReasonPhrase()); verify(otelContext).authMethod(AuthenticationScheme.SAF_IDT); verify(underTest).cleanHeadersOnAuthFail(exchange, MESSAGE); From fc9d817000fd470529e98864762d4a6264882768 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 29 Apr 2026 16:48:52 +0200 Subject: [PATCH 29/29] fix checkstyle Signed-off-by: Pablo Carle --- .../filters/RoutingConfigurationErrorFilterFactoryTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java index c875c7552c..71da0bca8e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/RoutingConfigurationErrorFilterFactoryTest.java @@ -11,14 +11,12 @@ package org.zowe.apiml.gateway.filters; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.gateway.filter.GatewayFilter; -import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.zowe.apiml.auth.AuthenticationScheme;