FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Backport 26.1: validate circular default values by andimarek · Pull Request #4450 · graphql-java/graphql-java · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .groovy  (3) .java  (4) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inp
!validationUtil.isValidLiteralValue((Value<?>) defaultValue.getValue(), inputObjectField.getType(), schema, graphQLContext, Locale.getDefault())) {
invalid = true;
} else if (defaultValue.isExternal() &&
!isValidExternalValue(schema, defaultValue.getValue(), inputObjectField.getType(), graphQLContext)) {
!isValidExternalValue(schema, defaultValue.getValue(), inputObjectField.getType(), graphQLContext, errorCollector)) {
invalid = true;
}
if (invalid) {
Expand All @@ -70,7 +70,7 @@ public TraversalControl visitGraphQLArgument(GraphQLArgument argument, Traverser
!validationUtil.isValidLiteralValue((Value<?>) defaultValue.getValue(), argument.getType(), schema, graphQLContext, Locale.getDefault())) {
invalid = true;
} else if (defaultValue.isExternal() &&
!isValidExternalValue(schema, defaultValue.getValue(), argument.getType(), graphQLContext)) {
!isValidExternalValue(schema, defaultValue.getValue(), argument.getType(), graphQLContext, errorCollector)) {
invalid = true;
}
if (invalid) {
Expand All @@ -80,7 +80,17 @@ public TraversalControl visitGraphQLArgument(GraphQLArgument argument, Traverser
return TraversalControl.CONTINUE;
}

private boolean isValidExternalValue(GraphQLSchema schema, Object externalValue, GraphQLInputType type, GraphQLContext graphQLContext) {
private boolean isValidExternalValue(
GraphQLSchema schema,
Object externalValue,
GraphQLInputType type,
GraphQLContext graphQLContext,
SchemaValidationErrorCollector errorCollector
) {
// Coercion expands nested field defaults. Avoid recursing into a cycle that has already made the schema invalid.
if (errorCollector.containsValidationError(SchemaValidationErrorType.DefaultValueCircularRef)) {
return true;
}
try {
ValuesResolver.externalValueToInternalValue(schema.getCodeRegistry().getFieldVisibility(), externalValue, type, graphQLContext, Locale.getDefault());
return true;
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package graphql.schema.validation;

import graphql.Internal;
import graphql.language.ArrayValue;
import graphql.language.ObjectField;
import graphql.language.ObjectValue;
import graphql.language.Value;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLSchemaElement;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeVisitorStub;
import graphql.schema.InputValueWithState;
import graphql.util.FpKit;
import graphql.util.TraversalControl;
import graphql.util.TraverserContext;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static graphql.schema.GraphQLTypeUtil.unwrapAll;

/**
* Validates that {@code InputObjectDefaultValueHasCycle(inputObject)} is {@code false}
* for every input object type, as required by the Input Object type validation rules
* in the GraphQL specification.
* <br>
* For example, consider this type configuration:
* <code>
* input A { b:B = {} }
* input B { a:A = {} }
* </code>
* <br>
* The default values used in these types form a cycle that can create an infinitely large
* value. This validator rejects default values that can create these kinds of cycles.
*
* @see <a href="https://spec.graphql.org/draft/#sec-Input-Objects.Type-Validation">Input Objects Type Validation</a>
*/
@Internal
public class NoDefaultValueCircularRefs extends GraphQLTypeVisitorStub {

private final Set<String> checkedFields = new LinkedHashSet<>();
private final LinkedHashSet<String> fieldPath = new LinkedHashSet<>();

@Override
public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType type, TraverserContext<GraphQLSchemaElement> context) {
checkType(type, getErrorCollector(context));
return TraversalControl.CONTINUE;
}

@Override
public TraversalControl visitGraphQLArgument(GraphQLArgument argument, TraverserContext<GraphQLSchemaElement> context) {
GraphQLType namedType = unwrapAll(argument.getType());
if (namedType instanceof GraphQLInputObjectType) {
checkType((GraphQLInputObjectType) namedType, getErrorCollector(context));
}
return TraversalControl.CONTINUE;
}

private void checkType(GraphQLInputObjectType type, SchemaValidationErrorCollector errorCollector) {
for (GraphQLInputObjectField field : type.getFieldDefinitions()) {
GraphQLInputObjectType fieldType = getInputObjectType(field);
if (fieldType == null) {
continue;
}
checkFieldDefaultValue(field, fieldType, type.getName(), errorCollector);
}
}

private void checkValue(
GraphQLInputObjectType inputObject,
@Nullable Object value,
SchemaValidationErrorCollector errorCollector
) {
if (value == null) {
return;
}
if (value instanceof ArrayValue) {
for (Value<?> itemValue : ((ArrayValue) value).getValues()) {
checkValue(inputObject, itemValue, errorCollector);
}
return;
}
if (FpKit.isIterable(value)) {
for (Object itemValue : FpKit.toIterable(value)) {
checkValue(inputObject, itemValue, errorCollector);
}
return;
}

Map<?, ?> valueMap = getValueMap(value);
if (valueMap == null) {
return;
}
checkObjectValue(inputObject, valueMap, errorCollector);
}

private void checkObjectValue(
GraphQLInputObjectType inputObject,
Map<?, ?> valueMap,
SchemaValidationErrorCollector errorCollector
) {
for (GraphQLInputObjectField field : inputObject.getFieldDefinitions()) {
boolean hasValue = valueMap.containsKey(field.getName());
if (!hasValue && field.getInputFieldDefaultValue().isNotSet()) {
continue;
}

GraphQLInputObjectType fieldType = getInputObjectType(field);
if (fieldType == null) {
continue;
}
if (hasValue) {
checkValue(fieldType, valueMap.get(field.getName()), errorCollector);
continue;
}
checkFieldDefaultValue(field, fieldType, inputObject.getName(), errorCollector);
}
}

private void checkFieldDefaultValue(
GraphQLInputObjectField field,
GraphQLInputObjectType fieldType,
String parentTypeName,
SchemaValidationErrorCollector errorCollector
) {
InputValueWithState defaultValue = field.getInputFieldDefaultValue();
if (!defaultValue.isLiteral() && !defaultValue.isExternal()) {
return;
}

String coordinate = parentTypeName + "." + field.getName();
if (fieldPath.contains(coordinate)) {
addError(coordinate, errorCollector);
return;
}
if (!checkedFields.add(coordinate)) {
return;
}

fieldPath.add(coordinate);
checkValue(fieldType, defaultValue.getValue(), errorCollector);
fieldPath.remove(coordinate);
}

private void addError(String coordinate, SchemaValidationErrorCollector errorCollector) {
List<String> path = new ArrayList<>(fieldPath);
List<String> intermediaries = path.subList(path.indexOf(coordinate) + 1, path.size());
String via = intermediaries.isEmpty()
? ""
: " via the default values of: " + String.join(", ", intermediaries);
String message = "Invalid circular reference. The default value of Input Object field "
+ coordinate + " references itself" + via + ".";
errorCollector.addError(new SchemaValidationError(
SchemaValidationErrorType.DefaultValueCircularRef, message));
}

private @Nullable GraphQLInputObjectType getInputObjectType(GraphQLInputObjectField field) {
GraphQLType type = unwrapAll(field.getType());
if (type instanceof GraphQLInputObjectType) {
return (GraphQLInputObjectType) type;
}
return null;
}

private @Nullable Map<?, ?> getValueMap(Object value) {
if (value instanceof Map) {
return (Map<?, ?>) value;
}
if (!(value instanceof ObjectValue)) {
return null;
}

Map<String, Value<?>> valueMap = new LinkedHashMap<>();
for (ObjectField field : ((ObjectValue) value).getObjectFields()) {
valueMap.put(field.getName(), field.getValue());
}
return valueMap;
}

private SchemaValidationErrorCollector getErrorCollector(TraverserContext<GraphQLSchemaElement> context) {
return context.getVarFromParents(SchemaValidationErrorCollector.class);
}
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ public enum SchemaValidationErrorType implements SchemaValidationErrorClassifica
OneOfNotInhabited,
RequiredInputFieldCannotBeDeprecated,
RequiredFieldArgumentCannotBeDeprecated,
RequiredDirectiveArgumentCannotBeDeprecated
RequiredDirectiveArgumentCannotBeDeprecated,
DefaultValueCircularRef
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@
@Internal
public class SchemaValidator {


private final List<GraphQLTypeVisitor> rules = new ArrayList<>();

public SchemaValidator() {
public List<GraphQLTypeVisitor> getRules() {
List<GraphQLTypeVisitor> rules = new ArrayList<>();
rules.add(new NoUnbrokenInputCycles());
rules.add(new NoDefaultValueCircularRefs());
rules.add(new TypesImplementInterfaces());
rules.add(new TypeAndFieldRule());
rules.add(new DefaultValuesAreValid());
Expand All @@ -27,9 +26,6 @@ public SchemaValidator() {
rules.add(new InputAndOutputTypesUsedAppropriately());
rules.add(new OneOfInputObjectRules());
rules.add(new DeprecatedInputObjectAndArgumentsAreValid());
}

public List<GraphQLTypeVisitor> getRules() {
return rules;
}

Expand All @@ -38,7 +34,7 @@ public Set<SchemaValidationError> validateSchema(GraphQLSchema schema) {
Map<Class<?>, Object> rootVars = new LinkedHashMap<>();
rootVars.put(GraphQLSchema.class, schema);
rootVars.put(SchemaValidationErrorCollector.class, validationErrorCollector);
new SchemaTraverser().depthFirstFullSchema(rules, schema, rootVars);
new SchemaTraverser().depthFirstFullSchema(getRules(), schema, rootVars);
return validationErrorCollector.getErrors();
}

Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -1446,20 +1446,20 @@ class SchemaDiffingTest extends Specification {
def schema1 = schema('''
input I {
name: String
field: I = {name: "default name"}
field: I = {name: "default name", field: null}
}
type Query {
foo(arg: I): String
}
}
''')
def schema2 = schema('''
input I {
name: String
field: [I] = [{name: "default name"}]
field: [I] = [{name: "default name", field: null}]
}
type Query {
foo(arg: I): String
}
}
''')

when:
Expand Down
Loading
Loading

Back | FazBrowse Home | New Git URL