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

Support fail on request/response violation #3

Merged
merged 8 commits into from
May 25, 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ openapi.validation.sample-rate=1.0

# Custom location of specification file within resources or filesystem.
openapi.validation.specification-file-path=/tmp/openapi-spec/openapi.json
# If it is within src/main/resources/folder/my-spec.json use
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
Expand All @@ -98,6 +100,10 @@ openapi.validation.validation-report-metric-name=openapi.violation
# Add additional tags to be logged with metrics. They should be in the format {KEY}={VALUE},{KEY}={VALUE}
# Default is no additional tags.
openapi.validation.validation-report-metric-additional-tags=service=example,team=chk

# Fail requests on request/response violations. Defaults to false.
openapi.validation.should-fail-on-request-violation=true
openapi.validation.should-fail-on-response-violation=true
```

### DataDog metrics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.Map;
import lombok.NonNull;

// TODO CHK-8357 can we get rid of this one?
public interface LoggerExtension {
Closeable addToLoggingContext(@NonNull Map<String, String> newTags);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.getyourguide.openapi.validation.api.model;

public enum ValidationResult { VALID, INVALID, NOT_APPLICABLE }
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,24 @@ public class DefaultTrafficSelector implements TrafficSelector {

private final double sampleRate;
private final Set<String> excludedPaths;
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,
Boolean shouldFailOnRequestViolation,
Boolean shouldFailOnResponseViolation
) {
this.sampleRate = sampleRate != null ? sampleRate : SAMPLE_RATE_DEFAULT;
this.excludedPaths = excludedPaths != null ? excludedPaths : Set.of();
this.shouldFailOnRequestViolation = shouldFailOnRequestViolation != null ? shouldFailOnRequestViolation : false;
this.shouldFailOnResponseViolation =
shouldFailOnResponseViolation != null ? shouldFailOnResponseViolation : false;
}

@Override
Expand All @@ -40,6 +54,16 @@ public boolean canResponseBeValidated(RequestMetaData request, ResponseMetaData
&& isContentTypeSupported(response.getContentType());
}

@Override
public boolean shouldFailOnRequestViolation(RequestMetaData request) {
return shouldFailOnRequestViolation;
}

@Override
public boolean shouldFailOnResponseViolation(RequestMetaData request) {
return shouldFailOnResponseViolation;
}

private boolean isExcludedRequest(RequestMetaData request) {
return excludedPaths.contains(request.getUri().getPath());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ default boolean canRequestBeValidated(RequestMetaData request) {
default boolean canResponseBeValidated(RequestMetaData request, ResponseMetaData response) {
return true;
}

default boolean shouldFailOnRequestViolation(RequestMetaData request) {
return false;
}

default boolean shouldFailOnResponseViolation(RequestMetaData request) {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import com.atlassian.oai.validator.model.Request;
import com.atlassian.oai.validator.model.SimpleRequest;
import com.atlassian.oai.validator.model.SimpleResponse;
import com.atlassian.oai.validator.report.ValidationReport;
import com.getyourguide.openapi.validation.api.model.Direction;
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
import com.getyourguide.openapi.validation.api.model.ResponseMetaData;
import com.getyourguide.openapi.validation.api.model.ValidationResult;
import com.getyourguide.openapi.validation.api.model.ValidatorConfiguration;
import com.getyourguide.openapi.validation.core.validator.OpenApiInteractionValidatorWrapper;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -39,13 +41,15 @@ public void validateResponseObjectAsync(final RequestMetaData request, ResponseM
threadPool.execute(() -> validateResponseObject(request, response, responseBody));
}

private void validateRequestObject(final RequestMetaData request, String requestBody) {
public ValidationResult validateRequestObject(final RequestMetaData request, String requestBody) {
try {
var simpleRequest = buildSimpleRequest(request, requestBody);
var result = validator.validateRequest(simpleRequest);
validationReportHandler.handleValidationReport(request, Direction.REQUEST, requestBody, result);
return buildValidationResult(result);
} catch (Exception e) {
log.error("Could not validate request", e);
return ValidationResult.NOT_APPLICABLE;
}
}

Expand All @@ -60,7 +64,11 @@ private static SimpleRequest buildSimpleRequest(RequestMetaData request, String
return requestBuilder.build();
}

private void validateResponseObject(final RequestMetaData request, ResponseMetaData response, final String responseBody) {
public ValidationResult validateResponseObject(
final RequestMetaData request,
ResponseMetaData response,
final String responseBody
) {
try {
var responseBuilder = new SimpleResponse.Builder(response.getStatusCode());
response.getHeaders().forEach(responseBuilder::withHeader);
Expand All @@ -75,8 +83,22 @@ private void validateResponseObject(final RequestMetaData request, ResponseMetaD
responseBuilder.build()
);
validationReportHandler.handleValidationReport(request, Direction.RESPONSE, responseBody, result);
return buildValidationResult(result);
} catch (Exception e) {
log.error("Could not validate response", e);
return ValidationResult.NOT_APPLICABLE;
}
}

private ValidationResult buildValidationResult(ValidationReport validationReport) {
if (validationReport == null) {
return ValidationResult.NOT_APPLICABLE;
}

if (validationReport.getMessages().isEmpty()) {
return ValidationResult.VALID;
}

return ValidationResult.INVALID;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class OpenApiValidationApplicationProperties {
private String validationReportMetricName;
private String validationReportMetricAdditionalTags;
private String excludedPaths;
private Boolean shouldFailOnRequestViolation;
private Boolean shouldFailOnResponseViolation;

public List<MetricTag> getValidationReportMetricAdditionalTags() {
if (validationReportMetricAdditionalTags == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ public class FallbackLibraryAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TrafficSelector defaultTrafficSelector() {
return new DefaultTrafficSelector(properties.getSampleRate(), properties.getExcludedPathsAsSet());
return new DefaultTrafficSelector(
properties.getSampleRate(),
properties.getExcludedPathsAsSet(),
properties.getShouldFailOnRequestViolation(),
properties.getShouldFailOnResponseViolation()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
"name": "openapi.validation.validation-report-metric-additional-tags",
"type": "java.lang.String",
"description": "Additional tags to be logged with metrics. They should be in the format {KEY}={VALUE},{KEY}={VALUE}. Default is no additional tags."
},
{
"name": "openapi.validation.should-fail-on-request-violation",
"type": "java.lang.Boolean",
"description": "If set to true the request will fail in case a request violation occurs. Defaults to false."
},
{
"name": "openapi.validation.should-fail-on-response-violation",
"type": "java.lang.Boolean",
"description": "If set to true the request will fail in case a response violation occurs. Defaults to false."
}
]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.getyourguide.openapi.validation;

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

import com.getyourguide.openapi.validation.api.metrics.MetricTag;
import java.util.List;
Expand All @@ -24,7 +26,9 @@ void getters() {
VALIDATION_REPORT_THROTTLE_WAIT_SECONDS,
VALIDATION_REPORT_METRIC_NAME,
VALIDATION_REPORT_METRIC_ADDITONAL_TAGS_STRING,
EXCLUDED_PATHS
EXCLUDED_PATHS,
true,
false
);

assertEquals(SAMPLE_RATE, loggingConfiguration.getSampleRate());
Expand All @@ -37,5 +41,7 @@ void getters() {
);
assertEquals(EXCLUDED_PATHS, loggingConfiguration.getExcludedPaths());
assertEquals(Set.of("/_readiness","/_liveness","/_metrics"), loggingConfiguration.getExcludedPathsAsSet());
assertTrue(loggingConfiguration.getShouldFailOnRequestViolation());
assertFalse(loggingConfiguration.getShouldFailOnResponseViolation());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.getyourguide.openapi.validation.api.selector.TrafficSelector;
import com.getyourguide.openapi.validation.core.OpenApiRequestValidator;
import com.getyourguide.openapi.validation.factory.ContentCachingWrapperFactory;
import com.getyourguide.openapi.validation.factory.ServletMetaDataFactory;
import com.getyourguide.openapi.validation.filter.OpenApiValidationHttpFilter;
import lombok.AllArgsConstructor;
Expand All @@ -21,13 +22,20 @@ public ServletMetaDataFactory servletMetaDataFactory() {
return new ServletMetaDataFactory();
}

@Bean
@ConditionalOnWebApplication(type = Type.SERVLET)
public ContentCachingWrapperFactory contentCachingWrapperFactory() {
return new ContentCachingWrapperFactory();
}

@Bean
@ConditionalOnWebApplication(type = Type.SERVLET)
public OpenApiValidationHttpFilter openApiValidationHttpFilter(
OpenApiRequestValidator validator,
TrafficSelector trafficSelector,
ServletMetaDataFactory metaDataFactory
ServletMetaDataFactory metaDataFactory,
ContentCachingWrapperFactory contentCachingWrapperFactory
) {
return new OpenApiValidationHttpFilter(validator, trafficSelector, metaDataFactory);
return new OpenApiValidationHttpFilter(validator, trafficSelector, metaDataFactory, contentCachingWrapperFactory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.getyourguide.openapi.validation.factory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

public class ContentCachingWrapperFactory {
public ContentCachingRequestWrapper buildContentCachingRequestWrapper(HttpServletRequest request) {
return new ContentCachingRequestWrapper(request);
}

public ContentCachingResponseWrapper buildContentCachingResponseWrapper(HttpServletResponse response) {
return new ContentCachingResponseWrapper(response);
}
}
Loading