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"}}
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
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:
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"}}}
]
}
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 select
s, update
s and delete
s 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 ()
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:
We can now write the permission rule for select
s 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!
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;
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"}}
}
}
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!
Sign up for our newsletter to know when we publish new articles.
Originally published at https://hasura.io on March 16, 2020.