Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,15 @@ public SchemaValidator() {

/**
* Build a new validator for the given API specification.
* Components are snapshotted at construction. Later changes to the API overlay are not observed;
* construct a new validator after changing the specification.
*
* @param api The API to build the validator for. If provided, is used to retrieve schemas in components
* for use in references.
*/
public SchemaValidator(final OpenApi3 api) {
this.jsonNode = api == null ? null : Overlay.toJson((OpenApi3Impl)api).get(COMPONENTS_FIELD);
JsonNode components = api == null ? null : Overlay.toJson((OpenApi3Impl)api).get(COMPONENTS_FIELD);
this.jsonNode = components == null ? null : components.deepCopy();
this.pathType = PathType.JSON_POINTER;
}

Expand Down Expand Up @@ -114,10 +117,11 @@ private Status doValidate(final JsonNode value, final JsonNode schema, boolean t

List<Error> processingReport = null;
try {
JsonNode validationSchema = schema.deepCopy();
if(jsonNode != null) {
((ObjectNode)schema).set(COMPONENTS_FIELD, jsonNode);
((ObjectNode)validationSchema).set(COMPONENTS_FIELD, jsonNode.deepCopy());
}
Schema jsonSchema = getRegistry(typeLoose, nullableKeywordEnabled, validationPathType).getSchema(schema);
Schema jsonSchema = getRegistry(typeLoose, nullableKeywordEnabled, validationPathType).getSchema(validationSchema);
processingReport = jsonSchema.validate(value);
} catch (Exception e) {
LOG.error("Unable to validate the value against the OpenAPI schema", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.networknt.aws.lambda.validator;

import com.networknt.jackson.JacksonDefaults;
import com.networknt.oas.OpenApiParser;
import com.networknt.oas.model.OpenApi3;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.JsonNode;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

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

class SchemaIsolationTest {
private static OpenApi3 api(String type) throws Exception {
// Resolution base for the inline document; the parser does not read this file.
var documentUrl = Path.of("schema-isolation.json").toUri().toURL();
return (OpenApi3) new OpenApiParser().parse("""
{"openapi":"3.0.3","info":{"title":"isolation","version":"1"},
"paths":{},"components":{"schemas":{"Value":{"type":"%s"}}}}
""".formatted(type), documentUrl, false);
}

private static JsonNode reference() {
return JacksonDefaults.mapper().readTree("{\"$ref\":\"#/components/schemas/Value\"}");
}

@Test
void validationNeverAddsComponentsToTheCallerSchema() throws Exception {
var validator = new SchemaValidator(api("integer"));
var schema = reference();
var original = schema.deepCopy();
assertNull(validator.validate(JacksonDefaults.mapper().readTree("7"), schema, false, false));
assertNotNull(validator.validate(JacksonDefaults.mapper().readTree("\"invalid\""), schema, false, false));
assertEquals(original, schema);
assertFalse(schema.has("components"));
}

@Test
void callerOwnedComponentsRemainUnchanged() throws Exception {
var schema = JacksonDefaults.mapper().readTree("""
{"$ref":"#/components/schemas/Value",
"components":{"schemas":{"Value":{"type":"string"}}}}
""");
var original = schema.deepCopy();
var validator = new SchemaValidator(api("integer"));
// API components still take precedence for this validation, as before.
assertNull(validator.validate(JacksonDefaults.mapper().readTree("7"), schema, false, false));
assertEquals(original, schema);
}

@Test
void concurrentValidationKeepsCallerInputsIsolated() throws Exception {
var integerValidator = new SchemaValidator(api("integer"));
var stringValidator = new SchemaValidator(api("string"));
var schema = reference();
var original = schema.deepCopy();
var number = JacksonDefaults.mapper().readTree("7");
var text = JacksonDefaults.mapper().readTree("\"7\"");
var tasks = new ArrayList<Callable<Void>>();
for (int i = 0; i < 40; i++) {
tasks.add(() -> {
assertNull(integerValidator.validate(number, schema, false, false));
assertNotNull(stringValidator.validate(number, schema, false, false));
assertNull(stringValidator.validate(text, schema, false, false));
assertNotNull(integerValidator.validate(text, schema, false, false));
assertNull(integerValidator.validate(text, schema, true, false));
return null;
});
}
var executor = Executors.newFixedThreadPool(4);
try {
for (var future : executor.invokeAll(tasks, 10, TimeUnit.SECONDS)) future.get(1, TimeUnit.SECONDS);
} finally {
executor.shutdownNow();
assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS));
}
assertEquals(original, schema);
}
}
Loading