Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easily mask sensitive headers from request logs by adding HeadersSanitizer #5188

Merged
merged 16 commits into from
Jan 30, 2024
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import static java.util.Objects.requireNonNull;

import java.util.Set;
import java.util.function.Function;

import com.google.common.collect.ImmutableSet;

/**
* A skeletal builder implementation for {@link HeadersSanitizer}.
*/
abstract class AbstractHeadersSanitizerBuilder<T> {

private Set<String> maskHeaders = ImmutableSet.of();

private Function<String, String> mask = (header) -> "****";

/**
* Sets the {@link Set} which includes headers to mask before logging.
*/
public AbstractHeadersSanitizerBuilder<T> maskHeaders(String... headers) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The headers defined in HttpHeaderNames should be compatible without additional casting.
  • maskingHeaders or sensitiveHeaders is preferred over maskHeaders because this function sets the headers to be masked, not the actual action.
Suggested change
public AbstractHeadersSanitizerBuilder<T> maskHeaders(String... headers) {
public AbstractHeadersSanitizerBuilder<T> maskingHeaders(CharSequence... headers) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point. I'll apply as below

  • maskHeaders -> maskingHeaders
  • mask -> maskingFunction

maskHeaders = ImmutableSet.copyOf(requireNonNull(headers, "headers"));
seonWKim marked this conversation as resolved.
Show resolved Hide resolved
return this;
}

/**
* Sets the {@link Set} which includes headers to mask before logging.
*/
public AbstractHeadersSanitizerBuilder<T> maskHeaders(Iterable<String> headers) {
seonWKim marked this conversation as resolved.
Show resolved Hide resolved
maskHeaders = ImmutableSet.copyOf(requireNonNull(headers, "headers"));
seonWKim marked this conversation as resolved.
Show resolved Hide resolved
return this;

Check warning on line 48 in core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java#L47-L48

Added lines #L47 - L48 were not covered by tests
}

/**
* Returns the {@link Set} which includes headers to mask before logging.
*/
final Set<String> maskHeaders() {
return maskHeaders;
}

/**
* Sets the {@link Function} to use to mask headers before logging.
*/
public AbstractHeadersSanitizerBuilder<T> mask(Function<String, String> mask) {
seonWKim marked this conversation as resolved.
Show resolved Hide resolved
this.mask = requireNonNull(mask, "mask");
return this;
}

/**
* Returns the {@link Function} to use to mask headers before logging.
*/
final Function<String, String> mask() {
return mask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import java.util.function.BiFunction;

import com.fasterxml.jackson.databind.JsonNode;

/**
* A sanitizer that sanitizes {@link HttpHeaders}.
*/
public interface HeadersSanitizer<T> extends BiFunction<RequestContext, HttpHeaders, T> {
/**
* Returns the default text {@link HeadersSanitizer}.
*/
static HeadersSanitizer<String> ofText() {
return TextHeadersSanitizer.INSTANCE;

Check warning on line 31 in core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java#L31

Added line #L31 was not covered by tests
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Returns a newly created {@link TextHeadersSanitizerBuilder}.
*/
static TextHeadersSanitizerBuilder builderForText() {
return new TextHeadersSanitizerBuilder();
}

/**
* Returns the default json {@link HeadersSanitizer}.
*/
static HeadersSanitizer<JsonNode> ofJson() {
return JsonHeadersSanitizer.INSTANCE;

Check warning on line 45 in core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java#L45

Added line #L45 was not covered by tests
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Returns a newly created {@link JsonHeadersSanitizerBuilder}.
*/
static JsonHeadersSanitizerBuilder builderForJson() {
return new JsonHeadersSanitizerBuilder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import io.netty.util.AsciiString;

/**
* A sanitizer that sanitizes {@link HttpHeaders} and returns {@link JsonNode}.
*/
public final class JsonHeadersSanitizer implements HeadersSanitizer<JsonNode> {

static final HeadersSanitizer<JsonNode> INSTANCE = new JsonHeadersSanitizerBuilder().build();
seonWKim marked this conversation as resolved.
Show resolved Hide resolved
private final Set<String> maskHeaders;
private final Function<String, String> mask;
private final ObjectMapper objectMapper;

JsonHeadersSanitizer(Set<String> maskHeaders, Function<String, String> mask, ObjectMapper objectMapper) {
this.maskHeaders = maskHeaders;
this.mask = mask;
this.objectMapper = objectMapper;
}

@Override
public JsonNode apply(RequestContext requestContext, HttpHeaders headers) {
final ObjectNode result = objectMapper.createObjectNode();
for (Map.Entry<AsciiString, String> e : headers) {
final String header = e.getKey().toString();
if (maskHeaders.contains(header)) {
result.put(header, mask.apply(e.getValue()));
} else {
result.put(header, e.getValue());
seonWKim marked this conversation as resolved.
Show resolved Hide resolved
}
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import static java.util.Objects.requireNonNull;

import java.util.Set;
import java.util.function.Function;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.internal.common.JacksonUtil;

/**
* A builder implementation for {@link JsonHeadersSanitizer}.
*/
public final class JsonHeadersSanitizerBuilder extends AbstractHeadersSanitizerBuilder<JsonNode> {

@Nullable
private ObjectMapper objectMapper;

/**
* Sets the {@link Set} which includes headers to mask before logging.
*/
@Override
public JsonHeadersSanitizerBuilder maskHeaders(String... headers) {
return (JsonHeadersSanitizerBuilder) super.maskHeaders(headers);
}

/**
* Sets the {@link Set} which includes headers to mask before logging.
*/
@Override
public JsonHeadersSanitizerBuilder maskHeaders(Iterable<String> headers) {
return (JsonHeadersSanitizerBuilder) super.maskHeaders(headers);

Check warning on line 51 in core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java#L51

Added line #L51 was not covered by tests
}

/**
* Sets the {@link Function} to use to mask headers before logging.
*/
@Override
public JsonHeadersSanitizerBuilder mask(Function<String, String> mask) {
return (JsonHeadersSanitizerBuilder) super.mask(mask);
}

/**
* Sets the {@link ObjectMapper} that will be used to convert headers into a {@link JsonNode}.
*/
public JsonHeadersSanitizerBuilder objectMapper(ObjectMapper objectMapper) {
this.objectMapper = requireNonNull(objectMapper, "objectMapper");
return this;

Check warning on line 67 in core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java#L66-L67

Added lines #L66 - L67 were not covered by tests
}

/**
* Returns a newly created JSON {@link HeadersSanitizer} based on the properties of this builder.
*/
public JsonHeadersSanitizer build() {
final ObjectMapper objectMapper = this.objectMapper != null ?
this.objectMapper : JacksonUtil.newDefaultObjectMapper();

return new JsonHeadersSanitizer(maskHeaders(), mask(), objectMapper);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import io.netty.util.AsciiString;

/**
* A sanitizer that sanitizes {@link HttpHeaders} and returns {@link String}.
*/
public final class TextHeadersSanitizer implements HeadersSanitizer<String> {

static final HeadersSanitizer<String> INSTANCE = new TextHeadersSanitizerBuilder().build();
ikhoon marked this conversation as resolved.
Show resolved Hide resolved

private final Set<String> maskHeaders;

private final Function<String, String> mask;

TextHeadersSanitizer(Set<String> maskHeaders, Function<String, String> mask) {
this.maskHeaders = maskHeaders;
this.mask = mask;
}

@Override
public String apply(RequestContext ctx, HttpHeaders headers) {
if (headers.isEmpty()) {
return headers.isEndOfStream() ? "[EOS]" : "[]";
}

final StringBuilder sb = new StringBuilder();
if (headers.isEndOfStream()) {
sb.append("[EOS], ");

Check warning on line 49 in core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java#L49

Added line #L49 was not covered by tests
} else {
sb.append('[');
}

for (Map.Entry<AsciiString, String> e : headers) {
final String header = e.getKey().toString();
if (maskHeaders.contains(header)) {
sb.append(header).append('=').append(mask.apply(e.getValue())).append(", ");
} else {
sb.append(header).append('=').append(e.getValue()).append(", ");
}
}

sb.setCharAt(sb.length() - 2, ']');
return sb.substring(0, sb.length() - 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import java.util.Set;
import java.util.function.Function;

/**
* A builder implementation for {@link TextHeadersSanitizer}.
*/
public final class TextHeadersSanitizerBuilder extends AbstractHeadersSanitizerBuilder<String> {

/**
* Sets the {@link Set} which includes headers to mask before logging.
*/
@Override
public TextHeadersSanitizerBuilder maskHeaders(String... headers) {
return (TextHeadersSanitizerBuilder) super.maskHeaders(headers);
}

/**
* Sets the {@link Set} which includes headers to mask before logging.
*/
@Override
public TextHeadersSanitizerBuilder maskHeaders(Iterable<String> headers) {
return (TextHeadersSanitizerBuilder) super.maskHeaders(headers);

Check warning on line 40 in core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java#L40

Added line #L40 was not covered by tests
}

/**
* Sets the {@link Function} to use to mask headers before logging.
*/
@Override
public TextHeadersSanitizerBuilder mask(Function<String, String> mask) {
return (TextHeadersSanitizerBuilder) super.mask(mask);
}

/**
* Returns a newly created text {@link HeadersSanitizer} based on the properties of this builder.
*/
public TextHeadersSanitizer build() {
return new TextHeadersSanitizer(maskHeaders(), mask());
}
}
Loading