Search code examples
springspring-bootgraphqlspring-graphql

How to access all directives on selected fields of a query, with GraphQL Spring Boot?


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.


Solution

  • 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.