Skip to content

Commit 6760775

Browse files
authored
Field visibility (graphql-java#641)
* Field visibility initial commit * removed enum and added a blacklist implementation * added introspection blacklist and also a default implementation as its own class * Test execution strategy as well * better name in test * Renamed based on PR and extra doco * Added schema from existing schema * cleanup naming
1 parent 6a4dc9d commit 6760775

20 files changed

+753
-45
lines changed

docs/execution.rst

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,76 @@ creating batched DataFetchers with get() methods annotated @Batched.
381381
.. This text will not be shown and if it does I have not done restructured comments right. We should add more details
382382
on how BatchedExecutionStrategy works here. Its a pretty special case that I don't know how to explain properly
383383

384+
Limiting Field Visibility
385+
-------------------------
386+
387+
By default every fields defined in a `GraphqlSchema` is available. There are cases where you may want to restrict certain fields
388+
depending on the user.
389+
390+
You can do this by using a `graphql.schema.visibility.GraphqlFieldVisibility` implementation and attaching it to the schema.
391+
392+
A simple `graphql.schema.visibility.BlockedFields` implementation based on fully qualified field name is provided.
393+
394+
.. code-block:: java
395+
396+
GraphqlFieldVisibility blockedFields = BlockedFields.newBlock()
397+
.addPattern("Character.id")
398+
.addPattern("Droid.appearsIn")
399+
.addPattern(".*\\.hero") // it uses regular expressions
400+
.build();
401+
402+
GraphQLSchema schema = GraphQLSchema.newSchema()
403+
.query(StarWarsSchema.queryType)
404+
.fieldVisibility(blockedFields)
405+
.build();
406+
407+
There is also another implementation that prevents instrumentation from being able to be performed on your schema, if that is a requirement.
408+
409+
Note that this puts your server in contravention of the graphql specification and expectations of most clients so use this with caution.
410+
411+
412+
.. code-block:: java
413+
414+
GraphQLSchema schema = GraphQLSchema.newSchema()
415+
.query(StarWarsSchema.queryType)
416+
.fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY)
417+
.build();
418+
419+
420+
You can create your own derivation of `GraphqlFieldVisibility` to check what ever you need to do to work out what fields
421+
should be visible or not.
422+
423+
.. code-block:: java
424+
425+
class CustomFieldVisibility implements GraphqlFieldVisibility {
426+
427+
final YourUserAccessService userAccessService;
428+
429+
CustomFieldVisibility(YourUserAccessService userAccessService) {
430+
this.userAccessService = userAccessService;
431+
}
432+
433+
@Override
434+
public List<GraphQLFieldDefinition> getFieldDefinitions(GraphQLFieldsContainer fieldsContainer) {
435+
if ("AdminType".equals(fieldsContainer.getName())) {
436+
if (!userAccessService.isAdminUser()) {
437+
return Collections.emptyList();
438+
}
439+
}
440+
return fieldsContainer.getFieldDefinitions();
441+
}
442+
443+
@Override
444+
public GraphQLFieldDefinition getFieldDefinition(GraphQLFieldsContainer fieldsContainer, String fieldName) {
445+
if ("AdminType".equals(fieldsContainer.getName())) {
446+
if (!userAccessService.isAdminUser()) {
447+
return null;
448+
}
449+
}
450+
return fieldsContainer.getFieldDefinition(fieldName);
451+
}
452+
}
453+
384454
385455
Query Caching
386456
-------------
@@ -434,3 +504,4 @@ with variables:
434504
}
435505
436506
The query is now reused regardless of variable values provided.
507+

src/main/java/graphql/GraphQL.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,6 @@ public ExecutionResult execute(ExecutionInput.Builder executionInputBuilder) {
302302
* Executes the graphql query using calling the builder function and giving it a new builder.
303303
* <p>
304304
* This allows a lambda style like :
305-
* <p>
306305
* <pre>
307306
* {@code
308307
* ExecutionResult result = graphql.execute(input -> input.query("{hello}").root(startingObj).context(contextObj));
@@ -357,7 +356,6 @@ public CompletableFuture<ExecutionResult> executeAsync(ExecutionInput.Builder ex
357356
* which is the result of executing the provided query.
358357
* <p>
359358
* This allows a lambda style like :
360-
* <p>
361359
* <pre>
362360
* {@code
363361
* ExecutionResult result = graphql.execute(input -> input.query("{hello}").root(startingObj).context(contextObj));

src/main/java/graphql/execution/ExecutionStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObject
585585
return TypeNameMetaFieldDef;
586586
}
587587

588-
GraphQLFieldDefinition fieldDefinition = parentType.getFieldDefinition(field.getName());
588+
GraphQLFieldDefinition fieldDefinition = schema.getFieldVisibility().getFieldDefinition(parentType, field.getName());
589589
if (fieldDefinition == null) {
590590
throw new GraphQLException("Unknown field " + field.getName());
591591
}

src/main/java/graphql/introspection/Introspection.java

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

33

4-
import graphql.language.AstValueHelper;
54
import graphql.language.AstPrinter;
5+
import graphql.language.AstValueHelper;
66
import graphql.schema.DataFetcher;
77
import graphql.schema.DataFetchingEnvironment;
88
import graphql.schema.GraphQLArgument;
@@ -171,7 +171,10 @@ private static String print(Object value, GraphQLInputType type) {
171171
Boolean includeDeprecated = environment.getArgument("includeDeprecated");
172172
if (type instanceof GraphQLFieldsContainer) {
173173
GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) type;
174-
List<GraphQLFieldDefinition> fieldDefinitions = fieldsContainer.getFieldDefinitions();
174+
List<GraphQLFieldDefinition> fieldDefinitions = environment
175+
.getGraphQLSchema()
176+
.getFieldVisibility()
177+
.getFieldDefinitions(fieldsContainer);
175178
if (includeDeprecated) return fieldDefinitions;
176179
List<GraphQLFieldDefinition> filtered = new ArrayList<>(fieldDefinitions);
177180
for (GraphQLFieldDefinition fieldDefinition : fieldDefinitions) {
@@ -182,6 +185,7 @@ private static String print(Object value, GraphQLInputType type) {
182185
return null;
183186
};
184187

188+
185189
public static DataFetcher interfacesFetcher = environment -> {
186190
Object type = environment.getSource();
187191
if (type instanceof GraphQLObjectType) {

src/main/java/graphql/schema/GraphQLSchema.java

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import graphql.schema.validation.InvalidSchemaException;
66
import graphql.schema.validation.SchemaValidationError;
77
import graphql.schema.validation.SchemaValidator;
8+
import graphql.schema.visibility.GraphqlFieldVisibility;
89

910
import java.util.ArrayList;
1011
import java.util.Arrays;
@@ -16,15 +17,19 @@
1617
import java.util.Set;
1718

1819
import static graphql.Assert.assertNotNull;
20+
import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY;
1921

2022
public class GraphQLSchema {
2123

24+
2225
private final GraphQLObjectType queryType;
2326
private final GraphQLObjectType mutationType;
2427
private final GraphQLObjectType subscriptionType;
2528
private final Map<String, GraphQLType> typeMap;
26-
private Set<GraphQLType> additionalTypes;
27-
private Set<GraphQLDirective> directives;
29+
private final Set<GraphQLType> additionalTypes;
30+
private final Set<GraphQLDirective> directives;
31+
private final GraphqlFieldVisibility fieldVisibility;
32+
2833

2934
public GraphQLSchema(GraphQLObjectType queryType) {
3035
this(queryType, null, Collections.emptySet());
@@ -35,16 +40,18 @@ public GraphQLSchema(GraphQLObjectType queryType, GraphQLObjectType mutationType
3540
}
3641

3742
public GraphQLSchema(GraphQLObjectType queryType, GraphQLObjectType mutationType, GraphQLObjectType subscriptionType, Set<GraphQLType> dictionary) {
38-
this(queryType, mutationType, subscriptionType, dictionary, Collections.emptySet());
43+
this(queryType, mutationType, subscriptionType, dictionary, Collections.emptySet(), DEFAULT_FIELD_VISIBILITY);
3944
}
4045

41-
public GraphQLSchema(GraphQLObjectType queryType, GraphQLObjectType mutationType, GraphQLObjectType subscriptionType, Set<GraphQLType> dictionary, Set<GraphQLDirective> directives) {
46+
public GraphQLSchema(GraphQLObjectType queryType, GraphQLObjectType mutationType, GraphQLObjectType subscriptionType, Set<GraphQLType> dictionary, Set<GraphQLDirective> directives, GraphqlFieldVisibility fieldVisibility) {
4247
assertNotNull(dictionary, "dictionary can't be null");
4348
assertNotNull(queryType, "queryType can't be null");
4449
assertNotNull(directives, "directives can't be null");
50+
assertNotNull(fieldVisibility, "fieldVisibility can't be null");
4551
this.queryType = queryType;
4652
this.mutationType = mutationType;
4753
this.subscriptionType = subscriptionType;
54+
this.fieldVisibility = fieldVisibility;
4855
this.additionalTypes = dictionary;
4956
this.directives = new HashSet<>(Arrays.asList(Directives.IncludeDirective, Directives.SkipDirective));
5057
this.directives.addAll(directives);
@@ -75,6 +82,10 @@ public GraphQLObjectType getSubscriptionType() {
7582
return subscriptionType;
7683
}
7784

85+
public GraphqlFieldVisibility getFieldVisibility() {
86+
return fieldVisibility;
87+
}
88+
7889
public List<GraphQLDirective> getDirectives() {
7990
return new ArrayList<>(directives);
8091
}
@@ -94,14 +105,34 @@ public boolean isSupportingSubscriptions() {
94105
return subscriptionType != null;
95106
}
96107

108+
/**
109+
* @return a new schema builder
110+
*/
97111
public static Builder newSchema() {
98112
return new Builder();
99113
}
100114

115+
/**
116+
* This allows you to build a schema from an existing schema. It copies everything from the existing
117+
* schema and then allows you to replace them.
118+
*
119+
* @param existingSchema the existing schema
120+
*
121+
* @return a new schema builder
122+
*/
123+
public static Builder newSchema(GraphQLSchema existingSchema) {
124+
return new Builder()
125+
.query(existingSchema.getQueryType())
126+
.mutation(existingSchema.getMutationType())
127+
.subscription(existingSchema.getSubscriptionType())
128+
.fieldVisibility(existingSchema.getFieldVisibility());
129+
}
130+
101131
public static class Builder {
102132
private GraphQLObjectType queryType;
103133
private GraphQLObjectType mutationType;
104134
private GraphQLObjectType subscriptionType;
135+
private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY;
105136

106137
public Builder query(GraphQLObjectType.Builder builder) {
107138
return query(builder.build());
@@ -130,6 +161,11 @@ public Builder subscription(GraphQLObjectType subscriptionType) {
130161
return this;
131162
}
132163

164+
public Builder fieldVisibility(GraphqlFieldVisibility fieldVisibility) {
165+
this.fieldVisibility = fieldVisibility;
166+
return this;
167+
}
168+
133169
public GraphQLSchema build() {
134170
return build(Collections.emptySet(), Collections.emptySet());
135171
}
@@ -141,7 +177,7 @@ public GraphQLSchema build(Set<GraphQLType> additionalTypes) {
141177
public GraphQLSchema build(Set<GraphQLType> additionalTypes, Set<GraphQLDirective> additionalDirectives) {
142178
assertNotNull(additionalTypes, "additionalTypes can't be null");
143179
assertNotNull(additionalDirectives, "additionalDirectives can't be null");
144-
GraphQLSchema graphQLSchema = new GraphQLSchema(queryType, mutationType, subscriptionType, additionalTypes, additionalDirectives);
180+
GraphQLSchema graphQLSchema = new GraphQLSchema(queryType, mutationType, subscriptionType, additionalTypes, additionalDirectives, fieldVisibility);
145181
new SchemaUtil().replaceTypeReferences(graphQLSchema);
146182
Collection<SchemaValidationError> errors = new SchemaValidator().validateSchema(graphQLSchema);
147183
if (errors.size() > 0) {

src/main/java/graphql/schema/SchemaUtil.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ private void collectTypesForInterfaces(GraphQLInterfaceType interfaceType, Map<S
108108
}
109109
result.put(interfaceType.getName(), interfaceType);
110110

111+
// this deliberately has open field visibility as its collecting on the whole schema
111112
for (GraphQLFieldDefinition fieldDefinition : interfaceType.getFieldDefinitions()) {
112113
collectTypes(fieldDefinition.getType(), result);
113114
for (GraphQLArgument fieldArgument : fieldDefinition.getArguments()) {
@@ -124,6 +125,7 @@ private void collectTypesForObjects(GraphQLObjectType objectType, Map<String, Gr
124125
}
125126
result.put(objectType.getName(), objectType);
126127

128+
// this deliberately has open field visibility as its collecting on the whole schema
127129
for (GraphQLFieldDefinition fieldDefinition : objectType.getFieldDefinitions()) {
128130
collectTypes(fieldDefinition.getType(), result);
129131
for (GraphQLArgument fieldArgument : fieldDefinition.getArguments()) {

0 commit comments

Comments
 (0)