Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,7 +51,7 @@
* </ol>
*
* <p>This filter is intended to be used on /login endpoints.</p>
*
* <p>
* Caution: Filter will read the body and make it available as a request attribute
*
* @see LoginRequest
Expand All @@ -72,6 +73,8 @@ public BasicLoginFilter(CompoundAuthProvider compoundAuthProvider, FailedAuthent
@Override
public Mono<Void> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -48,11 +49,15 @@ public class FailedAuthenticationWebHandler implements ServerAuthenticationFailu
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
var exchange = webFilterExchange.getExchange();
var requestUri = exchange.getRequest().getURI().getPath();
var otelContext = OtelRequestContext.of(exchange);
log.debug("Unauthorized access to '{}' endpoint", requestUri);
otelContext.authenticationFailed();
otelContext.authErrorMessage(exception.getMessage());
var bufferFactory = new DefaultDataBufferFactory();
AtomicReference<DefaultDataBuffer> buffer = new AtomicReference<>();
BiConsumer<ApiMessageView, HttpStatus> consumer = (message, status) -> {
exchange.getResponse().setStatusCode(status);
otelContext.authErrorType(status.getReasonPhrase());
if (message != null) {
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
try {
Expand All @@ -64,7 +69,7 @@ public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, A
buffer.set(bufferFactory.wrap(new byte[0]));
}
};
var addHeader = (BiConsumer<String, String>)(name, value) -> exchange.getResponse().getHeaders().add(name, value);
var addHeader = (BiConsumer<String, String>) (name, value) -> exchange.getResponse().getHeaders().add(name, value);
try {
handler.handleException(requestUri, consumer, addHeader, exception);
} catch (ServletException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private boolean assertAttributesBase(Attributes attributes, int port) {

@Nested
@AcceptanceTest
@ActiveProfiles({ "OpenTelemetryTest", "zos" })
@ActiveProfiles({"OpenTelemetryTest", "zos"})
@TestPropertySource(
properties = {
"otel.sdk.disabled=false",
Expand Down Expand Up @@ -118,7 +118,7 @@ void thenLogCustomAttributes() {
"apiml.security.filterChainConfiguration=new"
}
)
@ActiveProfiles({ "OpenTelemetryTest", "zos" })
@ActiveProfiles({"OpenTelemetryTest", "zos"})
class WhenOnboardedService extends AcceptanceTestWithMockServices {

private static final String VALID_OIDC_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl";
Expand Down Expand Up @@ -217,7 +217,7 @@ void givenRouted_whenAuthFail_thenLog() {
given()
.get(basePath + "/testservice/api/v1/200")
.then()
.statusCode(200);
.statusCode(200);

var logRecord = assertOneLogRecordExported();

Expand All @@ -226,7 +226,7 @@ void givenRouted_whenAuthFail_thenLog() {
var logBody = logRecord.getBodyValue().asString();
assertEquals("testservice", getAttribute(logBody, "service.id"));
assertEquals("GET", getAttribute(logBody, "http.request.method"));
assertEquals("FAILED", getAttribute(logBody, "auth.status"));
assertEquals("ERROR", getAttribute(logBody, "auth.status"));
assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id"));
assertEquals("200", getAttribute(logBody, "service.response_code"));
assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path"));
Expand All @@ -246,27 +246,28 @@ private Object getAttribute(String logBody, String attributeName) {
}

@Test
@Disabled("This test is for invalid authentication (server error). To be reviewed in follow up story")
void givenLoginEndpoint_thenLog() {
given()
.auth().preemptive()
.basic("wronguser", "wrongpass")
.post(basePath + "/gateway/api/v1/auth/login")
.then()
.statusCode(500);
.statusCode(401);

var logRecord = assertOneLogRecordExported();
assertAttributesBase(logRecord.getResource().getAttributes(), port);
@SuppressWarnings("null")
var logBody = logRecord.getBodyValue().asString();
assertEquals("apicatalog", getAttribute(logBody, "service.id"));
assertEquals("GET", getAttribute(logBody, "http.request.method"));
assertEquals("FAILED", getAttribute(logBody, "auth.status"));
assertEquals("localhost:testservice:" + mockServiceZoweJwt.getPort(), getAttribute(logBody, "service.instance.id"));
assertEquals("200", getAttribute(logBody, "service.response_code"));
assertEquals("/testservice/api/v1/200", getAttribute(logBody, "url.path"));
assertEquals("gateway", getAttribute(logBody, "service.id"));
assertEquals("POST", getAttribute(logBody, "http.request.method"));
assertEquals("ERROR", getAttribute(logBody, "auth.status"));
assertEquals("EACCES: Permission is denied; the specified password is incorrect", getAttribute(logBody, "auth.error.message"));
assertEquals("Unauthorized", getAttribute(logBody, "auth.error.type"));
assertEquals("localhost:gateway:" + port, getAttribute(logBody, "service.instance.id"));
assertEquals("401", getAttribute(logBody, "service.response_code"));
assertEquals("/gateway/api/v1/auth/login", getAttribute(logBody, "url.path"));
assertEquals("https", getAttribute(logBody, "url.scheme"));
assertEquals("zoweJwt", getAttribute(logBody, "auth.method"));
assertEquals("basicAuth", getAttribute(logBody, "auth.service.auth.method"));
}

@Test
Expand Down Expand Up @@ -320,7 +321,7 @@ void givenNoRoute_thenLog() {
given()
.get(basePath + "/nonexistant/api/v1/200")
.then()
.statusCode(404);
.statusCode(404);

var logRecord = assertOneLogRecordExported();
assertAttributesBase(logRecord.getResource().getAttributes(), port);
Expand Down Expand Up @@ -400,7 +401,7 @@ void givenRouted_withMisconfiguredAuthPassTicket_thenLog() {
assertNull(getAttribute(logBody, "user.id"));
assertEquals("testservicepterror", getAttribute(logBody, "service.id"));
assertEquals("GET", getAttribute(logBody, "http.request.method"));
assertEquals("FAILED", getAttribute(logBody, "auth.status"));
assertEquals("ERROR", getAttribute(logBody, "auth.status"));
assertEquals(mockServicePassTicketMisconfigured.getInstanceId(), getAttribute(logBody, "service.instance.id"));
assertEquals("200", getAttribute(logBody, "service.response_code"));
assertEquals("/testservicepterror/api/v1/200", getAttribute(logBody, "url.path"));
Expand Down Expand Up @@ -465,11 +466,11 @@ private String login() {
var token = given()
.contentType(ContentType.JSON)
.body("""
{
"username": "USER",
"password": "validPassword"
}
""")
{
"username": "USER",
"password": "validPassword"
}
""")
.log().all()
.when()
.post(URI.create(basePath + LOGIN_ENDPOINT))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.server.ServerWebExchange;
Expand Down Expand Up @@ -96,7 +97,6 @@
* public static class Config extends AbstractAuthSchemeFactory.AbstractConfig {
* }
* }
*
* @Data class MyResponse {
* private String token;
* }
Expand All @@ -112,27 +112,27 @@ public abstract class AbstractAuthSchemeFactory<T extends AbstractAuthSchemeFact

private static final Predicate<String> CERTIFICATE_HEADERS_TEST = headerName ->
StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[0]) ||
StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[1]) ||
StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[2]);
StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[1]) ||
StringUtils.equalsIgnoreCase(headerName, CERTIFICATE_HEADERS[2]);

private static final Predicate<HttpCookie> CREDENTIALS_COOKIE_INPUT = cookie ->
StringUtils.equalsIgnoreCase(cookie.getName(), PAT_COOKIE_AUTH_NAME) ||
StringUtils.equalsIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME) ||
StringUtils.startsWithIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME + ".");
StringUtils.equalsIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME) ||
StringUtils.startsWithIgnoreCase(cookie.getName(), COOKIE_AUTH_NAME + ".");
private static final Predicate<HttpCookie> CREDENTIALS_COOKIE = cookie ->
CREDENTIALS_COOKIE_INPUT.test(cookie) ||
StringUtils.equalsIgnoreCase(cookie.getName(), "jwtToken") ||
StringUtils.equalsIgnoreCase(cookie.getName(), "LtpaToken2");
StringUtils.equalsIgnoreCase(cookie.getName(), "jwtToken") ||
StringUtils.equalsIgnoreCase(cookie.getName(), "LtpaToken2");

private static final Predicate<String> CREDENTIALS_HEADER_INPUT = headerName ->
StringUtils.equalsIgnoreCase(headerName, HttpHeaders.AUTHORIZATION) ||
StringUtils.equalsIgnoreCase(headerName, PAT_HEADER_NAME);
StringUtils.equalsIgnoreCase(headerName, PAT_HEADER_NAME);
private static final Predicate<String> CREDENTIALS_HEADER = headerName ->
CREDENTIALS_HEADER_INPUT.test(headerName) ||
CERTIFICATE_HEADERS_TEST.test(headerName) ||
StringUtils.equalsIgnoreCase(headerName, "X-SAF-Token") ||
StringUtils.equalsIgnoreCase(headerName, CLIENT_CERT_HEADER) ||
StringUtils.equalsIgnoreCase(headerName, HttpHeaders.COOKIE);
CERTIFICATE_HEADERS_TEST.test(headerName) ||
StringUtils.equalsIgnoreCase(headerName, "X-SAF-Token") ||
StringUtils.equalsIgnoreCase(headerName, CLIENT_CERT_HEADER) ||
StringUtils.equalsIgnoreCase(headerName, HttpHeaders.COOKIE);

protected final InstanceInfoService instanceInfoService;
protected final MessageService messageService;
Expand Down Expand Up @@ -209,6 +209,10 @@ protected RequestCredentials.RequestCredentialsBuilder createRequestCredentials(
protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, String errorMessage) {
var otelContext = OtelRequestContext.of(exchange);
otelContext.authenticationFailed();
otelContext.authErrorMessage(errorMessage);
Optional.ofNullable(exchange.getResponse().getStatusCode())
.flatMap(httpStatusCode -> Optional.ofNullable(HttpStatus.resolve(httpStatusCode.value())))
.ifPresent(httpStatus -> otelContext.authErrorType(httpStatus.getReasonPhrase()));
Optional.ofNullable(getAuthenticationScheme()).ifPresent(otelContext::authMethod);

return exchange.getRequest().mutate().headers(headers -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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));
}

}
Expand Down Expand Up @@ -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")
Expand All @@ -144,20 +145,24 @@ void givenCookieAndHeaderInResponse_whenHandling_thenSetBoth() {
class Otel {

MockServerHttpRequest request = MockServerHttpRequest.get("/aPath").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
MockServerWebExchange exchange;
OtelRequestContext otelRequestContext;

@BeforeEach
void mockOtelContext() {
exchange = MockServerWebExchange.from(request);
otelRequestContext = spy(OtelRequestContext.of(exchange));
exchange.getAttributes().put("otel-context", otelRequestContext);
}

@Test
void givenOtelRequestContext_whenFail_thenCallAuthenticationFailed() {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
spy(AbstractAuthSchemeFactory.class).cleanHeadersOnAuthFail(exchange, "test");

verify(otelRequestContext, times(1)).authenticationFailed();
verify(otelRequestContext, times(1)).authErrorMessage("test");
verify(otelRequestContext, times(1)).authErrorType(HttpStatus.FORBIDDEN.getReasonPhrase());
}

@Test
Expand Down
Loading
Loading