Skip to content

Commit 9a69970

Browse files
committed
Add validation at document parse time
1 parent f8673e6 commit 9a69970

5 files changed

Lines changed: 82 additions & 12 deletions

File tree

src/main/java/graphql/validation/ValidationErrorType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ public enum ValidationErrorType implements ValidationErrorClassification {
4343
NullValueForNonNullArgument,
4444
SubscriptionMultipleRootFields,
4545
SubscriptionIntrospectionRootField,
46-
UniqueObjectFieldName
46+
UniqueObjectFieldName,
47+
UnknownOperation
4748
}

src/main/java/graphql/validation/Validator.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package graphql.validation;
22

33

4-
import graphql.ExperimentalApi;
54
import graphql.Internal;
65
import graphql.i18n.I18n;
76
import graphql.language.Document;
@@ -10,6 +9,7 @@
109
import graphql.validation.rules.DeferDirectiveLabel;
1110
import graphql.validation.rules.DeferDirectiveOnRootLevel;
1211
import graphql.validation.rules.DeferDirectiveOnValidOperation;
12+
import graphql.validation.rules.KnownOperationTypes;
1313
import graphql.validation.rules.UniqueObjectFieldName;
1414
import graphql.validation.rules.ExecutableDefinitions;
1515
import graphql.validation.rules.FieldsOnCorrectType;
@@ -52,7 +52,7 @@ public class Validator {
5252
* `graphql-java` will stop validation after a maximum number of validation messages has been reached. Attackers
5353
* can send pathologically invalid queries to induce a Denial of Service attack and fill memory with 10000s of errors
5454
* and burn CPU in process.
55-
*
55+
* <p>
5656
* By default, this is set to 100 errors. You can set a new JVM wide value as the maximum allowed validation errors.
5757
*
5858
* @param maxValidationErrors the maximum validation errors allow JVM wide
@@ -169,6 +169,10 @@ public List<AbstractRule> createRules(ValidationContext validationContext, Valid
169169

170170
DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector);
171171
rules.add(deferDirectiveLabel);
172+
173+
KnownOperationTypes knownOperationTypes = new KnownOperationTypes(validationContext, validationErrorCollector);
174+
rules.add(knownOperationTypes);
175+
172176
return rules;
173177
}
174178
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package graphql.validation.rules;
2+
3+
import graphql.Internal;
4+
import graphql.language.OperationDefinition;
5+
import graphql.schema.GraphQLSchema;
6+
import graphql.validation.AbstractRule;
7+
import graphql.validation.ValidationContext;
8+
import graphql.validation.ValidationErrorCollector;
9+
10+
import static graphql.validation.ValidationErrorType.UnknownOperation;
11+
12+
/**
13+
* Unique variable names
14+
* <p>
15+
* A GraphQL operation is only valid if all its variables are uniquely named.
16+
*/
17+
@Internal
18+
public class KnownOperationTypes extends AbstractRule {
19+
20+
public KnownOperationTypes(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
21+
super(validationContext, validationErrorCollector);
22+
}
23+
24+
@Override
25+
public void checkOperationDefinition(OperationDefinition operationDefinition) {
26+
OperationDefinition.Operation documentOperation = operationDefinition.getOperation();
27+
GraphQLSchema graphQLSchema = getValidationContext().getSchema();
28+
if (documentOperation == OperationDefinition.Operation.MUTATION
29+
&& graphQLSchema.getMutationType() == null) {
30+
String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", documentOperation.name());
31+
addError(UnknownOperation, operationDefinition.getSourceLocation(), message);
32+
} else if (documentOperation == OperationDefinition.Operation.SUBSCRIPTION
33+
&& graphQLSchema.getSubscriptionType() == null) {
34+
String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", documentOperation.name());
35+
addError(UnknownOperation, operationDefinition.getSourceLocation(), message);
36+
} else if (documentOperation == OperationDefinition.Operation.QUERY
37+
&& graphQLSchema.getQueryType() == null) {
38+
// This is unlikely to happen, as a validated GraphQLSchema must have a Query type by definition
39+
String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", documentOperation.name());
40+
addError(UnknownOperation, operationDefinition.getSourceLocation(), message);
41+
}
42+
}
43+
}

src/main/resources/i18n/Validation.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ KnownFragmentNames.undefinedFragment=Validation error ({0}) : Undefined fragment
3838
#
3939
KnownTypeNames.unknownType=Validation error ({0}) : Unknown type ''{1}''
4040
#
41+
KnownOperationTypes.noOperation=Validation error ({0}): The ''{1}'' operation is not supported by the schema
42+
#
4143
LoneAnonymousOperation.withOthers=Validation error ({0}) : Anonymous operation with other operations
4244
LoneAnonymousOperation.namedOperation=Validation error ({0}) : Operation ''{1}'' is following anonymous operation
4345
#

src/test/groovy/graphql/ParseAndValidateTest.groovy

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package graphql
22

33
import graphql.language.Document
4+
import graphql.language.SourceLocation
45
import graphql.parser.InvalidSyntaxException
56
import graphql.parser.Parser
6-
import graphql.schema.GraphQLSchema
77
import graphql.schema.idl.SchemaParser
8-
import graphql.schema.idl.TypeDefinitionRegistry
98
import graphql.schema.idl.UnExecutableSchemaGenerator
109
import graphql.validation.ValidationError
1110
import graphql.validation.ValidationErrorType
@@ -162,29 +161,50 @@ class ParseAndValidateTest extends Specification {
162161
!rs.errors.isEmpty() // all rules apply - we have errors
163162
}
164163

165-
def "issue 2740 - evidence of not working"() {
164+
def "validation error raised if mutation operation does not exist in schema"() {
166165
def sdl = '''
167166
type Query {
168-
myquery : String!
167+
myQuery : String!
169168
}
170169
'''
171170

172171
def registry = new SchemaParser().parse(sdl)
173172
def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry)
174-
def graphQL = GraphQL.newGraphQL(schema).build()
175-
176-
String request = "mutation MyMutation { mymutation }"
173+
String request = "mutation MyMutation { myMutation }"
177174

178175
when:
179-
def er = graphQL.execute(request)
176+
Document inputDocument = new Parser().parseDocument(request)
177+
List<ValidationError> errors = ParseAndValidate.validate(schema, inputDocument)
178+
180179
then:
181-
er.errors.size() == 1
180+
errors.size() == 1
181+
def error = errors.first()
182+
error.validationErrorType == ValidationErrorType.UnknownOperation
183+
error.message == "Validation error (UnknownOperation): The 'MUTATION' operation is not supported by the schema"
184+
error.locations == [new SourceLocation(1, 1)]
185+
}
186+
187+
def "validation error raised if subscription operation does not exist in schema"() {
188+
def sdl = '''
189+
type Query {
190+
myQuery : String!
191+
}
192+
'''
193+
194+
def registry = new SchemaParser().parse(sdl)
195+
def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry)
196+
197+
String request = "subscription MySubscription { mySubscription }"
182198

183199
when:
184200
Document inputDocument = new Parser().parseDocument(request)
185201
List<ValidationError> errors = ParseAndValidate.validate(schema, inputDocument)
186202

187203
then:
188204
errors.size() == 1
205+
def error = errors.first()
206+
error.validationErrorType == ValidationErrorType.UnknownOperation
207+
error.message == "Validation error (UnknownOperation): The 'SUBSCRIPTION' operation is not supported by the schema"
208+
error.locations == [new SourceLocation(1, 1)]
189209
}
190210
}

0 commit comments

Comments
 (0)