Skip to content

Allow excluding traffic by matching headers #5

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

Merged
merged 1 commit into from
May 26, 2023
Merged
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ openapi.validation.specification-file-path=folder/my-spec.json

# Comma separated list of paths to be excluded from validation. Default is no excluded paths
openapi.validation.excluded-paths=/_readiness,/_liveness,/_metrics
# Allows to exclude requests based on headers. Default is no excluded headers.
# Each entry is the header plus a matching regex. The regex is case insensitive.
openapi.validation.excluded-headers[0]=User-Agent: .*(bingbot|googlebot).*

# Throttle the validation reporting (logs & metrics) to a maximum of 1 log/metric per 10 seconds.
# Default is null which results in no throttling.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.getyourguide.openapi.validation.api.exclusions;

import java.util.regex.Pattern;

public record ExcludedHeader(String headerName, Pattern headerValuePattern) { }
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.getyourguide.openapi.validation.api.selector;

import com.getyourguide.openapi.validation.api.exclusions.ExcludedHeader;
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
import com.getyourguide.openapi.validation.api.model.ResponseMetaData;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

Expand All @@ -11,21 +14,24 @@ public class DefaultTrafficSelector implements TrafficSelector {

private final double sampleRate;
private final Set<String> excludedPaths;
private final List<ExcludedHeader> excludedHeaders;
private final Boolean shouldFailOnRequestViolation;
private final Boolean shouldFailOnResponseViolation;

public DefaultTrafficSelector(Double sampleRate, Set<String> excludedPaths) {
this(sampleRate, excludedPaths, null, null);
public DefaultTrafficSelector(Double sampleRate, Set<String> excludedPaths, List<ExcludedHeader> excludedHeaders) {
this(sampleRate, excludedPaths, excludedHeaders, null, null);
}

public DefaultTrafficSelector(
Double sampleRate,
Set<String> excludedPaths,
List<ExcludedHeader> excludedHeaders,
Boolean shouldFailOnRequestViolation,
Boolean shouldFailOnResponseViolation
) {
this.sampleRate = sampleRate != null ? sampleRate : SAMPLE_RATE_DEFAULT;
this.excludedPaths = excludedPaths != null ? excludedPaths : Set.of();
this.excludedHeaders = excludedHeaders != null ? excludedHeaders : Collections.emptyList();
this.shouldFailOnRequestViolation = shouldFailOnRequestViolation != null ? shouldFailOnRequestViolation : false;
this.shouldFailOnResponseViolation =
shouldFailOnResponseViolation != null ? shouldFailOnResponseViolation : false;
Expand Down Expand Up @@ -65,6 +71,17 @@ public boolean shouldFailOnResponseViolation(RequestMetaData request) {
}

private boolean isExcludedRequest(RequestMetaData request) {
return isRequestExcludedByHeader(request) || isRequestExcludedByPath(request);
}

private boolean isRequestExcludedByHeader(RequestMetaData request) {
return excludedHeaders.stream().anyMatch(excludedHeader -> {
var headerValue = request.getHeaders().get(excludedHeader.headerName());
return headerValue != null && excludedHeader.headerValuePattern().matcher(headerValue).matches();

Choose a reason for hiding this comment

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

Just out of curiosity, is it possible to have headerValue == null IYO?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it is possible because we go through the list of headers one wants to exclude like x-is-bot. Then request.getHeaders().get("x-is-bot"); can return null if there is no such header sent in the request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Screenshot 2023-05-26 at 08 48 59

});
}

private boolean isRequestExcludedByPath(RequestMetaData request) {
return excludedPaths.contains(request.getUri().getPath());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.getyourguide.openapi.validation.api.selector;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.getyourguide.openapi.validation.api.exclusions.ExcludedHeader;
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;

class DefaultTrafficSelectorTest {
private final TrafficSelector trafficSelector = new DefaultTrafficSelector(
1.0,
null,
List.of(
new ExcludedHeader("User-Agent", Pattern.compile(".*(bingbot|googlebot).*", Pattern.CASE_INSENSITIVE)),
new ExcludedHeader("x-is-bot", Pattern.compile("true", Pattern.CASE_INSENSITIVE))
)
);

@Test
public void testIsExcludedByHeaderPattern() {
assertHeaderIsExcluded(true,
"user-Agent", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)");
assertHeaderIsExcluded(false,
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36");

assertHeaderIsExcluded(true, "x-is-bot", "true");
assertHeaderIsExcluded(false, "x-is-bot", "truebot");

}

private void assertHeaderIsExcluded(boolean expectedExclusion, String headerName, String headerValue) {

var request = new RequestMetaData(
"GET",
URI.create("https://api.example.com/v1/path"),
toCaseInsensitiveMap(Map.of(
"Content-Type", "application/json",
"Content-Length", "10",
headerName, headerValue
))
);
assertEquals(!expectedExclusion, trafficSelector.shouldRequestBeValidated(request));
}

private Map<String, String> toCaseInsensitiveMap(Map<String, String> map) {
var newMap = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
newMap.putAll(map);
return newMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import static com.getyourguide.openapi.validation.OpenApiValidationApplicationProperties.PROPERTY_PREFIX;

import com.getyourguide.openapi.validation.api.exclusions.ExcludedHeader;
import com.getyourguide.openapi.validation.api.metrics.MetricTag;
import com.getyourguide.openapi.validation.util.CommaSeparatedStringsUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -27,6 +31,7 @@ public class OpenApiValidationApplicationProperties {
private String validationReportMetricName;
private String validationReportMetricAdditionalTags;
private String excludedPaths;
private List<String> excludedHeaders;
private Boolean shouldFailOnRequestViolation;
private Boolean shouldFailOnResponseViolation;

Expand All @@ -49,4 +54,21 @@ public List<MetricTag> getValidationReportMetricAdditionalTags() {
public Set<String> getExcludedPathsAsSet() {
return CommaSeparatedStringsUtil.convertCommaSeparatedStringToSet(excludedPaths);
}

public List<ExcludedHeader> getExcludedHeaders() {
if (excludedHeaders == null) {
return Collections.emptyList();
}

return excludedHeaders.stream()
.map(header -> {
var parts = header.split(":", 2);
if (parts.length != 2) {
return null;
}
return new ExcludedHeader(parts[0].trim(), Pattern.compile(parts[1].trim(), Pattern.CASE_INSENSITIVE));
})
.filter(Objects::nonNull)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public TrafficSelector defaultTrafficSelector() {
return new DefaultTrafficSelector(
properties.getSampleRate(),
properties.getExcludedPathsAsSet(),
properties.getExcludedHeaders(),
properties.getShouldFailOnRequestViolation(),
properties.getShouldFailOnResponseViolation()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"type": "java.lang.String",
"description": "Comma separated list of paths to be excluded from validation. Default is no excluded paths."
},
{
"name": "openapi.validation.excluded-headers",
"type": "java.util.List<java.lang.String>",
"description": "Headers with patterns to be excluded. e.g. `User-Agent: .*(bingbot|googlebot).*`. Default is no excluded paths."
},
{
"name": "openapi.validation.validation-report-throttle-wait-seconds",
"type": "java.lang.Integer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.getyourguide.openapi.validation.api.exclusions.ExcludedHeader;
import com.getyourguide.openapi.validation.api.metrics.MetricTag;
import java.util.List;
import java.util.Set;
Expand All @@ -17,6 +18,7 @@ class OpenApiValidationApplicationPropertiesTest {
private static final String VALIDATION_REPORT_METRIC_NAME = "openapi_validation_error";
private static final String VALIDATION_REPORT_METRIC_ADDITONAL_TAGS_STRING = "service=payment,team=chk";
private static final String EXCLUDED_PATHS = "/_readiness,/_liveness,/_metrics";
private static final List<String> EXCLUDED_HEADERS = List.of("User-Agent: .*(bingbot|googlebot).*", "x-is-bot: true");

@Test
void getters() {
Expand All @@ -27,21 +29,38 @@ void getters() {
VALIDATION_REPORT_METRIC_NAME,
VALIDATION_REPORT_METRIC_ADDITONAL_TAGS_STRING,
EXCLUDED_PATHS,
EXCLUDED_HEADERS,
true,
false
);

assertEquals(SAMPLE_RATE, loggingConfiguration.getSampleRate());
assertEquals(SPECIFICATION_FILE_PATH, loggingConfiguration.getSpecificationFilePath());
assertEquals(VALIDATION_REPORT_THROTTLE_WAIT_SECONDS, loggingConfiguration.getValidationReportThrottleWaitSeconds());
assertEquals(VALIDATION_REPORT_THROTTLE_WAIT_SECONDS,
loggingConfiguration.getValidationReportThrottleWaitSeconds());
assertEquals(VALIDATION_REPORT_METRIC_NAME, loggingConfiguration.getValidationReportMetricName());
assertEquals(
List.of(new MetricTag("service", "payment"), new MetricTag("team", "chk")),
loggingConfiguration.getValidationReportMetricAdditionalTags()
);
assertEquals(EXCLUDED_PATHS, loggingConfiguration.getExcludedPaths());
assertEquals(Set.of("/_readiness","/_liveness","/_metrics"), loggingConfiguration.getExcludedPathsAsSet());
assertExcludedHeaders(loggingConfiguration.getExcludedHeaders());
assertEquals(Set.of("/_readiness", "/_liveness", "/_metrics"), loggingConfiguration.getExcludedPathsAsSet());
assertTrue(loggingConfiguration.getShouldFailOnRequestViolation());
assertFalse(loggingConfiguration.getShouldFailOnResponseViolation());
}

private void assertExcludedHeaders(List<ExcludedHeader> excludedHeaders) {
assertEquals(EXCLUDED_HEADERS.size(), excludedHeaders.size());
for (int i = 0; i < EXCLUDED_HEADERS.size(); i++) {
assertExcludedHeader(excludedHeaders, i);
}
}

private static void assertExcludedHeader(List<ExcludedHeader> excludedHeaders, int index) {
var excludedHeader = EXCLUDED_HEADERS.get(index);
var parts = excludedHeader.split(":");
assertEquals(parts[0].trim(), excludedHeaders.get(index).headerName());
assertEquals(parts[1].trim(), excludedHeaders.get(index).headerValuePattern().pattern());
}
}