SwaggerResponseValidator.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2024 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.utils.openapi.validator.swagger;
import static com.atlassian.oai.validator.util.ContentTypeUtils.findMostSpecificMatch;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import org.openspcoop2.utils.openapi.validator.OpenapiLibraryValidatorConfig;
import com.atlassian.oai.validator.interaction.response.ResponseValidator;
import com.atlassian.oai.validator.model.ApiOperation;
import com.atlassian.oai.validator.model.Body;
import com.atlassian.oai.validator.model.Response;
import com.atlassian.oai.validator.report.LevelResolver;
import com.atlassian.oai.validator.report.MessageResolver;
import com.atlassian.oai.validator.report.ValidationReport;
import com.atlassian.oai.validator.report.ValidationReport.Level;
import com.atlassian.oai.validator.schema.SchemaValidator;
import com.atlassian.oai.validator.schema.transform.AdditionalPropertiesInjectionTransformer;
import com.atlassian.oai.validator.util.ContentTypeUtils;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.responses.ApiResponse;
/**
* SwaggerResponseValidator
*
* @author $Author$
* @version $Rev$, $Date$
*
*/
public class SwaggerResponseValidator {
private final MessageResolver normalValidatorMessages;
private final SchemaValidator normalSchemaValidator;
private final ResponseValidator normalValidator;
private final MessageResolver fileValidatorMessages;
private final SchemaValidator fileSchemaValidator;
private final ResponseValidator fileValidator;
private final boolean validateWildcardSubtypeAsJson;
public SwaggerResponseValidator(OpenAPI openApi, OpenapiLibraryValidatorConfig config) {
this.validateWildcardSubtypeAsJson = config.isValidateWildcardSubtypeAsJson();
var errorLevelResolverBuilder = getLevelResolverBuilder(config);
this.normalValidatorMessages = new MessageResolver(errorLevelResolverBuilder.build());
this.normalSchemaValidator = new SchemaValidator(openApi, this.normalValidatorMessages);
if (!config.isSwaggerRequestValidator_InjectingAdditionalPropertiesFalse()) {
var transformers = this.normalSchemaValidator.transformers;
this.normalSchemaValidator.transformers = transformers.stream()
.filter( t -> t != AdditionalPropertiesInjectionTransformer.getInstance())
.collect(Collectors.toList());
}
this.normalValidator = new ResponseValidator(this.normalSchemaValidator, this.normalValidatorMessages, openApi, Arrays.asList());
// file validator, non entra nel merito del body.
errorLevelResolverBuilder.withLevel("validation.response.body", Level.IGNORE);
errorLevelResolverBuilder.withLevel("validation.response.body.missing", Level.ERROR);
this.fileValidatorMessages = new MessageResolver(errorLevelResolverBuilder.build());
this.fileSchemaValidator = new SchemaValidator(openApi, this.fileValidatorMessages);
if (!config.isSwaggerRequestValidator_InjectingAdditionalPropertiesFalse()) {
var transformers = this.fileSchemaValidator.transformers;
this.fileSchemaValidator.transformers = transformers.stream()
.filter( t -> t != AdditionalPropertiesInjectionTransformer.getInstance())
.collect(Collectors.toList());
}
this.fileValidator = new ResponseValidator(this.fileSchemaValidator, this.fileValidatorMessages, openApi, Arrays.asList());
}
public ValidationReport validateResponse(Response response, ApiOperation apiOperation) {
final ApiResponse responseSchema = getApiResponse(response, apiOperation);
if (responseSchema.getContent() == null) {
return this.normalValidator.validateResponse(response, apiOperation);
}
// VALIDAZIONE CUSTOM 1:
// Controllo che il content-type nullo non sia ammesso quando è richiesto un content
// Le risposte vuote sono quelle senza content, se il content c'è, ci deve essere
// anche il mediaType
Content contentSchema = responseSchema.getContent();
if (contentSchema != null && !contentSchema.isEmpty()) {
if (response.getContentType().isEmpty()) {
return ValidationReport.singleton(
this.normalValidatorMessages.create(
"validation.response.contentType.notAllowed",
"[RESPONSE] Required Content-Type is missing"
));
}
}
if (response.getResponseBody().isPresent() && response.getContentType().isEmpty()) {
return ValidationReport.singleton(
this.normalValidatorMessages.create(
"validation.response.contentType.notAllowed",
"[RESPONSE] Empty Content-Type not allowed if body is present"
));
}
// VALIDAZIONE CUSTOM 2
// Se lo schema del response body è: type: string, format: binary, ovvero un BinarySchema,
// allora al più valida che il body sia un json e valida tutto il resto della richiesta
// Se invece il format è base64 controlla che sia in base64
final Optional<String> mostSpecificMatch = findMostSpecificMatch(response, responseSchema.getContent().keySet());
if (!mostSpecificMatch.isPresent()) {
// Se non matcho il content-type, lascio fare al normal validator,
return this.normalValidator.validateResponse(response, apiOperation);
}
final MediaType mediaType = responseSchema.getContent().get(mostSpecificMatch.get());
com.google.common.net.MediaType responseMediaType = com.google.common.net.MediaType.parse(mostSpecificMatch.get());
final Body responseBody = response.getResponseBody().orElse(null);
ValidationReport report = ValidationReport.empty();
// Validazione schema string format binary
if (SwaggerValidatorUtils.isBinarySchemaFile(mediaType.getSchema()) && responseBody != null) {
if (ContentTypeUtils.isJsonContentType(response)) {
report = report.merge(SwaggerValidatorUtils.validateJsonFormat(responseBody,this.normalValidatorMessages,false))
.merge(this.fileValidator.validateResponse(response, apiOperation));
}
}
// validazione schema string format base64
else if (SwaggerValidatorUtils.isBase64SchemaFile(mediaType.getSchema()) && responseBody != null) {
report = report.merge(SwaggerValidatorUtils.validateBase64Body(responseBody,this.normalValidatorMessages,false))
.merge(this.fileValidator.validateResponse(response, apiOperation));
}
// validazione schema subtype *
else if (this.validateWildcardSubtypeAsJson && responseMediaType.subtype().equals("*")) {
report = this.normalSchemaValidator
.validate( () -> response.getResponseBody().get().toJsonNode(), mediaType.getSchema(), "response.body")
.merge(this.normalValidator.validateResponse(response, apiOperation));
}
else {
report = this.normalValidator.validateResponse(response, apiOperation);
}
return report;
}
private static LevelResolver.Builder getLevelResolverBuilder(OpenapiLibraryValidatorConfig config) {
var errorLevelResolver = LevelResolver.create();
// Il LevelResolver serve a gestire il livello di serietà dei messaggi
// Di default il LevelResolver porta segnala ogni errore di validazione come
// un ERROR, quindi dobbiamo disattivarli selettivamente.
// Le chiavi da usare per il LevelResolver sono nel progetto swagger-validator
// sotto src/main/resources/messages.properties
// Config Response
if(!config.isValidateResponseHeaders()) {
errorLevelResolver.withLevel("validation.response.parameter.header", Level.IGNORE);
}
if(!config.isValidateResponseBody()) {
errorLevelResolver.withLevel("validation.response.body", Level.IGNORE);
}
return errorLevelResolver;
}
private ApiResponse getApiResponse(final Response response, final ApiOperation apiOperation) {
final ApiResponse apiResponse = apiOperation.getOperation().getResponses()
.get(Integer.toString(response.getStatus()));
if (apiResponse == null) {
return apiOperation.getOperation().getResponses().get("default"); // try the default response
}
return apiResponse;
}
}