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 @@ -30,11 +30,16 @@
import graphql.schema.idl.errors.MissingTypeError;
import graphql.schema.idl.errors.NotAnInputTypeError;

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

import static graphql.Assert.assertNotNull;
import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION;
import static graphql.introspection.Introspection.DirectiveLocation.ENUM;
import static graphql.introspection.Introspection.DirectiveLocation.ENUM_VALUE;
Expand Down Expand Up @@ -182,6 +187,9 @@ private static boolean isNoNullArgWithoutDefaultValue(InputValueDefinition defin
}

private void commonCheck(Collection<DirectiveDefinition> directiveDefinitions, List<GraphQLError> errors) {
List<DirectiveDefinition> directiveDefinitionsList = new ArrayList<>(directiveDefinitions);
Map<String, DirectiveDefinition> directiveDefinitionsByName = getByName(directiveDefinitionsList, DirectiveDefinition::getName, mergeFirst());

directiveDefinitions.forEach(directiveDefinition -> {
assertTypeName(directiveDefinition, errors);
directiveDefinition.getInputValueDefinitions().forEach(inputValueDefinition -> {
Expand All @@ -192,6 +200,73 @@ private void commonCheck(Collection<DirectiveDefinition> directiveDefinitions, L
}
});
});
checkIndirectDirectiveCycles(directiveDefinitionsByName, errors);
}

private static Map<String, InputValueDefinition> directiveReferences(DirectiveDefinition directiveDefinition) {
Map<String, InputValueDefinition> result = new LinkedHashMap<>();
for (InputValueDefinition inputValueDefinition : directiveDefinition.getInputValueDefinitions()) {
recordDirectiveReferences(directiveDefinition, result, inputValueDefinition);
}
return result;
}

private static void recordDirectiveReferences(DirectiveDefinition directiveDefinition,
Map<String, InputValueDefinition> result,
InputValueDefinition inputValueDefinition) {
for (Directive directive : inputValueDefinition.getDirectives()) {
if (directive.getName().equals(directiveDefinition.getName())) {
continue;
}
result.putIfAbsent(directive.getName(), inputValueDefinition);
}
}

private static void checkIndirectDirectiveCycles(
Map<String, DirectiveDefinition> directiveDefinitionsByName,
List<GraphQLError> errors) {
Set<String> checked = new LinkedHashSet<>();
// Insertion order records the active recursion path for the error message.
LinkedHashSet<String> currentPath = new LinkedHashSet<>();
for (DirectiveDefinition directiveDefinition : directiveDefinitionsByName.values()) {
checkDirectiveReferencesForCycles(directiveDefinition, directiveDefinitionsByName, checked, currentPath, errors);
}
}

private static void checkDirectiveReferencesForCycles(DirectiveDefinition directiveDefinition,
Map<String, DirectiveDefinition> directiveDefinitionsByName,
Set<String> checked,
LinkedHashSet<String> currentPath,
List<GraphQLError> errors) {
String directiveName = directiveDefinition.getName();
if (checked.contains(directiveName)) {
return;
}

currentPath.add(directiveName);
for (Map.Entry<String, InputValueDefinition> reference : directiveReferences(directiveDefinition).entrySet()) {
String referencedDirectiveName = reference.getKey();
if (currentPath.contains(referencedDirectiveName)) {
DirectiveDefinition repeatedDirective = assertNotNull(directiveDefinitionsByName.get(referencedDirectiveName));
String cyclePath = directiveCyclePath(referencedDirectiveName, currentPath);
errors.add(new DirectiveIllegalReferenceError(repeatedDirective, reference.getValue(), cyclePath));
continue;
}
DirectiveDefinition referencedDirective = directiveDefinitionsByName.get(referencedDirectiveName);
if (referencedDirective != null) {
checkDirectiveReferencesForCycles(referencedDirective, directiveDefinitionsByName, checked, currentPath, errors);
}
}
currentPath.remove(directiveName);
checked.add(directiveName);
}

private static String directiveCyclePath(String repeatedDirectiveName, LinkedHashSet<String> currentPath) {
List<String> pathList = new ArrayList<>(currentPath);
int cycleStart = pathList.indexOf(repeatedDirectiveName);
List<String> cyclePath = new ArrayList<>(pathList.subList(cycleStart, pathList.size()));
cyclePath.add(repeatedDirectiveName);
return String.join(" -> ", cyclePath);
}

private static void assertTypeName(NamedNode<?> node, List<GraphQLError> errors) {
Expand Down Expand Up @@ -224,4 +299,4 @@ private static TypeDefinition<?> findTypeDefFromRegistry(String typeName, TypeDe
}
return typeRegistry.scalars().get(typeName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ public DirectiveIllegalReferenceError(DirectiveDefinition directive, NamedNode l
directive.getName(), location.getName(), lineCol(location)
));
}
}

public DirectiveIllegalReferenceError(DirectiveDefinition directive, NamedNode location, String cyclePath) {
super(directive,
String.format("'%s' must not reference itself via directive cycle '%s' on '%s''%s'",
directive.getName(), cyclePath, location.getName(), lineCol(location)
));
}
}
22 changes: 22 additions & 0 deletions src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import graphql.schema.GraphQLType
import graphql.schema.GraphQLTypeUtil
import graphql.schema.GraphQLUnionType
import graphql.schema.GraphqlTypeComparatorRegistry
import graphql.schema.idl.errors.DirectiveIllegalReferenceError
import graphql.schema.idl.errors.NotAnInputTypeError
import graphql.schema.idl.errors.NotAnOutputTypeError
import graphql.schema.idl.errors.SchemaProblem
Expand Down Expand Up @@ -2270,6 +2271,27 @@ class SchemaGeneratorTest extends Specification {
schema != null
}

def "#4201 indirect cyclical directive definitions are rejected without stack overflow"() {
given:
def registry = new SchemaParser().parse('''
directive @foo(x: Int @bar(y: 1)) on FIELD_DEFINITION | ARGUMENT_DEFINITION
directive @bar(y: Int @foo(x: 2)) on FIELD_DEFINITION | ARGUMENT_DEFINITION

type Query {
field: String @foo(x: 10) @bar(y: 20)
}
''')

when:
UnExecutableSchemaGenerator.makeUnExecutableSchema(registry)

then:
def e = thrown(SchemaProblem)
e.errors.size() == 1
e.errors.get(0) instanceof DirectiveIllegalReferenceError
e.errors.get(0).getMessage().contains("'foo' must not reference itself via directive cycle 'foo -> bar -> foo'")
}

def "code registry default data fetcher is respected"() {
def sdl = '''
type Query {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,66 @@ class SchemaTypeDirectivesCheckerTest extends Specification {
errors.get(0).getMessage() == "'invalidExample' must not reference itself on 'arg''[@2:39]'"
}

def "directive must not indirectly reference itself - #name"() {
given:
def registry = parse(spec)
def errors = []

when:
new SchemaTypeDirectivesChecker(registry, RuntimeWiring.newRuntimeWiring().build()).checkTypeDirectives(errors)

then:
errors.size() == 1
errors.get(0) instanceof DirectiveIllegalReferenceError
errors.get(0).getMessage().contains(cycleMessage)

where:
name << ["two directives", "three directives"]
spec << [
'''
directive @foo(arg: String @bar) on ARGUMENT_DEFINITION
directive @bar(arg: String @foo) on ARGUMENT_DEFINITION

type Query {
f1 : String
}
''',
'''
directive @dirA(x: Int @dirB(y: 1)) on ARGUMENT_DEFINITION
directive @dirB(y: Int @dirC(z: 2)) on ARGUMENT_DEFINITION
directive @dirC(z: Int @dirA(x: 3)) on ARGUMENT_DEFINITION

type Query {
f1 : String
}
'''
]
cycleMessage << [
"'foo' must not reference itself via directive cycle 'foo -> bar -> foo'",
"'dirA' must not reference itself via directive cycle 'dirA -> dirB -> dirC -> dirA'"
]
}

def "acyclic directive references are allowed"() {
given:
def registry = parse('''
directive @foo(arg: String @bar) on ARGUMENT_DEFINITION
directive @bar(arg: String @baz) on ARGUMENT_DEFINITION
directive @baz on ARGUMENT_DEFINITION

type Query {
f1 : String
}
''')
def errors = []

when:
new SchemaTypeDirectivesChecker(registry, RuntimeWiring.newRuntimeWiring().build()).checkTypeDirectives(errors)

then:
errors.isEmpty()
}

def "directive must not begin with '__'"() {
given:
def spec = '''
Expand Down
Loading