Field Permissions in Graphql

Published on
6 min read
Field Permissions in Graphql

Introduction

GraphQL is no more a new topic. It has already become a primary choice for handling API data due to the flexibility it provides. In almost any API, having a granular access control is desirable but implementing such a system becomes very tedious with REST API. In GraphQL, you can handle this quite easily. In fact there are few ways of doing this and In this post, we are going to see how to use directives to have access control on fields.

This post assumes you have a basic understanding of GraphQL.

What is a directive ?

A directive decorates part of a GraphQL schema or operation with additional configuration. This allows you to have logic in your schema which is more expressible. Directives are preceded by the @ character, like so:

type Post {
  body: String @deprecated(reason: "Use `html`.")
  html: String
}

This example shows the @deprecated directive, which is a default directive (i.e., it's part of the GraphQL specification). It demonstrates the following about directives:

  • Directives can take arguments of their own (reason in this case).
  • Directives appear after the declaration of what they decorate (the body field in this case)

Building a simple GraphQL API

Let's start with a simple API and see how we can apply a directive on a field to control its access for logged in user vs a non logged in user. We are going to use a simple blogging application as an example. The blog will have authors and posts. A post will have a title, content and a notes section. The notes field is only for the author and is not meant to be displayed publicly.

Let's start with a simple NPM project.

npm init

Add the dependencies.

yarn add apollo-server graphql

We will be working with some sample data which looks like this.

// db.js

// This could also be MongoDB, PostgreSQL, etc
export const db = {
  authors: [
    {
      id: 1,
      name: "Abhishek Saha"
    }
  ],
  posts: [
    {
      author: 1, // this is a relation by id
      id: "1",
      title: "Hello World",
      body: "Content of hello world",
      notes: "some todos for this post which is not public"
    }
  ]
};

Next, create an index.js file for writing a simple GraphQL server.

import { ApolloServer, gql } from "apollo-server";
import {db} from "./db.js";

const typeDefs = gql`
    type Query {
      posts: [Post]
      post(id: ID!): Post
    }
    type Author {
      id: ID
      name: String
    }
    type Post {
      author: Author
      id: ID
      title: String
      body: String
      notes: String
    }
`;

const resolvers = {
  Query: {
    posts: () => db.posts,
    post: (_, { id }) => db.posts.find((post) => post.id === id),
  },
  Author: {
    name: (id) => {
      return db.authors.find((author) => author.id === id).name;
    },
  },
};

const server = new ApolloServer({
  context: ({ req }) => {
    // headers gets converted to lowercase by browser
    const isLoggedIn = !!req.headers.isloggedin; 
    return { isLoggedIn };
  },
  typeDefs,
  resolvers
});

server.listen().then(({ url }) => console.log(`Server ready at ${url}`));

We have created a schema (typeDefs) to access the data and added resolvers which is responsible to fetch the data for us. Next we create a new instance of Apollo server and pass the schema, resolvers and context.

Every request passes through context. Here you should validate the JWT token. In this example, we are simplifying this by passing isloggedin in the headers whose value would be Boolean.

And finally, we start listening to the server.

Let’s add nodemon to run the server locally. Nodemon helps to reload the server by watching the files for changes during development:

yarn add nodemon -D

Next add a script in package.json to turn on the server.

"scripts": {
    "start": "nodemon index.js"
},

Let's start the server from the terminal.

yarn start

Solution 1 - Access control inside resolver

Our intent is to make sure that the notes field inside post is only available for the logged in user. We differentiate a logged in user vs a non logged in user is by checking the authorization header. This can be implemented by passing a JWT token. However, this post does not cover the implementation of that.

Let's create a new field resolver called notes. We can access the context of this field and check if it has isLoggedIn key and if so, we will return the value of notes. Or else it will be set to null.

resolvers: {
    Query: {
      posts: () => db.posts,
      post: (_, { id }) => db.posts.find((post) => post.id === id)
    },
    Post: {
      notes: ({ notes }, _, context) => {
        if (context.isLoggedIn === "true") {
          return notes;
        }
      }
    },
    Author: {
      name: (id) => {
        return db.authors.find((author) => author.id === id).name;
      }
    }
}

Although this is easy to implement, and it works as expected, it's not the most ideal solution for big applications. It can easily get messy when you have to implement this for different fields across multiple queries.

Solution 2 - Field Directive on Schema

This is a more convenient and more expressive way of adding authorization on the field level.

Now we can write a one time logic for this directive which will be responsible to check the access of any field which needs this authorization.

// directive.js
const {
  SchemaDirectiveVisitor,
} = require("apollo-server-express");
const { defaultFieldResolver } = require("graphql");

class isLoggedInDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const originalResolve = field.resolve || defaultFieldResolver;
    field.resolve = async function (...args) {
      const context = args[2];
      if (!context.isLoggedIn) {
       return "";
      }
      const data = await originalResolve.apply(this, args);
      return data;
    };
  }
}

module.exports = { isLoggedInDirective };

To extend the SchemaDirectiveVisitor class, we implement the visitFieldDefination method. It takes the field parameter. We generate our result by setting the resolve property an async function that resolves to the result that we want the promise to resolve to. This can be a little difficult to understand in the first glance, but you will understand better once you start working with it.

We need to add this directive in our Apollo Server.

...
const server = new ApolloServer({
  typeDefs,
  resolvers,
  schemaDirectives: {
    isLoggedIn: isLoggedInDirective,
  },
  ...
});
...

Finally, we can use this directive on the field of our schema.

When we execute the above by setting isLoggedIn as false, we get empty notes.

However, when we execute the above with isLoggedIn as true, we get the value of notes.

If you would like to try this demo, you can use this Codesandbox link.

Conclusion

In this post, we saw how to add field level authorization using directives. It's a more expressive way to add logic to schema without polluting it. There are other ways as well, like using middlewares. You can read about it in one of my previous post here - Chaining Resolvers.

Author
Abhishek Saha
Abhishek Saha

Passionate about exploring the frontiers of technology. I am the creator and maintainer of Letterpad which is an open source project and also a platform.

Discussion (0)