DEV Community

Hasura for Hasura

Posted on • Originally published at hasura.io on

Authorization rules for a multi-tenant system - Google cloud platform

We had earlier covered writing permission rules for some popular use cases. In this post we will look at writing permission rules for a multi-tenant system.

Authorization in a multi-tenant system usually means two things:

  1. Each user needs to only have access to resources from that tenant.
  2. A user's role depends on the tenant they are trying to access

If your use case does not require per tenant roles, then have a look at this gist for a simpler implementation. We will loosely base this post on predefined roles in Google cloud platform's (GCP) role-based access control system. GCP's role-based access control system is an interesting use case because it is not only multi-tenant, the roles within each tenant are also hierarchical.

GCP roles overview

In GCP, every resource type has roles associated with it. For example: access to Google cloud storage buckets is controlled using storage_admin, storage_editor, storage_viewer roles. Similarly roles compute_admin, compute_editor, compute_viewer, etc control access to the compute engines.

Roles can be per resource as well. For eg: Each bucket has a storage_admin, storage_editor, storage_viewer role associated with it. So a storage_viewer for a given bucket can access only that bucket but a someone with a global storage_viewer role can view any bucket.

Both global roles and per resource roles are hierarchical i.e a Storage Admin can do anything a Storage Editor can do, etc.

Finally, every role is per project so a user can be storage_admin in one project but be storage_editor in another.

Setup

Database Schema

We will assume the following schema:

We have a projects table and a users table to keep track of projects and users in the system. roles table has the list of all roles along with hierarchy information. user_project_roles maps users to their roles in a project and user_bucket_roles maps users to their roles for a given bucket.

We will also assume that we've created a one-to-one relationship user_bucket_roles from storage_buckets to user_bucket_roles. Note that keeping with the common convention, the relationship and the target table have the same name.

Note : While GCP has many resources, in the above schema we have only included the storage_buckets table. We will be looking at how to write permission rules for this table. Permission rules for other tables would be similar.

Creating a Storage Bucket in practice will probably involve coordinating between multiple services, allocating space, etc. The goal for this post is to only explain how to implement a system similar to GCP's role-based access control system. As such, we will conveniently ignore these complexities.

Flattening hierarchical roles

Similar to Example 3 in the previous post we will flatten user_project_roles and user_bucket_roles into flattened_user_project_roles and flattened_user_bucket_roles using the gist below. This allows us to not worry about the hierarchy in the roles.

For example if a user is assigned the storage_admin role in the user_project_rules table, flattened_user_project_roles will have 3 rows assigning them: storage_admin, storage_editor and storage_viewer.

Permission rules

Since we have modeled the roles in the database, we will use a single role called user in Hasura to define permissions rules. We will assume that the session variable X-Hasura-User-Id contains the user_id and X-Hasura-Project-Id contains the project that the user is trying to access.

Permission rules on storage_buckets

Select permissions:

A user can view a storage_bucket if they have the storage_viewer role on the given bucket or they have the global storage_viewer role.

We can implement the first rule with the following permission rule:

{
  "_exists": {
    "_table": {
      "schema": "public",
      "name": "flattened_user_project_roles"
    },
    "_where": {
      "_and": [
        {
          "user_id": {
            "_eq": "X-Hasura-User-Id"
          }
        },
        {
          "role_id": {
            "_eq": "storage_viewer"
          }
        },
        {
          "project_id": {
            "_eq": "X-Hasura-Project-ID"
          }
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The above JSON uses the _exists operator to check if there is a row in the flattened_user_project_roles with the given role, project_id and user_id.

The second rule can be implemented as:

{
  "user_bucket_roles": {
    "_and": [
      {
        "user_id": {
          "_eq": "X-Hasura-User-Id"
        }
      },
      {
        "role_id": {
          "_eq": "storage_viewer"
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above rule user_bucket_roles is a many-to-many relationship between storage_buckets and user_bucket_roles. Hasura evaluates the rule by fetching user_bucket_roles for the current row and validating that user_id and role_id are equal to the given values.

We can now put the two in an _or clause for the final rule:

{
  "_or": [
    {
      "_exists": {
        "_table": {
          "schema": "public",
          "name": "flattened_user_project_roles"
        },
        "_where": {
          "_and": [
            {
              "user_id": {
                "_eq": "X-Hasura-User-Id"
              }
            },
            {
              "role_id": {
                "_eq": "storage_viewer"
              }
            },
            {
              "project_id": {
                "_eq": "X-Hasura-Project-ID"
              }
            }
          ]
        }
      }
    },
    {
      "user_bucket_roles": {
        "_and": [
          {
            "user_id": {
              "_eq": "X-Hasura-User-Id"
            }
          },
          {
            "role_id": {
              "_eq": "storage_viewer"
            }
          }
        ]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Update permissions:

Update permissions would look the same as the select permissions except we would be using the role_id storage_editor instead of storage_viewer

We also we need to prevent the user from updating the project_id column of a bucket:

Delete permissions:

Delete permissions would again look the same as the select permissions except we would be using the role_id storage_admin instead of storage_viewer

Insert permissions:

Only a user with a storage_admin role should be able to create a bucket:

{
  "_exists": {
    "_table": {
      "schema": "public",
      "name": "flattened_user_project_roles"
    },
    "_where": {
      "_and": [
        {
          "user_id": {
            "_eq": "X-Hasura-User-Id"
          }
        },
        {
          "role_id": {
            "_eq": "storage_admin"
          }
        },
        {
          "project_id": {
            "_eq": "X-Hasura-Project-ID"
          }
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We also want the project_id of the created bucket to be X-Hasura-Project-Id. We can do this using column presets:

Permission rules on user_project_roles and user_bucket_roles

We also need rules that allow certain users to assign or remove the roles of other users. We can have roles role_admin & role_viewer for being able to edit and view user roles respectively. Permission rules on user_project_roles and user_bucket_roles would then look similar to the insert permission rule on storage_buckets with the role name changed.

Conclusion

In this post we've seen how to implement permission rules for a full-fledged hierarchical multi-tenant system. Postgres Views and Hasura's Permission DSL make a rather powerful combination!

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 (1)

Collapse
 
rodolfosilva profile image
Rodolfo Silva • Edited

Should we define a permission check for the project_id at storage_buckets? To avoid a user of another tenant ? Maybe I'm wrong. But i believe this implementation only check if the user has a permission to list all registry including all projects. @hasurahq_staff

Something like this

{
  "_and": [
    {
      "_exists": {
        "_table": {
          "schema": "public",
          "name": "flattened_user_project_roles"
        },
        "_where": {
          "_and": [
            {
              "user_id": {
                "_eq": "X-Hasura-User-Id"
              }
            },
            {
              "role_id": {
                "_eq": "storage_viewer"
              }
            },
            {
              "project_id": {
                "_eq": "X-Hasura-Project-ID"
              }
            }
          ]
        }
      }
    },
    {
      "project_id": {
        "_eq": "X-Hasura-Project-ID"
      }
    }
  ]
}

And:

{
  "_or": [
    {
      "_and": [
        {
          "_exists": {
            "_table": {
              "schema": "public",
              "name": "flattened_user_project_roles"
            },
            "_where": {
              "_and": [
                {
                  "user_id": {
                    "_eq": "X-Hasura-User-Id"
                  }
                },
                {
                  "role_id": {
                    "_eq": "storage_viewer"
                  }
                },
                {
                  "project_id": {
                    "_eq": "X-Hasura-Project-ID"
                  }
                }
              ]
            }
          }
        },
        {
          "project_id": {
            "_eq": "X-Hasura-Project-ID"
          }
        }
      ]
    },
    {
      "user_bucket_roles": {
        "_and": [
          {
            "user_id": {
              "_eq": "X-Hasura-User-Id"
            }
          },
          {
            "role_id": {
              "_eq": "storage_viewer"
            }
          }
        ]
      }
    }
  ]
}