You are viewing the preview version of this book
Click here for the full version.

Access control

Access control in any API is about the definition of who can do what. For example, there might be users who can add or remove other users, or some data is only accessible to one group but not to others. This is especially important for multitenant applications where you have data for multiple customers available in a single API. But some sort of access control is needed for almost all real-world use-cases.

GraphQL is about graphs, so it helps to think in nodes (objects) and edges that connect them. This way, the main question of access control is: what nodes a user can reach?

In this chapter, we'll explore the various approaches to control access in a GraphQL API. The methods here are generic to all GraphQL implementations, and not just AWS AppSync.

Example data model

In this chapter we'll use a data model that has Users, Groups, and Articles. Users belong to groups, and these provide strict separation, such as Slack workspaces. Users in one group should not know about users in another group.

Users in the same group can be friends. This provides a link between the User objects.

Finally, users can write articles. These are public, so articles published by a user in a group can be read by users in another group too.

Data model
Run the example

Deploy the schema in your account and run these queries live. You'll need an AWS account, the AWS CLI, and Terraform.

How to deploy and more info in the GitHub repository: https://github.com/sashee/graphql-access-control-example

The schema for these types:

type User {
  username: String!
  friends: [User]
  # we'll use this to see
  # how a naive implementation can be insecure
  group_unsafe: Group
  group: Group
}

type Group {
  name: String
  users: [User]
}

type Article {
  text: String
  author: User
}

Then the API allows these queries:

type Query {
  user(username: String!): User

  allUsers: [User]
  @aws_cognito_user_pools(cognito_groups: ["admin"])
  @aws_auth(cognito_groups: ["admin"])

  allArticles: [Article]
}
Example object graph

Entry points

Entry points are the queries and the mutations the API defines. They can return a type that provides fields the query can use to move to other objects. These are the easiest to secure, as usually there are not that many of them.

User query

For example, the user(username: String!): User query returns a User object. The query can then define the fields it needs and those can include other types too. Such as this query:

query MyQuery {
  user(username: "user1") {
    username
    friends {
      username
    }
  }
}

The user query returns a User object, then the friends returns other Users. Here, the query provides the entry point to the object graph. If a user can't send a query then they can't enter the graph there.

For example, the allUsers provides a way to retrieve multiple User objects. If a normal (non-admin) user can't call it then they can't accidentally get users from other groups.

Both queries and mutations can return complex types that allow moving to other objects in the graph, but mutations also make changes, so it's especially important to implement access control for them.

Traversals

When a query (or a mutation) has access to an object, it can move to other objects from there, allowing it to reach data deeper in the graph. In the previous example we saw that a Query can return an object and using its friends field it is possible to get other users:

query MyQuery {
  user(username: "user1") {
    username
    friends {
      username
    }
  }
}
Friends field

The above query returns the user1 object, as it is returned by the user query, and all its friends as they are returned by the friends field. Moreover, a query can also fetch the friends of the user's friends:

query MyQuery {
  user(username: "user1") {
    username
    friends {
      username
      friends {
        username
      }
    }
  }
}
Friends' friends

A more complex example

But there is a more sinister path built-in to the data model that can expose more data. Let's say the entry points are secure: an administrator can use the user Query to get users in its own group. With this, admin1 can only get three users: user1, user3, and admin1, and none from the other group. Since groups are supposed to provide hard boundaries between users, this is the expected functionality.

But articles are outside the group separation, and they have an author. Because of this, users from one group should have limited access to other groups.

Visually, an administrator should be able to reach these objects:

Objects admin1 can reach

Let's focus on the User.group field! Without taking articles into account, limiting the entry points is enough: if a user can only get users in the same group, then there is no way allowing access to the group field would create a vulnerability.

Navigating to the group and listing the users

But add the articles and suddenly a user from one group can navigate to another group and list users there:

Navigating from the article to the author and then the group

This shows how entry points and traversals together define what objects are accessible to users. Securing entry points alone is misleading: an unrelated change (adding a new object type) can open ways to navigate from the allowed portion of the object graph to other parts.

Implementation

There is more, but you've reached the end of this preview
Read this and all other chapters in full and get lifetime access to:
  • all future updates
  • full web-based access
  • PDF and Epub versions