Keeping certain parts of your GraphQL schema hidden from Introspection
Tags: graphql, node, webdev, tutorial
Published: 2021-06-25 19:44:41

GraphQL is a popular and powerful way to build your APIs and power your website; we use it for most of our APIs at Anvil. One of the best things about GraphQL is that it provides an Introspection Query capability which allows users to explore and learn about what's possible in a given API. In that way, GraphQL is "self-documenting". Some great tools like [Playground][Playground] and Anvil's own [SpectaQL][SpectaQL] leverage the Introspection Query.

## The Problem and Solution

Overall, the Introspection Query is a great thing, but there is one major downside: everything in your GraphQL schema will be visible to the world. Chances are your schema contains some Types, Fields, Queries, Mutations, etc, that you do not want the world to see or know about. Shutting down your Introspetion Query is one way to keep things private, but then your users cannot discover anything about your API. Fortunately there's another option: Directives. In this post, I will talk about how to leverage custom GraphQL Directives to hide sensitive things from the Introspection Query results so they can remain private.

## Implementation

At Anvil we are mostly a [Node](https://nodejs.org/en/) shop and use [Apollo](https://www.apollographql.com/) as our GraphQL server framework. As such, this post will feature a solution that is specific to that framework, and I highly recommend reading Apollo's docs on [Implementing Directives][Implementing Directives]. However, Directives are part of the general [GraphQL Specification](http://spec.graphql.org/June2018/#sec-Type-System.Directives), so every implementation ought to have a way to accomplish what I'm about to show you.


First, we need to define the directive in SDL by specifying (1) a name for it (2) any arguments to the directive and (3) what locations the directive is applicable on. We'll call our directive `undocumented`, it won't take any arguments, and it will be applicable to `Object`, `Field` and `Enum` definitions (you can obviously adjust any of this as necessary). Here's the SDL:
```sdl
"""
A directive that will filter things from the
Introspection Query results
"""
directive @undocumented on 
  | OBJECT | FIELD_DEFINITION | ENUM
```

Now we may decorate any Objects, Fields and Enums in our schema that we want to hide from the Introspection Query like so:
```sdl
type User {
  id: Int!
  email: String!
  # Don't show this Field!
  password: String! @undocumented
}

# Don't show this Type!
type SecretThing @undocumented {
  ...
}

...
```

Easy enough, right? Not so fast - we still have to implement it! As described in the [Implementing Directives][Implementing Directives] documentation, we'll want to create a subclass of the `SchemaDirectiveVisitor` class. Typically, the `SchemaDirectiveVisitor` class is used to implement the *augmentation* of data via directives, but we would like to completely remove some things from an Introspection Query result so we'll need a little extra help.

So that we can *filter* things rather than just *augment* them, we'll be using the [GraphQL Introspection Filtering][GraphqlIntrospectionFiltering] library[^1]. This library basically hooks into the internals of the `graphql` library and modifies the `introspection` module to check for some special, supplemental static methods in your `SchemaDirectiveVisitor` subclass. The return value of these methods will indicate whether a thing should be hidden or shown. It's probably best understood by an example:
```js
// UndocumentedDirective.js

import { SchemaDirectiveVisitor } from 'graphql-tools'

export default class UndocumentedDirective extends SchemaDirectiveVisitor {

  //****************************************
  // These methods are standard SchemaDirectiveVisitor
  // methods to be overridden. They allow us to "mark"
  // the things that were decorated with this directive
  // by setting the `isDocumented` property to `true`
  // 

  visitObject (subject) {
    subject.isUndocumented = true
  }

  visitEnum (subject) {
    subject.isUndocumented = true
  }

  visitFieldDefinition (subject) {
    subject.isUndocumented = true
  }

  //
  //****************************************

  //****************************************
  // These static methods are used by the
  // graphql-introspection-filtering library to decide
  // whether or not to show or hide things based on their
  // boolean responses
  // 
  
  static visitTypeIntrospection (type) {
    return UndocumentedDirective.isAccessible(type)
  }

  static visitFieldIntrospection (field) {
    return UndocumentedDirective.isAccessible(field)
  }

  // Don't show that this directive itself exists
  static visitDirectiveIntrospection ({ name }) {
    return name !== 'undocumented'
  }

  //
  //****************************************
  
  // If the thing has not been marked by the directive to
  // be undocumented, then it's accessible
  static isAccessible (thing) {
    return !thing.isUndocumented
  }
}
```

Finally, to pull it all together we need to build our executable schema out of all this and pass it along to our Apollo constructor:

```js
import { makeExecutableSchema } from 'graphql-tools'
import makeFilteredSchema, { schemaDirectivesToFilters } from 'graphql-introspection-filtering'
import ApolloServer from 'wherever-is-appropriate-for-your-stack'
import UndocumentedDirective from './UndocumentedDirective'

const typeDefs = `<your SDL here>`
const resolvers = {...}
const schemaDirectives = {
  // The key used here must match the name of the directive
  // we defined in SDL earlier
  undocumented: UndocumentedDirective,
}

const executableSchema = makeExecutableSchema({
  typeDefs,
  resolvers,
  schemaDirectives,
})

// Create a filters structure for any of our schemaDirectives
// that added any special methods for the
// graphql-introspection-filtering library
const filters = schemaDirectivesToFilters(schemaDirectives)

// Augment the schema and Introspection behavior to use the
// filters that were created
const filteredSchema = makeFilteredSchema(executableSchema, filters)

// Create our Apollo Server
const apolloServer = new ApolloServer({
  schema: filteredSchema,
  ...,
})

```

Profit! Now all Introspection Query responses will have anything decorated with the `@undocumented` directive removed from the results.

## The Catch
While this is super easy to leverage from this point on, there is a catch: __You must ensure that any *references* to definitions that you've hidden are *also* hidden__. If you're not careful about this, you can break your schema for many 3rd party tools (e.g. [Playground][Playground]) that leverage the Introspection Query. Imagine the following bit of SDL:
```sdl
type Secret @undocumented {
  aField: String
}

type MyType {
  aField: String,
  secretField: Secret
}
```

Uh oh, the `secretField` on `MyType` references a Type that is hidden from the output. Some tools will have trouble dealing with this non-existent reference. You can fix this by adding the `@undocumented` directive to the `secretField` definition like so:
```sdl
type MyType {
  aField: String,
  secretField: Secret @undocumented
}
```

This requires you to be careful when using the `@undocumented` directive if you don't want to break some tools by having an incomplete schema. This can be a cumbersome and challenging task. In a future blog post, we'll outline how to make this less painful. Stay tuned!




If you have questions, please do not hesitate to contact us at:
[developers@useanvil.com](mailto:developers@useanvil.com)

[Playground]: https://github.com/graphql/graphql-playground
[SpectaQL]: https://github.com/anvilco/spectaql
[Implementing Directives]: https://www.apollographql.com/docs/apollo-server/schema/creating-directives/
[GraphqlIntrospectionFiltering]: https://www.npmjs.com/package/graphql-introspection-filtering

[^1]: This library is currently on version `2.x`, but we are using `1.x`. Our examples are therefore only suitable for `1.x`.