DEV Community

Hasura
Hasura

Posted on • Originally published at hasura.io on

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.

Authorization with Hasura: Recap

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"}}
Enter fullscreen mode Exit fullscreen mode

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.

Example 1: Access control for a payroll management system

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 <!--kg-card-end: markdown-->

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"}
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Example 2: Per-document roles in Google Docs

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"}}}
    ]
}

Enter fullscreen mode Exit fullscreen mode

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"}}}
    ]
}

Enter fullscreen mode Exit fullscreen mode

Delete permissions:

{"owners": {"user_id": {"_eq": "X-Hasura-User-Id"}}}}
Enter fullscreen mode Exit fullscreen mode

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:

-- Create the function
CREATE OR REPLACE FUNCTION insert_owner_for_document ()
  RETURNS TRIGGER
  AS $BODY$
DECLARE
  session_variables json;
BEGIN
  session_variables := current_setting('hasura.user', 't');
  INSERT INTO owners (document_id, user_id)
    VALUES (NEW.id, (session_variables->>'x-hasura-user-id')::INTEGER);
  RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;

CREATE TRIGGER trigger_insert_owner
  AFTER INSERT ON documents
  FOR EACH ROW
  EXECUTE PROCEDURE insert_owner_for_document ()
Enter fullscreen mode Exit fullscreen mode

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.

Example 3: Hierarchical roles in an organization

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"} }
            ]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

CREATE OR REPLACE VIEW flattened_user_roles AS
WITH RECURSIVE sub_tree AS (
  SELECT
    roles.id AS role_id,
    user_roles.user_id AS user_id
  FROM
    user_roles,
    roles
  WHERE
    roles.id = user_roles.role_id
  UNION ALL
  SELECT
    r.id AS role_id,
    st.user_id AS user_id
  FROM
    roles r,
    sub_tree st
  WHERE
    r.parent_role_id = st.role_id
)
SELECT
  *
FROM
  sub_tree;
Enter fullscreen mode Exit fullscreen mode

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.

Example 4: Attribute-based access control for a CMS

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"}}
    }
}
Enter fullscreen mode Exit fullscreen mode

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, tweet to us at @HasuraHQ or join us on Discord for more discussions on Hasura & GraphQL!

Sign up for our newsletter to know when we publish new articles.

Top comments (0)