Skip to main content

Enforce permissions in a GraphQL API

Understand where to add Permit.io permission checks in a GraphQL API, and what each option costs to build and maintain. This page is for backend developers who serve a GraphQL API and need to control which users can run each query and mutation. For a working implementation, see Add Permit permission checks to Apollo Server.

Map GraphQL operations to resources and actions

Keep policy out of your GraphQL code by describing each operation as a resource and an action. Your server tells Permit what is happening, for example "user john@permit.io runs get on launch", and the Permit policy decides whether the user can do it. You change who can do what in the policy, without changing the server code.

Why a resource and action mapping

The GraphQL server maps operations, fields, or data source calls to Permit resources and actions, then calls permit.check() with the user, the action, and the resource. The options in Choose where to check permissions differ in where the mapping lives.

Directive libraries that put roles in the schema

Authorization directive libraries such as graphql-directive-auth write roles into the schema, for example a directive that requires the admin role on a field. Role names then live in your application code. Changing who can access a field means changing and redeploying the schema. A resource and action mapping keeps role names and rules in the Permit policy instead.

Choose where to check permissions

You can combine these options in one server:

OptionWhere the check runsTrade-off
ResolversIn each resolver functionFine-grained, but every resolver needs its own check
Schema directivesOn schema fields, through a directive your server implementsScales with the schema, but you edit the schema
Data sourcesBetween the GraphQL server and the databaseChecks follow the database schema, not the GraphQL types
Server plugins or middlewareOnce per request, from the operation name or typeOne place for all checks, based on a mapping

Check permissions in resolvers

Call permit.check() in each resolver function. Each resolver gets fine-grained control, and each new resolver needs a new check, so the checks become harder to maintain as the number of resolvers grows. Resolver checks are the GraphQL equivalent of route-level checks in a REST API.

Map fields with schema directives

Add a directive with a resource and an action to schema fields, and implement the directive in your GraphQL server so it calls permit.check(). Permit doesn't provide the @permit directive: the following schema shows the mapping, and your server supplies the directive implementation.

type Book {
title: String
author: Author
rating: Int @permit(resource: "book_rating", action: "get")
}

type Author {
name: String
books: [Book] @permit(resource: "author_books", action: "get")
}

Check permissions in data sources

Add the check in the data source layer, right before the database call. The checks map to the database schema instead of your GraphQL types, which couples authorization to how you store data. Data source checks are the GraphQL equivalent of function-level checks before a database call in a REST API.

The following Apollo Server data source checks whether the user can get a product before loading it. permit is an initialized Permit client, and userKey is the user key from the verified request token:

import DataLoader from 'dataloader';

class ProductsDataSource {
constructor(dbConnection, permit, userKey) {
this.dbConnection = dbConnection;
this.permit = permit;
this.userKey = userKey; // the user key from the verified request token
this.batchProducts = new DataLoader(async (ids) => {
const productList = await this.dbConnection.fetchAllKeys(ids);
return ids.map((id) => productList.find((product) => product.id === id));
});
}

async getProductFor(id) {
// permission check - user, action, resource
const allowed = await this.permit.check(this.userKey, "get", { type: "product", attributes: { id } });
if (!allowed) {
throw new Error("Not allowed");
}
return this.batchProducts.load(id);
}
}

Check permissions in a server plugin

Check every request in one server plugin or middleware. The plugin reads the operation from the request and looks up its resource and action, in one of two ways:

  • Mapping object. A map from operation name to a resource and an action, for example launches to the getall action on the launch resource.
  • Operation type. The operation name is the resource, and the action is write for a mutation or read for a query.

Both plugins, with setup and verification steps, are in Add Permit permission checks to Apollo Server.

Next steps