Hasura Authorization System through Examples

Authorization systems are about being able to specify rules that will allow the system to answer, “Can user U perform action A [on resource R]?”. In this post, we look at implementing some real-world authorization rules using Hasura's JSON-based DSL. But first, a quick recap of Hasura's authorization system.

Every request to Hasura executes against a set of session variables. These variables are expected to be set by the authentication system. While arbitrary variables can be set and used in defining authorization rules, two variables are of particular interest in the context of authorization:

  • X-Hasura-User-Id: This variable usually denotes the user executing the request.
  • X-Hasura-Role: This variable denotes the role with which the user is executing the current request. Hasura has a built-in notion of a role and will explicitly look for this variable to infer the role.

We can create new roles—which are just arbitrary names—from the Hasura console. Once a role is created, we can set permissions for select, insert, update, and delete for each table in our schema. A permission rule is a JSON object that looks something like this:

{"user_id": {"_eq": "X-Hasura-User-Id"}}

The above rule is equivalent to saying: allow this request to execute on the current row if the value of the user_id column equals the value of the session variable X-Hasura-User-Id.

The JSON here is essentially a domain language for expressing authorization rules. It is possible to express pretty complex rules:

  • Any columns in the table can be used in place of user_id
  • Several operators ( _gt, _lt, _in, etc) can be used in place of _eq operator
  • It is possible to traverse relationships (we will see how to do this shortly)
  • Operators _and and _or can be used to chain rules
  • It is even possible to query tables not related to the current object as part of the rule execution (again, more on this shortly).

All of this is documented in the official documentation. Let us now look at how to apply these in the real world.

Simple role-based access control

Consider a simple HRMS system with the roles HR, Employee, Manager, and Director. We want to implement the following authorization rules:

  • Only HR should be able to edit payrolls
  • Employees should be able to see their own payrolls
  • Managers should be able to view all payrolls, but not edit them

The database schema and relationships for this use case would look like:

Once the above schema and relationships have been created in Hasura, we can create the custom roles HR, Employee, and Manager. We then need to create the following permission rules on the payroll table for each of these roles:

  • HR:
    • Select: Without any checks
    • Insert: Without any checks
    • Update: Without any checks
    • Delete: Without any checks
  • Employee:
    • Select: { "employee_id": {"_eq": "X-Hasura-User-Id"} }
    • Insert: Denied
    • Update: Denied
    • Delete: Denied
  • Manager:
    • Select: Without any checks
    • Insert: Denied
    • Update: Denied
    • Delete: Denied

Now suppose we want a manager to only be able to read or update their reportees payrolls (but not payrolls of other employees). Assuming relationships "employee" from payroll to employee (using employee_id), and "manager" from employee to employee (using manager_id), we need to change the select and update rules for the role manager on the payrolls table to be:

{ 
    "employee": {
    	"manager_id": {"_eq": "X-Hasura-User-Id"}
    }
}

The above rule tells Hasura to fetch the employee associated with the payroll record and match their manager_id with the current user id. Hasura can traverse arbitrarily nested relationships, making this a powerful construct. In addition, we need to set the column update permissions so that only the salary field can be updated:

Per-resource roles

Lets us a look at a more complicated example: a Google Docs clone where we have documents, users, and roles per document. Each document can have owners, editors, and viewers with these rules:

  • Owners can read, update and delete a document
  • Editors can read and update a document
  • Viewers can only read a document
  • Other users can't read, update or delete a document

In this case, we can use a single Hasura role user and directly model the per document role in the schema. The database schema and relationships would look something like this:

After creating the above schema and relationships in Hasura, we can configure the following permission rules on the documents table for the Hasura role user:

Select permissions:

{
    "_or": [
        {"owners": {"user_id": {"_eq": "X-Hasura-User-Id"}}},
        {"editors": {"user_id": {"_eq": "X-Hasura-User-Id"}}},
        {"viewers": {"user_id": {"_eq": "X-Hasura-User-Id"}}}
    ]
}
 

This rule illustrates the use of the _or operator. The rule will apply if any of the three conditions in the array are matched.

Update permissions:

{
    "_or": [
        {"owners": {"user_id": {"_eq": "X-Hasura-User-Id"}}},
        {"editors": {"user_id": {"_eq": "X-Hasura-User-Id"}}}
    ]
}
 

Delete permissions:

{"owners": {"user_id": {"_eq": "X-Hasura-User-Id"}}}}

Insert permissions:

Any user can insert a document. However, whenever a document is created, we need to make sure that the owners table is also populated along with the (user_id, document_id) tuple so that selects, updates and deletes don't fail. This can be done by setting up a Postgres event trigger:

Note: In this example, there are two role-based systems in action: Hasura's built-in role-based access control system, and the per-document role-based access control system implemented using the above database schema and permission rules. As far as Hasura's system is concerned, all requests are being executed against the user role.

Hierarchical roles

In a hierarchical role-based access control system, roles are arranged into a hierarchy and a user with a role will have access to anything that the role or a child role has access to.

Example: Consider an organization with the following role hierarchy: employee > lead > manager > director > department_head. A user who is higher in the hierarchy should be able to do any action that someone lower in the hierarchy can do. For example, a manager should be able to do anything a lead or an employee can do. Another way of looking at this is that the manager is assigned multiple roles [manager, lead, engineer], and they have access to a resource if at least one of these roles has access to it.

For this example, we will again use a single Hasura role user and directly model the roles in the schema:

We will also assume there is a documents table that only managers should have access to.

For matching a user against all applicable roles in the hierarchy, we need to recursively traverse the role table to fetch all the roles. While Hasura's DSL allows us to traverse relationships, we cannot do so recursively. So we have to figure out a way to flatten the role hierarchy. Assume we have magically created the following view:

  • flattened_user_roles (user_id, role_id): This will contain the result of traversing the role hierarchy and creating all possible (user_id, role_id) combinations.

We can now write the permission rule for selects on the documents table:

{
    "_exists": {
        "_table": {
           "table": "flattened_user_roles",
           "schema": "public"
        },
        "_where": {
            "_and": [
                { "user_id": {"_eq": "X-Hasura-User-Id"} },
                { "role_id": {"_eq": "manager"} }
            ]
        }
    }
}

The above rule illustrates the use of the _exists operator. _exists allows us to write rules that query tables unrelated to the row being accessed. The rule succeeds if the conditions in the _where clause yield at least one row.

How do we generate flattened_user_roles though? Postgres' WITH RECURSIVE to the rescue!

In the above query, we create a view that generates a flattened version of the hierarchical user_roles table.

Exercise: How would you implement a scenario with per-resource role-based access control that is also hierarchical? Hint: Instead of using _exists above, traverse the relationship from the object.

Note: This example also shows how to model scenarios where one user can have multiple roles. Hasura's built-in role-based access control system currently allows a request to only be executed against a single role. However, by modeling the roles directly in the database, we can implement scenarios where one user has multiple roles. We are also working on adding multiple role support directly to Hasura.

Attribute-based access control

Attribute-based access control is about being able to write authorization rules based on the attributes of a resource.

Example: Consider a university with multiple departments, and each department has documents and users. We want users to be able to read documents that belong to their department. The schema in this case would look something like:

Assuming there is a department relationship defined from document to department, and a users relationship defined from department to users, the select permissions on the documents table would be:

{
    "department": {
    	"users" : {"id": {"_eq": "X-Hasura-User-Id"}}
    }
}

Conclusion

In this post, we looked at implementing authorization rules for several real-world use cases. Along the way, we learned the syntax for writing some pretty complex rules. The example under "Hierarchical roles in an organization" illustrated using a view to handle more complex scenarios. If you would like to learn more:

  • The learn course has a tutorial on building permission rules for a Slack clone.
  • The documentation explains everything in great detail.
  • This blog post explains how to build authorization rules for a Hacker News clone.

If you are using Hasura and need help with authorization, or want to share some interesting use cases you have implemented, ping us on Discord or tweet to us at @HasuraHQ!

Blog
17 Mar, 2020
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.