Common access control patterns with Hasura GraphQL Engine

Introduction

In this post, we will look at some access control patterns that can be used with Hasura to granularly allow/restrict the data. This is a summary blog post from Hasura Streams and the video has been uploaded to Youtube. If you prefer watching, similar examples have been covered in this video:

Hasura GraphQL Engine

Hasura GraphQL Engine is a thin GraphQL server that sits on any Postgres database and allows you to CRUD the data with realtime GraphQL and access control.

This post assumes that you have basic understanding of Hasura and relational data models. Check out this guide if you have never used Hasura before.

Hasura enables role based access control which can be integrated with most Auth providers. The access control rules in Hasura are functions of session variables. Session variables are x-hasura-* variables like x-hasura-role, x-hasura-user-id that can be decoded from the request headers of the GraphQL request. You could have any number of session variables to make the rules more granular.

Auth flow with GraphQL Engine

Let us see how to set up access control rules as functions of these session variables. Hasura infers the GraphQL schema from the Postgres schema, which means that setting access control rules on the tables, columns and their relationships corresponds to setting access control rules on the fields of your GraphQL schema.

Permissions

The access control rules are referred to as Permissions in the Hasura console. When you go to any table, you can see a permissions tab for that table where you can set permissions.

UI

The Hasura console has a neat UI for setting permissions. It also has a filter-builder that will make building filters and checks very joyful.

Roles

With each GraphQL request made to it, Hasura checks the role of the client user in the session variable x-hasura-role. Looking at this role, Hasura looks for the permissions for this role and allows or restricts CRUD accordingly.

The roles in Hasura have nothing to do with the Postgres roles and users. These roles are implemented at the Hasura Layer.

You can define these roles in the Hasura console and there is no limit on multiple roles being defined.

Insert Permission

The permissions to insert rows in a table is composed of three parts:

  • Check constraint: This is a boolean value built out of session variables, values of the fields of the row being inserted and all the logical operators. For example, to insert into users table, you want to set a condition like { "id": { ¬†"_eq": "x-hasura-user-id" } }. This condition enforces that whenever a row is being inserted to the users table, it can only be inserted if the id of the row is equal to the x-hasura-user-id value in the session variables.
  • Columns: You can restrict insertion to only particular columns (rest of them being null, default or being inferred from column presets)
  • Column Presets: Column presets are values that you can set to be assigned to fields while they are being inserted. For example, when an entry is being inserted in a users table, we can take the id from x-hasura-user-id session variable. This helps to avoid parsing the session information on the client.

Select Permission

  • Filter: This is a boolean value built out of session variables and the values of the fields being selected. For example, in an articles table, you want all users to be allowed to query all published articles, but only their own unpublished articles. To implement that, you would add a filter like:
{
  "or": [
    {
      "author_id": {
        "_eq": "x-hasura-user-id"
      }
    },
    {
      "is_published": true
    }
  ]
}
  • Columns: Sometimes you want some roles to not have access to particular columns of the table. In such cases, you can explicitly choose which columns to allow and restrict.

Update Permission

  • Filter: This is a boolean value built out of session variables and the values of the fields being updated. For example, in an articles table, you want users to modify only their own articles. To do that, your update filter would look like:
{
  "author_id": {
    "_eq": "x-hasura-user-id"
  }
}
  • Columns: You can restrict which columns can be updated. This comes handy in cases when you do not want created_at field to ever be updated, but you might want to update the title field in the articles table.
  • Column presets: Like with insert, if you want to automatically update certain fields with certain values without it being explicitly mentioned in the request.

Delete Permission

  • Filter: Like with update, the filter on delete permisson is a boolean condition which needs to be satisfied before the the row can be deleted.

Examples

With the above ideas, let us look at some specific cases and how they can be modelled. Firstly, lets use a base schema for a HackerNews like application. This is the postgres schema:

Let me show you a sample GraphQL query to get all the articles along with their authors, comments, upvotes and downvotes.

query {
  articles {
    id
    title
    content
    content_type
    author {
      id
      name
    }
    comments {
      body
      id
      author {
        id
        name
      }
    }
    article_upvotes_aggregate {
      aggregate {
        count
      }
    }
    article_downvotes_aggregate {
      aggregate {
        count
      }
    }
  }
}

Now let us try to target some specific access control use cases with the above schema.

Enforcing users to insert articles as themselves

Say I am a user with id = 123. This means that the value of x-hasura-user-id in my session information would be 123. Now, when I am inserting an article in the articles table, you would want the author_id to be 123, which means you would want me to insert the article with myself as the author. To enforce that, you could take two approaches:

  1. Through check constraint: In the insert permission of the articles table, you can set a check constraint like { "author_id": { "_eq": "x-hasura-user-id" } }. This would ensure that the article would be inserted only if the author_id in the insert payload matches the x-hasura-user-id in the session variables.
  2. Through column presets: In the insert permission of the articles table, you can disable inserting into author_id column and set a column preset such that the author_id is automatically taken from the x-hasura-user-id session variable. In this way, the author_id would be inserted appropriately without the client needing to explicitly mention it.

You can use exactly the same approach for restricting users from updating articles that are not published by them.

Multiple roles

Say you have two roles: user and moderator. A moderator should be allowed to update the title of every post unconditionally while a user should be allowed to update the title only for their own posts. So the update permission for:

  • moderator: Should be allowed to update title and content of without any filter.
  • user: Should be allowed to update the title and content with the filter { "author_id": { "_eq": "x-hasura-user-id" } }.

Also, the moderator should be allowed to delete every post while a user should be allowed to delete only their own posts. So the delete permission for:

  • moderator: Should be allowed to delete without checks.
  • user : Should be allowed to delete ¬†with the filter { "author_id": { "_eq": "x-hasura-user-id" } }

Access control through views

In a typical news discussion app, the downvotes or upvotes on an article must be anonymous. This means, that the users should not be able to get all the information from article_downvotes and article_upvotes tables. To achieve anonymity, you can create a view that just has the article_id and the number of upvotes or downvotes. For article_downvotes, the view would look like:

CREATE VIEW article_downvote_count as
SELECT article_id,
  count(*) AS downvotes
FROM article_downvotes
GROUP BY article_id;

Now, once this view is created, you can set a select permission on this view such that it can be selected without any checks. You can implement the restriction similarly for article_upvotes table as well.

Downvoting based on Karma

In a website like HackerNews, you get down-voting privilege only when you reach a particular Karma (say 500). So on a table like article_downvotes which is a many to many relationship between articles and users, your insert permission would have a check:

{
  "and": [
    {
      "user" : {
        "id": {
          "_eq": "x-hasura-user-id"
        }
      }
    },
    {
      "user": {
        "karma": {
          "_gte": 500
        }
      }
    }
  ]
}

The above insert permission ensures that:

  1. The user is inserting an entry as themselves (author_id is equal to x-hasura-user-id)
  2. The insert is successful only if the inserting user has karma greater than or eual to 500.

Enforcing fields to have only particular values

You can emulate enum-like behavior using Hasura Permissions. For example, in the articles table, the content_type field should be either "url" or "text". Any other value is invalid. Therefore, the check condition in the insert permission on articles table looks like:

{
  "_and": [
    {
      "user" : {
        "id": {
          "_eq": "x-hasura-user-id"
        }
      }
    },
    {
      "content_type": {
        "_in": ["text", "url"]
      }
    }
  ]
}

This permission makes sure that:

  1. The user is allowed to insert only as themselves. (author_id is equal to x-hasura-user-id)
  2. In the article being inserted, the content_type field must be either text or url.

You can similarly add different complicated access control rules for different roles and secure your data. Let us know if you have any questions in comments or join our Discord channel where we are super active. Also, let us know if you need more examples in the comments.

Reference


Hasura is an open-source engine that gives you realtime GraphQL APIs on new or existing Postgres databases, with built-in support for stitching custom GraphQL APIs and triggering webhooks on database changes.