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.
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
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 theid
of the row is equal to thex-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
fromx-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 thetitle
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:
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 theauthor_id
in the insert payload matches thex-hasura-user-id
in the session variables.Through column presets: In the insert permission of the
articles
table, you can disable inserting intoauthor_id
column and set a column preset such that theauthor_id
is automatically taken from thex-hasura-user-id
session variable. In this way, theauthor_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 updatetitle
andcontent
of without any filter. -
user
: Should be allowed to update thetitle
andcontent
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 be:
{
"and": [
{
"user" : {
"id": {
"_eq": "x-hasura-user-id"
}
}
},
{
"user": {
"karma": {
"_gte": 500
}
}
}
]
}
The above insert permission ensures that:
- The user is inserting an entry as themselves (
author_id
is equal tox-hasura-user-id
) - 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 of 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:
- The user is allowed to insert only as themselves. (
author_id
is equal tox-hasura-user-id
) - 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.
Top comments (0)