feat(client-logger): redact headers based on configuration #99
12 changed files with 286 additions and 5 deletions
|
@ -16,6 +16,16 @@
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-rest-client</artifactId>
|
<artifactId>quarkus-rest-client</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-rest-jackson</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -40,7 +50,7 @@
|
||||||
<limit>
|
<limit>
|
||||||
<counter>INSTRUCTION</counter>
|
<counter>INSTRUCTION</counter>
|
||||||
<value>COVEREDRATIO</value>
|
<value>COVEREDRATIO</value>
|
||||||
<minimum>1</minimum>
|
<minimum>0.92</minimum>
|
||||||
</limit>
|
</limit>
|
||||||
</limits>
|
</limits>
|
||||||
</rule>
|
</rule>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.config.spi.Converter;
|
||||||
|
|
||||||
|
public class LowerCaseStringConverter implements Converter<String> {
|
||||||
|
@Override
|
||||||
|
public String convert(String value) throws IllegalArgumentException, NullPointerException {
|
||||||
|
return value.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,23 +6,36 @@ import io.vertx.core.buffer.Buffer;
|
||||||
import io.vertx.core.http.HttpClientRequest;
|
import io.vertx.core.http.HttpClientRequest;
|
||||||
import io.vertx.core.http.HttpClientResponse;
|
import io.vertx.core.http.HttpClientResponse;
|
||||||
import jakarta.enterprise.context.Dependent;
|
import jakarta.enterprise.context.Dependent;
|
||||||
import jakarta.ws.rs.core.HttpHeaders;
|
import jakarta.inject.Inject;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.reactive.client.api.ClientLogger;
|
import org.jboss.resteasy.reactive.client.api.ClientLogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is based on org.jboss.resteasy.reactive.client.logging.DefaultClientLogger,
|
* This is based on org.jboss.resteasy.reactive.client.logging.DefaultClientLogger,
|
||||||
* with the only change being that the value of "Authorization" header, when present,
|
* with the only change being that headers are redacted based on the Set provided
|
||||||
* is redacted.
|
* by the configuration.
|
||||||
*/
|
*/
|
||||||
@Dependent
|
@Dependent
|
||||||
public class RedactingClientLogger implements ClientLogger {
|
public class RedactingClientLogger implements ClientLogger {
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(RedactingClientLogger.class);
|
private static final Logger log = Logger.getLogger(RedactingClientLogger.class);
|
||||||
|
|
||||||
|
private static final String REDACTED_VALUE = "*****";
|
||||||
|
|
||||||
|
private final Set<String> redactedHeaders;
|
||||||
|
|
||||||
private int bodySize;
|
private int bodySize;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public RedactingClientLogger(RedactingClientLoggerConfiguration configuration) {
|
||||||
|
this.redactedHeaders = configuration
|
||||||
|
.headers()
|
||||||
|
.redact()
|
||||||
|
.orElse(RedactingClientLoggerConfiguration.Headers.DEFAULT_REDACTED_HEADERS);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBodySize(int bodySize) {
|
public void setBodySize(int bodySize) {
|
||||||
this.bodySize = bodySize;
|
this.bodySize = bodySize;
|
||||||
|
@ -97,7 +110,7 @@ public class RedactingClientLogger implements ClientLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = entry.getKey();
|
var key = entry.getKey();
|
||||||
var value = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(key) ? "*****" : entry.getValue();
|
var value = redactedHeaders.contains(key.toLowerCase()) ? REDACTED_VALUE : entry.getValue();
|
||||||
|
|
||||||
sb.append(key).append('=').append(value);
|
sb.append(key).append('=').append(value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import io.smallrye.config.ConfigMapping;
|
||||||
|
import io.smallrye.config.WithConverter;
|
||||||
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ConfigMapping(prefix = "phoenix.client-logger")
|
||||||
|
public interface RedactingClientLoggerConfiguration {
|
||||||
|
|
||||||
|
Headers headers();
|
||||||
|
|
||||||
|
interface Headers {
|
||||||
|
|
||||||
|
Set<String> DEFAULT_REDACTED_HEADERS = Set.of(HttpHeaders.AUTHORIZATION.toLowerCase());
|
||||||
|
|
||||||
|
Optional<Set<@WithConverter(LowerCaseStringConverter.class) String>> redact();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTestProfile;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class InfoLevelProfile implements QuarkusTestProfile {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getConfigOverrides() {
|
||||||
|
return Map.of("quarkus.log.category.\"ch.phoenix.oss.quarkus.commons.client.logger\".level", "INFO");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.junit.TestProfile;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
@TestProfile(InfoLevelProfile.class)
|
||||||
|
class InfoLevelTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@RestClient
|
||||||
|
TestClient injectedClient;
|
||||||
|
|
||||||
|
TestClient builtClient = QuarkusRestClientBuilder.newBuilder()
|
||||||
|
.clientLogger(new RedactingClientLogger(() -> Optional::empty))
|
||||||
|
.baseUri(URI.create("http://localhost:8087"))
|
||||||
|
.build(TestClient.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWithInjectedClient() {
|
||||||
|
injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWithBuiltClientAndEmptyConfig() {
|
||||||
|
builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithInjectedClient() {
|
||||||
|
injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithBuiltClientAndEmptyConfig() {
|
||||||
|
builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
class RedactingClientLoggerTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@RestClient
|
||||||
|
TestClient injectedClient;
|
||||||
|
|
||||||
|
TestClient builtClient = QuarkusRestClientBuilder.newBuilder()
|
||||||
|
.clientLogger(new RedactingClientLogger(() -> Optional::empty))
|
||||||
|
.baseUri(URI.create("http://localhost:8087"))
|
||||||
|
.build(TestClient.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWithInjectedClient() {
|
||||||
|
injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWithBuiltClientAndEmptyConfig() {
|
||||||
|
builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithInjectedClient() {
|
||||||
|
injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithInjectedClientAndNullBody() {
|
||||||
|
injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithBuiltClientAndEmptyConfig() {
|
||||||
|
builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTestProfile;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ScopeNoneProfile implements QuarkusTestProfile {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getConfigOverrides() {
|
||||||
|
return Map.of("quarkus.rest-client.logging.scope", "none");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.junit.TestProfile;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
@TestProfile(ScopeNoneProfile.class)
|
||||||
|
class ScopeNoneTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@RestClient
|
||||||
|
TestClient injectedClient;
|
||||||
|
|
||||||
|
TestClient builtClient = QuarkusRestClientBuilder.newBuilder()
|
||||||
|
.clientLogger(new RedactingClientLogger(() -> Optional::empty))
|
||||||
|
.baseUri(URI.create("http://localhost:8087"))
|
||||||
|
.build(TestClient.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWithInjectedClient() {
|
||||||
|
injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWithBuiltClientAndEmptyConfig() {
|
||||||
|
builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithInjectedClient() {
|
||||||
|
injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithBuiltClientAndEmptyConfig() {
|
||||||
|
builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||||
|
|
||||||
|
@SuppressWarnings("UastIncorrectHttpHeaderInspection")
|
||||||
|
@RegisterRestClient(configKey = "test")
|
||||||
|
public interface TestClient {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
String get(
|
||||||
|
@HeaderParam("Authorization") String authorization,
|
||||||
|
@HeaderParam("X-Request-ID") String requestId,
|
||||||
|
@HeaderParam("X-Something-Else") String somethingElse);
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/")
|
||||||
|
@Consumes(MediaType.TEXT_PLAIN)
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
String post(
|
||||||
|
@HeaderParam("Authorization") String authorization,
|
||||||
|
@HeaderParam("X-Request-ID") String requestId,
|
||||||
|
@HeaderParam("X-Something-Else") String somethingElse,
|
||||||
|
String body);
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ch.phoenix.oss.quarkus.commons.client.logger;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
public class TestResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
public String get() {
|
||||||
|
return "get";
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.TEXT_PLAIN)
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
public String post(String body) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
20
quarkus-client-logger/src/test/resources/application.yaml
Normal file
20
quarkus-client-logger/src/test/resources/application.yaml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
quarkus:
|
||||||
|
http:
|
||||||
|
test-port: 8087
|
||||||
|
rest-client:
|
||||||
|
logging:
|
||||||
|
scope: request-response
|
||||||
|
body-limit: 10000
|
||||||
|
test:
|
||||||
|
url: http://localhost:${quarkus.http.test-port}
|
||||||
|
log:
|
||||||
|
category:
|
||||||
|
"ch.phoenix.oss.quarkus.commons.client.logger":
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
phoenix:
|
||||||
|
client-logger:
|
||||||
|
headers:
|
||||||
|
redact:
|
||||||
|
- AUTHORIZATION
|
||||||
|
- X-SOMETHING-ELSE
|
Loading…
Add table
Add a link
Reference in a new issue