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-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 5eb5aeff35..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 @@ -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,28 +32,73 @@ 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, 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 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, HttpsConfig config, String issuer) { + 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, List scopes, HttpsConfig config, String issuer) { + long now = System.currentTimeMillis(); + long expiration = now - 200_000L; + Key jwtSecret = SecurityUtils.loadKey(config); + + var builder = Jwts.builder(); + + builder + .subject(username) + .claim("dom", domain) + .issuedAt(new Date(now)) + .expiration(new Date(expiration)) + .issuer(issuer) + .id(UUID.randomUUID().toString()) + .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 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/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java b/apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java index 0824974131..4ab3066ab6 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,13 +21,21 @@ 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.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; 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; @@ -39,7 +49,12 @@ import java.io.ByteArrayInputStream; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.*; +import java.text.ParseException; +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; @@ -59,7 +74,6 @@ *
  • z/OSMF token exchange
  • *
  • Zowe JWT generation
  • * - *

    * *

    * This bean is only active when {@code modulithConfig} is present in the Spring context. @@ -86,41 +100,50 @@ 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() { - 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 Mono> 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)); + 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 AuthorizationResponse<>(headers, response)); } @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)) { - return createErrorMessage("ApplicationName not provided."); + otelRequestContext.authErrorType(ApplicationNameNotProvidedException.class.getName()); + return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."),null); } try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -137,12 +160,13 @@ 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())); } catch (Exception e) { log.debug("Token has expired", e); + otelRequestContext.authErrorType(e.getClass().getName()); return createInvalidAuthenticationErrorMessage(); } } @@ -156,21 +180,28 @@ 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)) { - return createErrorMessage("ApplicationName not provided."); + otelRequestContext.authErrorType(ApplicationNameNotProvidedException.class.getName()); + return createAuthorizationResponse(createErrorMessage("ApplicationName not provided."), null); } try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -186,24 +217,31 @@ 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); - return createErrorMessage(e.getMessage()); + 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); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); @@ -215,29 +253,35 @@ 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); - return createErrorMessage(e.getMessage()); + otelRequestContext.authErrorType(e.getClass().getName()); + return createAuthorizationResponse(createErrorMessage(e.getMessage()), null); } } @Override - public Mono> zoweJwt(RequestCredentials requestCredentials) { + public Mono> zoweJwt(RequestCredentials requestCredentials, ServerWebExchange exchange) { + var otelRequestContext = OtelRequestContext.of(exchange); try { var request = new RequestCredentialsHttpServletRequestAdapter(requestCredentials); Optional authSource = authSourceService.getAuthSourceFromRequest(request); + AuthorizationResponse missingAuthenticationErrorResponse; if (authSource.isEmpty()) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } updateServiceId(authSource, request); if (!authSourceService.isValid(authSource.get())) { - return createMissingAuthenticationErrorMessage(); + missingAuthenticationErrorResponse = createMissingAuthenticationErrorMessage(); + otelRequestContext.authErrorType(missingAuthenticationErrorResponse.getBody()); + return createAuthorizationResponse((ErrorHeaders) missingAuthenticationErrorResponse.getHeaders(), null); } var authSourceParsed = authSourceService.parse(authSource.get()); var token = authSourceService.getJWT(authSource.get()); - var response = ZaasTokenResponse.builder() - .cookieName(COOKIE_AUTH_NAME) + var response = ZaasTokenResponse.builder().cookieName(COOKIE_AUTH_NAME) .token(token) .userId(authSourceParsed.getUserId()) .distributedIds(authSource.filter(OIDCAuthSource.class::isInstance) @@ -247,9 +291,13 @@ 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) { + otelRequestContext.authSourceType(AuthSource.AuthSourceType.JWT.name()); + } + otelRequestContext.authErrorType(e.getClass().getName()); return createInvalidAuthenticationErrorMessage(); } } 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..cf36697ffd 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,7 +49,11 @@ 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()); + otelContext.authErrorType(exception.getClass().getName()); var bufferFactory = new DefaultDataBufferFactory(); AtomicReference buffer = new AtomicReference<>(); BiConsumer consumer = (message, status) -> { @@ -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/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/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 8ffa801eed..d99bad2b5e 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -19,20 +19,33 @@ 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.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; 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.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; 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; import org.zowe.apiml.zaas.security.mapping.OIDCExternalMapper; +import org.zowe.apiml.zaas.security.mapping.X509NativeMapper; +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; @@ -45,9 +58,17 @@ 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.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 { @@ -69,7 +90,7 @@ private boolean assertAttributesBase(Attributes attributes, int port) { @Nested @AcceptanceTest - @ActiveProfiles({ "OpenTelemetryTest", "zos" }) + @ActiveProfiles({"OpenTelemetryTest", "zos"}) @TestPropertySource( properties = { "otel.sdk.disabled=false", @@ -114,10 +135,13 @@ 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" }) + @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"; @@ -125,50 +149,30 @@ class WhenOnboardedService extends AcceptanceTestWithMockServices { @Autowired private LogRecordExporter logExporter; + @Autowired + private HttpConfig httpConfig; + @MockitoBean private OIDCExternalMapper oidcExternalMapper; @MockitoBean private OIDCTokenProvider oidcTokenProvider; - private MockService mockServiceZoweJwt; - private MockService mockServicePassTicket; - private MockService mockServicePassTicketMisconfigured; - private MockService mockServiceBypass; + @MockitoBean + private X509NativeMapper x509TokenProvider; + + @MockitoBean + private TokenCreationService tokenCreationService; + + @MockitoBean + private ApimlAccessTokenProvider apimlAccessTokenProvider; @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); - - mockServiceZoweJwt = mockService("testservice") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.ZOWE_JWT) - .addEndpoint("/testservice/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(); - - mockServiceBypass = mockService("testservicebp") - .scope(Scope.CLASS) - .authenticationScheme(AuthenticationScheme.BYPASS) - .addEndpoint("/testservicebp/200") - .responseCode(200) - .and().start(); + if (!SslContext.isInitialized()) { + SslContextConfigurer configurer = new SslContextConfigurer("password".toCharArray(), "../keystore/client_cert/client-certs.p12", "../keystore/localhost/localhost.keystore.p12"); + SslContext.prepareSslAuthentication(configurer); + } } @AfterAll @@ -214,264 +218,639 @@ private LogRecordData assertOneLogRecordExported(String expectedUrl) { return logRecord; } - @Test - void givenRouted_whenAuthFail_thenLog() { - given() - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); + // Requests that target API ML (/login, /query, /logout, /services, etc.) + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class WhenRequestToAPIML { + + @Test + void givenLoginEndpoint_failure_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("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")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("basicAuth", getAttribute(logBody, "auth.service.auth.method")); + } - var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); + @Test + void givenCatalogEndpoint_thenLog() { + given() + .get(basePath + "/apicatalog/ui/v1/index.html") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported("/apicatalog/ui/v1/index.html"); + 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")); + } - 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("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("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 { - return objectMapper.readValue(logBody, Map.class).get(attributeName); - } catch (JsonProcessingException e) { - fail("Invalid JSON", e); - return null; + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class WhenServiceDoesNotExist { + + @Test + void givenNoRoute_thenLog() { + given() + .get(basePath + "/nonexistant/api/v1/200") + .then() + .statusCode(404); + + var logRecord = assertOneLogRecordExported("/nonexistant/api/v1/200"); + 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 - @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); + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class WhenServiceBypass { - var logRecord = assertOneLogRecordExported("/testservice/api/v1/200"); - 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("https", getAttribute(logBody, "url.scheme")); - assertEquals("zoweJwt", getAttribute(logBody, "auth.method")); - } + private MockService mockServiceBypass; - @Test - void givenCatalogEndpoint_thenLog() { - given() - .get(basePath + "/apicatalog/ui/v1/index.html") - .then() - .statusCode(200); + @BeforeAll + void init() { + mockServiceBypass = mockService("testservicebp") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.BYPASS) + .addEndpoint("/testservicebp/200") + .responseCode(200) + .and().start(); + } + + @Test + void thenLog() { + given() + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicebp/api/v1/200") + .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("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")); + } - var logRecord = assertOneLogRecordExported("/apicatalog/ui/v1/index.html"); - 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_withAuthSuccess_thenLog() { - given() - .cookie("apimlAuthenticationToken", login()) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); + @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(); + } - 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("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")); - } + @Nested + 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("/testservice/api/v1/200"); + 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); + @Test + void givenRouted_withX509_success_thenLog() { + given() + .config(SslContext.clientCertUser) + .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("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")); + } - var logRecord = assertOneLogRecordExported("/nonexistant/api/v1/200"); - 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_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("/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")); + } - @Test - void givenRouted_withAuthPassTicketSucess_thenLog() { - given() - .cookie(AUTH_COOKIE, login()) - .get(basePath + "/testservicept/api/v1/200") - .then() - .statusCode(200); + @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, pat) + .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("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("PAT", getAttribute(logBody, "auth.method")); + } - var logRecord = assertOneLogRecordExported("/testservicept/api/v1/200"); - 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() - .cookie(AUTH_COOKIE, login()) - .get(basePath + "/testservicebp/api/v1/200") - .then() - .statusCode(200); + @Nested + class WhenAuthFailure { + + @Test + void givenRouted_whenAuthFail_thenLog() { + given() + .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("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")); + } - 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("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 whenOidcTokenInvalid_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); + + 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 - void givenRouted_withMisconfiguredAuthPassTicket_thenLog() { - given() - .cookie(AUTH_COOKIE, login()) - .get(basePath + "/testservicepterror/api/v1/200") - .then() - .statusCode(200); + @Test + void whenInvalidToken_thenLog() { + given() + .cookie("apimlAuthenticationToken", "invalid.jwt.token") + .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("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 whenExpiredToken_thenLog() { + given() + .cookie("apimlAuthenticationToken", createExpiredZoweJwtToken("USER", "z/OS", "Ltpa", httpConfig.getHttpsConfig())) + .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("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")); + } + + } + + } + + @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")); + } + + } - 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("FAILED", 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_thenLog() { - when(oidcTokenProvider.isValid(VALID_OIDC_TOKEN)).thenReturn(true); - when(oidcExternalMapper.mapToMainframeUserId(any())).thenReturn("USER"); + @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(); + } - given() - .header(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_OIDC_TOKEN) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); + @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")); + } + + } + + @Nested + class WhenAuthPresent { + + @Test + void whenSucess_thenLog() { + given() + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicept/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported("/testservicept/api/v1/200"); + 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 { + + @Test + void whenNoTokenProvided_thenLog() { + given() + .get(basePath + "/testservicept/api/v1/200") + .then() + .statusCode(200); + + 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")); + } + + } - 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")); } - @Test - void givenRouted_withX509_thenLog() { - given() - .config(SslContext.clientCertUser) - .get(basePath + "/testservice/api/v1/200") - .then() - .statusCode(200); + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class WhenServiceRequiresSafIdt { + + private MockService mockServiceSafIdt; + + @BeforeAll + void init() { + mockServiceSafIdt = mockService("testservicesafidt") + .scope(Scope.CLASS) + .authenticationScheme(AuthenticationScheme.SAF_IDT) + .applid("TSTSVRID") + .addEndpoint("/testservicesafidt/200") + .responseCode(200) + .and().start(); + } + + @BeforeEach + void setUp() { + Mockito.reset(tokenCreationService); + } + + @Nested + class WhenAuthPresent { + + @Test + void whenSuccess_thenLog() { + when(tokenCreationService.createSafIdTokenWithoutCredentials("USER", "TSTSVRID")).thenReturn("validsafidt"); + given() + .cookie(AUTH_COOKIE, login()) + .get(basePath + "/testservicesafidt/api/v1/200") + .then() + .statusCode(200); + + 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("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(AUTH_COOKIE, login()) + .get(basePath + "/testservicesafidt/api/v1/200") + .then() + .statusCode(200); + + 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("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("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")); + assertNull(getAttribute(logBody, "auth.method")); + } + + } + + @Nested + class WhenAuthAbsent { + + @Test + void thenLog() { + given() + .get(basePath + "/testservicesafidt/api/v1/200") + .then() + .statusCode(200); + + 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("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("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.method")); + } + + } - 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("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")); + } + + private Object getAttribute(String logBody, String attributeName) { + var objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(logBody, Map.class).get(attributeName); + } catch (JsonProcessingException e) { + fail("Invalid JSON", e); + return null; + } } 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/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java index fcfaa4d73e..d835a53ccc 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/StartupMessageAcceptanceTest.java @@ -25,6 +25,8 @@ import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; import org.springframework.cloud.netflix.eureka.server.event.EurekaRegistryAvailableEvent; 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.util.ReflectionTestUtils; import org.zowe.apiml.discovery.ApimlInstanceRegistry; @@ -47,6 +49,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) class StartupMessageAcceptanceTest { abstract static class BaseStartupTest extends AcceptanceTestWithMockServices { 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..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 @@ -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; @@ -96,7 +101,6 @@ * public static class Config extends AbstractAuthSchemeFactory.AbstractConfig { * } * } - * * @Data class MyResponse { * private String token; * } @@ -111,28 +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) || - StringUtils.startsWithIgnoreCase(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); + CERTIFICATE_HEADERS_TEST.test(headerName) || + 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; @@ -150,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 @@ -196,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). @@ -209,6 +215,7 @@ protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials( protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, String errorMessage) { var otelContext = OtelRequestContext.of(exchange); otelContext.authenticationFailed(); + otelContext.authErrorMessage(errorMessage); Optional.ofNullable(getAuthenticationScheme()).ifPresent(otelContext::authMethod); return exchange.getRequest().mutate().headers(headers -> { @@ -253,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/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/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/gateway/filters/AbstractTokenFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java index 0631263bd9..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 @@ -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; @@ -52,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; } @@ -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,23 @@ 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"); } @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..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 @@ -10,9 +10,12 @@ 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.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.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; @@ -21,39 +24,57 @@ 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 = MockServerWebExchange.from(request); - @BeforeAll + @Spy + private RoutingConfigurationErrorFilterFactory underTest = new RoutingConfigurationErrorFilterFactory(null, null); + @Spy + private OtelRequestContext otelContext = OtelRequestContext.of(exchange); + + @BeforeEach void init() { + exchange = MockServerWebExchange.from(request); var config = new RoutingConfigurationErrorFilterFactory.Config(); config.setMessage(MESSAGE); config.setAuthenticationScheme("safIdt"); config.setServiceId("serviceId"); - underTest = spy(new RoutingConfigurationErrorFilterFactory(null, null)); filter = underTest.apply(config); } @Test - void givenConfig_whenApply_thenSetAuthInformation() { - var otelContext = spy(OtelRequestContext.of(exchange)); + void givenConfig_whenApply_thenSetAuthInformationWithoutErrorType() { 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() { + 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); } 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()); 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..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 @@ -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); } } }