I have an authentication directive, used to restrict fields to certain authentication levels
directive @auth(role: [String!]!) on FIELD_DEFINITION
For example, with the following schema
type Query {
test: TestResultType! @auth(role: ["USER", "ADMIN"])
}
type TestResultType {
customer: Customer!
seller: Seller!
}
type Customer {
email: String!
username: String!
password: String! @auth(role: "ADMIN")
}
type Seller {
brandName: String!
email: String!
username: String!
password: String! @auth(role: "ADMIN")
}
The query test
would be restricted to either "USER"
or "ADMIN"
, and the password
field of both Customer
and Seller
are restricted to only "ADMIN"
.
If I have the authorization level of "USER"
, but not "ADMIN"
, then the following query should go through just fine because I am not requesting anything that is protected with the @auth(role: "ADMIN")
directive
query {
test {
customer {
email
}
seller {
brandName
email
}
}
}
However, if I have the authorization level of "USER"
, but not "ADMIN"
, then the following query should return an error since I selected the password
fields in the query, which is protected with the @auth(role: "ADMIN")
directive
query {
test {
customer {
email
password
}
seller {
brandName
email
password
}
}
}
To work with directives in Spring Boot GraphQL, I must register a SchemaDirectiveWiring
with a RuntimeWiringConfigurer
bean. I have registered AuthorizationDirective
public class AuthorizationDirective implements SchemaDirectiveWiring {
@Override
public GraphQLFieldDefinition onField(
SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition> wiringEnv) {
// Get current data fetcher
GraphQLFieldsContainer fieldsContainer = wiringEnv.getFieldsContainer();
GraphQLFieldDefinition fieldDefinition = wiringEnv.getFieldDefinition();
final DataFetcher<?> currentDataFetcher = wiringEnv
.getCodeRegistry()
.getDataFetcher(fieldsContainer, fieldDefinition);
// Apply data fetcher with authorization logic
final DataFetcher<?> authorizingDataFetcher = buildAuthorizingDataFetcher(
wiringEnv,
currentDataFetcher);
wiringEnv.getCodeRegistry()
.dataFetcher(
fieldsContainer,
fieldDefinition,
authorizingDataFetcher);
return fieldDefinition;
}
private DataFetcher<Object> buildAuthorizingDataFetcher(
SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition> wiringEnv,
DataFetcher<?> currentDataFetcher) {
return fetchingEnv -> {
// Implementation here
};
}
}
Where I am lost is, how do I extract the REQUESTED fields and information from either the SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition>
or DataFetchingEnvironment
objects, that are available to me in the buildAuthorizingDataFetcher()
function. I managed to extract ALL fields from wiringEnv
by performing a breadth-first traversal like this:
Queue<GraphQLSchemaElement> nodeQueue = new LinkedBlockingQueue<>(
wiringEnv.getElement().getType().getChildren());
while (!nodeQueue.isEmpty()) {
var node = nodeQueue.remove();
if (GraphQLFieldDefinition.class.isAssignableFrom(node.getClass()))
// Perform logic on graphql field node
System.out.println(((GraphQLFieldDefinition) node).getName());
nodeQueue.addAll(node.getChildren());
}
And I could also see how I could do something similar with fetchingEnv
, however, I don't want ALL fields of a query, I only want the ones selected by the user. Is there a way to access this information?
EDIT: I found a way to get a list of all the selections:
fetchingEnv.getSelection().getFields();
This returns a list of SelectedField
, which is exactly what I wanted, however, these SelectedField
objects lack any information about directives.
I found a way to do it.
The following code snippet will return an object of type List<SelectedField>
var selectionSet = fetchingEnv.getSelectionSet().getFields();
Then, you can iterate through this list to extract the List<GraphQLFieldDefinition>
object from your selection set.
var fieldDefs = selectionSet.stream()
.flatMap(s -> s.getFieldDefinitions().stream())
.toList()
Finally, you can extract the List<GraphQLDirective>
object from the field definitions.
var directives = fieldDefs.stream()
.map(f -> f.getDirective("name"))
.filter(Objects::nonNull)
.toList();
And then you can perform all sorts of other checks on the directives that you need.