|
3 | 3 |
|
4 | 4 | import com.google.common.collect.ImmutableSet; |
5 | 5 | import graphql.Assert; |
| 6 | +import graphql.ExecutionResult; |
6 | 7 | import graphql.GraphQLContext; |
7 | 8 | import graphql.Internal; |
8 | 9 | import graphql.PublicApi; |
| 10 | +import graphql.execution.MergedField; |
| 11 | +import graphql.execution.MergedSelectionSet; |
9 | 12 | import graphql.execution.ValuesResolver; |
10 | 13 | import graphql.language.AstPrinter; |
11 | 14 | import graphql.schema.FieldCoordinates; |
|
32 | 35 | import graphql.schema.GraphQLSchema; |
33 | 36 | import graphql.schema.GraphQLUnionType; |
34 | 37 | import graphql.schema.InputValueWithState; |
| 38 | +import org.jetbrains.annotations.NotNull; |
35 | 39 |
|
36 | 40 | import java.util.ArrayList; |
37 | 41 | import java.util.HashSet; |
38 | 42 | import java.util.LinkedHashMap; |
39 | 43 | import java.util.List; |
40 | 44 | import java.util.Locale; |
41 | 45 | import java.util.Map; |
| 46 | +import java.util.Optional; |
42 | 47 | import java.util.Set; |
| 48 | +import java.util.concurrent.atomic.AtomicBoolean; |
43 | 49 | import java.util.function.Function; |
44 | 50 | import java.util.stream.Collectors; |
45 | 51 |
|
|
58 | 64 | import static graphql.schema.GraphQLTypeUtil.unwrapAllAs; |
59 | 65 | import static graphql.schema.GraphQLTypeUtil.unwrapOne; |
60 | 66 |
|
| 67 | +/** |
| 68 | + * GraphQl has a unique capability called <a href="https://spec.graphql.org/October2021/#sec-Introspection">Introspection</a> that allow |
| 69 | + * consumers to inspect the system and discover the fields and types available and makes the system self documented. |
| 70 | + * <p> |
| 71 | + * Some security recommendations such as <a href="https://owasp.org/www-chapter-vancouver/assets/presentations/2020-06_GraphQL_Security.pdf">OWASP</a> |
| 72 | + * recommend that introspection be disabled in production. The {@link Introspection#enabledJvmWide(boolean)} method can be used to disable |
| 73 | + * introspection for the whole JVM or you can place {@link Introspection#INTROSPECTION_DISABLED} into the {@link GraphQLContext} of a request |
| 74 | + * to disable introspection for that request. |
| 75 | + */ |
61 | 76 | @PublicApi |
62 | 77 | public class Introspection { |
| 78 | + |
| 79 | + |
| 80 | + /** |
| 81 | + * Placing a boolean value under this key in the per request {@link GraphQLContext} will enable |
| 82 | + * or disable Introspection on that request. |
| 83 | + */ |
| 84 | + public static final String INTROSPECTION_DISABLED = "INTROSPECTION_DISABLED"; |
| 85 | + private static final AtomicBoolean INTROSPECTION_ENABLED_STATE = new AtomicBoolean(true); |
| 86 | + |
| 87 | + /** |
| 88 | + * This static method will enable / disable Introspection at a JVM wide level. |
| 89 | + * |
| 90 | + * @param enabled the flag indicating the desired enabled state |
| 91 | + * |
| 92 | + * @return the previous state of enablement |
| 93 | + */ |
| 94 | + public static boolean enabledJvmWide(boolean enabled) { |
| 95 | + return INTROSPECTION_ENABLED_STATE.getAndSet(enabled); |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * @return true if Introspection is enabled at a JVM wide level or false otherwise |
| 100 | + */ |
| 101 | + public static boolean isEnabledJvmWide() { |
| 102 | + return INTROSPECTION_ENABLED_STATE.get(); |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * This will look in to the field selection set and see if there are introspection fields, |
| 107 | + * and if there is,it checks if introspection should run, and if not it will return an errored {@link ExecutionResult} |
| 108 | + * that can be returned to the user. |
| 109 | + * |
| 110 | + * @param mergedSelectionSet the fields to be executed |
| 111 | + * |
| 112 | + * @return an optional error result |
| 113 | + */ |
| 114 | + public static Optional<ExecutionResult> isIntrospectionSensible(GraphQLContext graphQLContext, MergedSelectionSet mergedSelectionSet) { |
| 115 | + MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName()); |
| 116 | + if (schemaField != null) { |
| 117 | + if (!isIntrospectionEnabled(graphQLContext)) { |
| 118 | + return mkDisabledError(schemaField); |
| 119 | + } |
| 120 | + } |
| 121 | + MergedField typeField = mergedSelectionSet.getSubField(TypeMetaFieldDef.getName()); |
| 122 | + if (typeField != null) { |
| 123 | + if (!isIntrospectionEnabled(graphQLContext)) { |
| 124 | + return mkDisabledError(typeField); |
| 125 | + } |
| 126 | + } |
| 127 | + // later we can put a good faith check code here to check the fields make sense |
| 128 | + return Optional.empty(); |
| 129 | + } |
| 130 | + |
| 131 | + @NotNull |
| 132 | + private static Optional<ExecutionResult> mkDisabledError(MergedField schemaField) { |
| 133 | + IntrospectionDisabledError error = new IntrospectionDisabledError(schemaField.getSingleField().getSourceLocation()); |
| 134 | + return Optional.of(ExecutionResult.newExecutionResult().addError(error).build()); |
| 135 | + } |
| 136 | + |
| 137 | + private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { |
| 138 | + if (!isEnabledJvmWide()) { |
| 139 | + return false; |
| 140 | + } |
| 141 | + return !graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false); |
| 142 | + } |
| 143 | + |
63 | 144 | private static final Map<FieldCoordinates, IntrospectionDataFetcher<?>> introspectionDataFetchers = new LinkedHashMap<>(); |
64 | 145 |
|
65 | 146 | private static void register(GraphQLFieldsContainer parentType, String fieldName, IntrospectionDataFetcher<?> introspectionDataFetcher) { |
@@ -636,6 +717,7 @@ public enum DirectiveLocation { |
636 | 717 | return environment.getGraphQLSchema().getType(name); |
637 | 718 | }; |
638 | 719 |
|
| 720 | + // __typename is always available |
639 | 721 | public static final IntrospectionDataFetcher<?> TypeNameMetaFieldDefDataFetcher = environment -> simplePrint(environment.getParentType()); |
640 | 722 |
|
641 | 723 | @Internal |
|
0 commit comments