Skip to content
3 changes: 2 additions & 1 deletion src/main/java/graphql/validation/ValidationErrorType.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ public enum ValidationErrorType implements ValidationErrorClassification {
NullValueForNonNullArgument,
SubscriptionMultipleRootFields,
SubscriptionIntrospectionRootField,
UniqueObjectFieldName
UniqueObjectFieldName,
UnknownOperation
}
8 changes: 6 additions & 2 deletions src/main/java/graphql/validation/Validator.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package graphql.validation;


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

DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector);
rules.add(deferDirectiveLabel);

KnownOperationTypes knownOperationTypes = new KnownOperationTypes(validationContext, validationErrorCollector);
rules.add(knownOperationTypes);

return rules;
}
}
48 changes: 48 additions & 0 deletions src/main/java/graphql/validation/rules/KnownOperationTypes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package graphql.validation.rules;

import graphql.Internal;
import graphql.language.OperationDefinition;
import graphql.schema.GraphQLSchema;
import graphql.util.StringKit;
import graphql.validation.AbstractRule;
import graphql.validation.ValidationContext;
import graphql.validation.ValidationErrorCollector;

import static graphql.validation.ValidationErrorType.UnknownOperation;

/**
* Unique variable names
* <p>
* A GraphQL operation is only valid if all its variables are uniquely named.
*/
@Internal
public class KnownOperationTypes extends AbstractRule {

public KnownOperationTypes(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
super(validationContext, validationErrorCollector);
}

@Override
public void checkOperationDefinition(OperationDefinition operationDefinition) {
OperationDefinition.Operation documentOperation = operationDefinition.getOperation();
GraphQLSchema graphQLSchema = getValidationContext().getSchema();
if (documentOperation == OperationDefinition.Operation.MUTATION
&& graphQLSchema.getMutationType() == null) {
String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation));
addError(UnknownOperation, operationDefinition.getSourceLocation(), message);
} else if (documentOperation == OperationDefinition.Operation.SUBSCRIPTION
&& graphQLSchema.getSubscriptionType() == null) {
String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation));
addError(UnknownOperation, operationDefinition.getSourceLocation(), message);
} else if (documentOperation == OperationDefinition.Operation.QUERY
&& graphQLSchema.getQueryType() == null) {
// This is unlikely to happen, as a validated GraphQLSchema must have a Query type by definition
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A valid GraphQLSchema should have already been checked for a query type, I am being defensive here

String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation));
addError(UnknownOperation, operationDefinition.getSourceLocation(), message);
}
}

private String formatOperation(OperationDefinition.Operation operation) {
return StringKit.capitalize(operation.name().toLowerCase());
}
Copy link
Member Author

@dondonz dondonz Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our Operation enums are ALL CAPS and I thought that was too shouty

}
2 changes: 2 additions & 0 deletions src/main/resources/i18n/Validation.properties
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ KnownFragmentNames.undefinedFragment=Validation error ({0}) : Undefined fragment
#
KnownTypeNames.unknownType=Validation error ({0}) : Unknown type ''{1}''
#
KnownOperationTypes.noOperation=Validation error ({0}): The ''{1}'' operation is not supported by the schema
#
LoneAnonymousOperation.withOthers=Validation error ({0}) : Anonymous operation with other operations
LoneAnonymousOperation.namedOperation=Validation error ({0}) : Operation ''{1}'' is following anonymous operation
#
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/i18n/Validation_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ KnownFragmentNames.undefinedFragment=Validierungsfehler ({0}) : Undefiniertes Fr
#
KnownTypeNames.unknownType=Validierungsfehler ({0}) : Unbekannter Typ ''{1}''
#
KnownOperationTypes.noOperation=Validierungsfehler ({0}): ''{1}'' Operation wird vom Schema nicht unterstützt
#
LoneAnonymousOperation.withOthers=Validierungsfehler ({0}) : Anonyme Operation mit anderen Operationen
LoneAnonymousOperation.namedOperation=Validierungsfehler ({0}) : Operation ''{1}'' folgt der anonymen Operation
#
Expand Down
6 changes: 3 additions & 3 deletions src/test/groovy/graphql/GraphQLTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ class GraphQLTest extends Specification {
thrown(GraphQLException)
}

def "null mutation type does not throw an npe re: #345 but returns and error"() {
def "null mutation type does not throw an npe but returns and error"() {
given:

GraphQLSchema schema = newSchema().query(
Expand All @@ -370,7 +370,7 @@ class GraphQLTest extends Specification {

then:
result.errors.size() == 1
result.errors[0].class == MissingRootTypeException
((ValidationError) result.errors[0]).validationErrorType == ValidationErrorType.UnknownOperation
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to discuss this

Previously, the same sort of problem would be caught at execution time, throwing a MissingRootTypeException if the operation could not be executed. Given that I've now moved forward validation, it's very unlikely the old exception will ever be triggered. However for safety I'd like to be defensive and leave in the old code. See SchemaUtil and getOperationRootType.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline - will keep in the old error in case, to be conservative

}

def "#875 a subscription query against a schema that doesn't support subscriptions should result in a GraphQL error"() {
Expand All @@ -393,7 +393,7 @@ class GraphQLTest extends Specification {

then:
result.errors.size() == 1
result.errors[0].class == MissingRootTypeException
((ValidationError) result.errors[0]).validationErrorType == ValidationErrorType.UnknownOperation
}

def "query with int literal too large"() {
Expand Down
80 changes: 80 additions & 0 deletions src/test/groovy/graphql/ParseAndValidateTest.groovy
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package graphql

import graphql.language.Document
import graphql.language.SourceLocation
import graphql.parser.InvalidSyntaxException
import graphql.parser.Parser
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.UnExecutableSchemaGenerator
import graphql.validation.ValidationError
import graphql.validation.ValidationErrorType
import graphql.validation.rules.NoUnusedFragments
Expand Down Expand Up @@ -155,4 +160,79 @@ class ParseAndValidateTest extends Specification {
then:
!rs.errors.isEmpty() // all rules apply - we have errors
}

def "validation error raised if mutation operation does not exist in schema"() {
def sdl = '''
type Query {
myQuery : String!
}
'''

def registry = new SchemaParser().parse(sdl)
def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry)
String request = "mutation MyMutation { myMutation }"

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

then:
errors.size() == 1
def error = errors.first()
error.validationErrorType == ValidationErrorType.UnknownOperation
error.message == "Validation error (UnknownOperation): The 'Mutation' operation is not supported by the schema"
error.locations == [new SourceLocation(1, 1)]
}

def "validation error raised if subscription operation does not exist in schema"() {
def sdl = '''
type Query {
myQuery : String!
}
'''

def registry = new SchemaParser().parse(sdl)
def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry)

String request = "subscription MySubscription { mySubscription }"

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

then:
errors.size() == 1
def error = errors.first()
error.validationErrorType == ValidationErrorType.UnknownOperation
error.message == "Validation error (UnknownOperation): The 'Subscription' operation is not supported by the schema"
error.locations == [new SourceLocation(1, 1)]
}

def "known operation validation rule checks all operations in document"() {
def sdl = '''
type Query {
myQuery : String!
}
'''

def registry = new SchemaParser().parse(sdl)
def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry)
String request = "mutation MyMutation { myMutation } subscription MySubscription { mySubscription }"

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

then:
errors.size() == 2
def error1 = errors.get(0)
error1.validationErrorType == ValidationErrorType.UnknownOperation
error1.message == "Validation error (UnknownOperation): The 'Mutation' operation is not supported by the schema"
error1.locations == [new SourceLocation(1, 1)]

def error2 = errors.get(1)
error2.validationErrorType == ValidationErrorType.UnknownOperation
error2.message == "Validation error (UnknownOperation): The 'Subscription' operation is not supported by the schema"
error2.locations == [new SourceLocation(1, 36)]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ class KnownDirectivesTest extends Specification {
field: String
}

type Subscription {
field: String
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test needed an adjustment to be valid

'''

def schema = TestUtil.schema(sdl)
Expand Down
Loading