Skip to content
Closed
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
28 changes: 24 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def reactiveStreamsVersion = '1.0.3'
def slf4jVersion = '1.7.35'
def releaseVersion = System.env.RELEASE_VERSION
def antlrVersion = '4.9.3' // https://mvnrepository.com/artifact/org.antlr/antlr4-runtime
def guavaVersion = '32.1.1-jre'
version = releaseVersion ? releaseVersion : getDevelopmentVersion()
group = 'com.graphql-java'

Expand Down Expand Up @@ -91,7 +92,7 @@ dependencies {
api 'com.graphql-java:java-dataloader:3.2.0'
api 'org.reactivestreams:reactive-streams:' + reactiveStreamsVersion
antlr 'org.antlr:antlr4:' + antlrVersion
implementation 'com.google.guava:guava:31.0.1-jre'
implementation 'com.google.guava:guava:' + guavaVersion
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
testImplementation 'org.codehaus.groovy:groovy:3.0.9'
Expand Down Expand Up @@ -127,7 +128,7 @@ shadowJar {
}
relocate('org.antlr.v4.runtime', 'graphql.org.antlr.v4.runtime')
dependencies {
include(dependency('com.google.guava:guava:31.0.1-jre'))
include(dependency('com.google.guava:guava:' + guavaVersion))
include(dependency('org.antlr:antlr4-runtime:' + antlrVersion))
}
from "LICENSE.md"
Expand Down Expand Up @@ -155,7 +156,7 @@ shadowJar {
bnd('''
-exportcontents: graphql.*
-removeheaders: Private-Package
Import-Package: !com.google.*,!org.checkerframework.*,!javax.annotation.*,!graphql.com.google.*,!org.antlr.*,!graphql.org.antlr.*,*
Import-Package: !com.google.*,!org.checkerframework.*,!javax.annotation.*,!graphql.com.google.*,!org.antlr.*,!graphql.org.antlr.*,!sun.misc.*,*
''')
}

Expand All @@ -172,8 +173,27 @@ task removeNotNeededGuava(type: Zip) {
}
}

task extractWithoutGuava(type: Copy) {
from({ zipTree({ "build/libs/graphql-java-${project.version}.jar" }) }) {
exclude('/com/**')
}
into layout.buildDirectory.dir("extract")
}

task buildNewJar(type: Jar) {
from layout.buildDirectory.dir("extract")
archiveFileName = "graphql-java-tmp.jar"
destinationDirectory = file("${project.buildDir}/libs")
manifest {
from file("build/extract/META-INF/MANIFEST.MF")
}
doLast {
delete("build/libs/graphql-java-${project.version}.jar")
file("build/libs/graphql-java-tmp.jar").renameTo(file("build/libs/graphql-java-${project.version}.jar"))
}
}

shadowJar.finalizedBy removeNotNeededGuava
shadowJar.finalizedBy extractWithoutGuava, buildNewJar


task testng(type: Test) {
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/graphql/ErrorClassification.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,21 @@ public interface ErrorClassification {
default Object toSpecification(GraphQLError error) {
return String.valueOf(this);
}

/**
* This produces a simple ErrorClassification that represents the provided String. You can
* use this factory method to give out simple but custom error classifications.
*
* @param errorClassification the string that represents the error classification
*
* @return a ErrorClassification that is that provided string
*/
static ErrorClassification errorClassification(String errorClassification) {
return new ErrorClassification() {
@Override
public String toString() {
return errorClassification;
}
};
}
}
8 changes: 8 additions & 0 deletions src/main/java/graphql/execution/AsyncExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
import graphql.introspection.Introspection;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;

Expand Down Expand Up @@ -46,6 +48,12 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont

MergedSelectionSet fields = parameters.getFields();
Set<String> fieldNames = fields.keySet();

Optional<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields);
if (isNotSensible.isPresent()) {
return CompletableFuture.completedFuture(isNotSensible.get());
}

Async.CombinedBuilder<FieldValueInfo> futures = Async.ofExpectedSize(fields.size());
List<String> resolvedFields = new ArrayList<>(fieldNames.size());
for (String fieldName : fieldNames) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
import graphql.introspection.Introspection;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx;
Expand Down Expand Up @@ -39,6 +41,13 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
MergedSelectionSet fields = parameters.getFields();
ImmutableList<String> fieldNames = ImmutableList.copyOf(fields.keySet());

// this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code
// so belts and braces
Optional<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields);
if (isNotSensible.isPresent()) {
return CompletableFuture.completedFuture(isNotSensible.get());
}

CompletableFuture<List<ExecutionResult>> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, index, prevResults) -> {
MergedField currentField = fields.getSubField(fieldName);
ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField));
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/graphql/execution/ValuesResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,15 @@ private static Object valueToLiteral(GraphqlFieldVisibility fieldVisibility, Inp
if (valueMode == NORMALIZED) {
return assertShouldNeverHappen("can't infer normalized structure");
}
return valueToLiteralLegacy(inputValueWithState.getValue(), type);
Value<?> value = valueToLiteralLegacy(
inputValueWithState.getValue(),
type);
//
// the valueToLiteralLegacy() nominally cant know if null means never set or is set to a null value
// but this code can know - its is SET to a value so, it MUST be a Null Literal
// this method would assert at the end of it if inputValueWithState.isNotSet() were true
//
return value == null ? NullValue.of() : value;
}
if (inputValueWithState.isLiteral()) {
return inputValueWithState.getValue();
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/graphql/i18n/I18n.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public enum BundleType {
protected I18n(BundleType bundleType, Locale locale) {
assertNotNull(bundleType);
assertNotNull(locale);
this.resourceBundle = ResourceBundle.getBundle(bundleType.baseName, locale);
// load the resource bundle with this classes class loader - to help avoid confusion in complicated worlds
// like OSGI
this.resourceBundle = ResourceBundle.getBundle(bundleType.baseName, locale, I18n.class.getClassLoader());
}

public ResourceBundle getResourceBundle() {
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/graphql/introspection/Introspection.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@

import com.google.common.collect.ImmutableSet;
import graphql.Assert;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.GraphQLContext;
import graphql.Internal;
import graphql.PublicApi;
import graphql.execution.MergedField;
import graphql.execution.MergedSelectionSet;
import graphql.execution.ValuesResolver;
import graphql.language.AstPrinter;
import graphql.schema.FieldCoordinates;
Expand All @@ -31,13 +36,16 @@
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLUnionType;
import graphql.schema.InputValueWithState;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import static graphql.Assert.assertTrue;
Expand All @@ -56,8 +64,83 @@
import static graphql.schema.GraphQLTypeUtil.unwrapOne;
import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY;

/**
* GraphQl has a unique capability called <a href="https://spec.graphql.org/October2021/#sec-Introspection">Introspection</a> that allow
* consumers to inspect the system and discover the fields and types available and makes the system self documented.
* <p>
* Some security recommendations such as <a href="https://owasp.org/www-chapter-vancouver/assets/presentations/2020-06_GraphQL_Security.pdf">OWASP</a>
* recommend that introspection be disabled in production. The {@link Introspection#enabledJvmWide(boolean)} method can be used to disable
* introspection for the whole JVM or you can place {@link Introspection#INTROSPECTION_DISABLED} into the {@link GraphQLContext} of a request
* to disable introspection for that request.
*/
@PublicApi
public class Introspection {


/**
* Placing a boolean value under this key in the per request {@link GraphQLContext} will enable
* or disable Introspection on that request.
*/
public static final String INTROSPECTION_DISABLED = "INTROSPECTION_DISABLED";
private static final AtomicBoolean INTROSPECTION_ENABLED_STATE = new AtomicBoolean(true);

/**
* This static method will enable / disable Introspection at a JVM wide level.
*
* @param enabled the flag indicating the desired enabled state
*
* @return the previous state of enablement
*/
public static boolean enabledJvmWide(boolean enabled) {
return INTROSPECTION_ENABLED_STATE.getAndSet(enabled);
}

/**
* @return true if Introspection is enabled at a JVM wide level or false otherwise
*/
public static boolean isEnabledJvmWide() {
return INTROSPECTION_ENABLED_STATE.get();
}

/**
* This will look in to the field selection set and see if there are introspection fields,
* and if there is,it checks if introspection should run, and if not it will return an errored {@link ExecutionResult}
* that can be returned to the user.
*
* @param mergedSelectionSet the fields to be executed
*
* @return an optional error result
*/
public static Optional<ExecutionResult> isIntrospectionSensible(GraphQLContext graphQLContext, MergedSelectionSet mergedSelectionSet) {
MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName());
if (schemaField != null) {
if (!isIntrospectionEnabled(graphQLContext)) {
return mkDisabledError(schemaField);
}
}
MergedField typeField = mergedSelectionSet.getSubField(TypeMetaFieldDef.getName());
if (typeField != null) {
if (!isIntrospectionEnabled(graphQLContext)) {
return mkDisabledError(typeField);
}
}
// later we can put a good faith check code here to check the fields make sense
return Optional.empty();
}

@NotNull
private static Optional<ExecutionResult> mkDisabledError(MergedField schemaField) {
IntrospectionDisabledError error = new IntrospectionDisabledError(schemaField.getSingleField().getSourceLocation());
return Optional.of(ExecutionResultImpl.newExecutionResult().addError(error).build());
}

private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) {
if (!isEnabledJvmWide()) {
return false;
}
return !graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false);
}

private static final Map<FieldCoordinates, IntrospectionDataFetcher<?>> introspectionDataFetchers = new LinkedHashMap<>();

private static void register(GraphQLFieldsContainer parentType, String fieldName, IntrospectionDataFetcher<?> introspectionDataFetcher) {
Expand Down Expand Up @@ -592,6 +675,7 @@ public enum DirectiveLocation {
return environment.getGraphQLSchema().getType(name);
};

// __typename is always available
public static final IntrospectionDataFetcher<?> TypeNameMetaFieldDefDataFetcher = environment -> simplePrint(environment.getParentType());

@Internal
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package graphql.introspection;

import graphql.ErrorClassification;
import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.Internal;
import graphql.language.SourceLocation;

import java.util.Collections;
import java.util.List;

@Internal
public class IntrospectionDisabledError implements GraphQLError {

private final List<SourceLocation> locations;

public IntrospectionDisabledError(SourceLocation sourceLocation) {
locations = sourceLocation == null ? Collections.emptyList() : Collections.singletonList(sourceLocation);
}

@Override
public String getMessage() {
return "Introspection has been disabled for this request";
}

@Override
public List<SourceLocation> getLocations() {
return locations;
}

@Override
public ErrorClassification getErrorType() {
return ErrorClassification.errorClassification("IntrospectionDisabled");
}
}
15 changes: 15 additions & 0 deletions src/main/java/graphql/parser/ParseCancelledTooDeepException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package graphql.parser;

import graphql.Internal;
import graphql.language.SourceLocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Internal
public class ParseCancelledTooDeepException extends InvalidSyntaxException {

@Internal
public ParseCancelledTooDeepException(String msg, @Nullable SourceLocation sourceLocation, @Nullable String offendingToken, int maxTokens, @NotNull String tokenType) {
super(sourceLocation, msg, null, offendingToken, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package graphql.parser;

import graphql.Internal;

@Internal
public class ParseCancelledTooManyCharsException extends InvalidSyntaxException {

@Internal
public ParseCancelledTooManyCharsException(String msg, int maxCharacters) {
super(null, msg, null, null, null);
}
}
Loading