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:
| Option | Where the check runs | Trade-off |
|---|---|---|
| Resolvers | In each resolver function | Fine-grained, but every resolver needs its own check |
| Schema directives | On schema fields, through a directive your server implements | Scales with the schema, but you edit the schema |
| Data sources | Between the GraphQL server and the database | Checks follow the database schema, not the GraphQL types |
| Server plugins or middleware | Once per request, from the operation name or type | One 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
launchesto thegetallaction on thelaunchresource. - Operation type. The operation name is the resource, and the action is
writefor a mutation orreadfor a query.
Both plugins, with setup and verification steps, are in Add Permit permission checks to Apollo Server.