Motivation

Hasura Permissions are Hard

This project was born out of a few observations about Hasura’s Permission Rules:

  • Hasura permissions are hard to configure.
    • The amount of JSON quickly proliferates.
    • There aren’t any mechanisms to keep the rules DRY (e.g., there are no fragments).
    • The rules need to be replicated to lots of different tables.
  • Hasura permissions are hard to share with non-technical stakeholders.
    • They’re baked into Hasura’s YAML metadata.
    • They’re JSON that only engineers can understand.
  • Hasura permissions are hard to change.
    • They’re tightly coupled to application code.

Open Policy Agent: A Panacea

Open Policy Agent addresses a lot of these concerns, by the very act of decoupling policy decision-making from policy enforcement.

It frees the logic behind authorization and entitilements, codifying it in readable language that any stakeholder can read.

Open Policy Agent (OPA, pronounced “oh-pa”) … provides a high-level declarative language that lets you specify policy as code and simple APIs to offload policy decision-making from your software. You can use OPA to enforce policies in microservices, Kubernetes, CI/CD pipelines, API gateways, and more… OPA decouples policy decision-making from policy enforcement. When your software needs to make policy decisions it queries OPA and supplies structured data (e.g., JSON) as input.

The End Goal: OPA -> Hasura

How nice would it be if you could just sync your OPA policies straight into Hasura?

That is the dream of this project.

A CLI that you can use to simply import your OPA policies and sync them to your Hasura metadata.

Installation

Find us on Homebrew and tea.xyz

Architecture

Transpilation works in a few phases:

  1. We parse external policies (e.g., written in Rego) and transforms them into an internal structure.
  2. We convert that internal structure into a GraphQL AST.
  3. We convert that GraphQL AST into JSON for Hasura Permissions Rules.
  4. (Future) We sync the JSON into your Hasura metadata.

Abstract Syntax Trees (ASTs) 🌲

We rely heavily on several libraries to do the heavy lifting around ASTs:

Example OPA Policy Evaluation

OPA policies are expressed in the Rego Policy Language, a high-level declarative language purpose-built for expressing policies over complex hierarchical data structures.

Before we can evaluate a policy, we first need some data to evaluate it against!

Data

Here’s some example data:

{
  "orgUnits": [
    {"id": 1, "name": "US Dept of Education"},
    {"id": 2, "name": "NY State DoE", "parent": 1},
    {"id": 3, "name": "NYC Public Schools", "parent": 2},
    {"id": 4, "name": "NYC PS 46", "parent": 3},
    {"id": 5, "name": "NYC PS 99", "parent": 3}
  ],
  "classrooms": [
    {"id": 1, "orgUnitID": 4, "name": "1st Period"},
    {"id": 2, "orgUnitID": 4, "name": "2nd Period"},
    {"id": 3, "orgUnitID": 5, "name": "1st Period"},
    {"id": 4, "orgUnitID": 5, "name": "2nd Period"}
  ],
  "teachers": [
    {"id": 1, "orgUnitID": 4, "name": "Alice"},
    {"id": 2, "orgUnitID": 4, "name": "Bob"},
    {"id": 3, "orgUnitID": 5, "name": "Celeste"},
    {"id": 4, "orgUnitID": 5, "name": "Diego"}
  ],
  "teacherClassrooms": [
    {"teacherID": 1, "classroomID": 1},
    {"teacherID": 2, "classroomID": 2},
    {"teacherID": 3, "classroomID": 3},
    {"teacherID": 4, "classroomID": 4}
  ]
}

In this example, we’re keeping the JSON data as normalized as our database. OPA allows nesting, so we could nest the teachers array under classrooms… but that would diverge from our Hasura schema — which is ultimately what we want to map to!

Policy

Let’s suppose we need to implement some new authorization rules:

  • Teachers should only have write access to classrooms they teach.
  • Teachers should only have read access to classrooms in their school.
  • Teachers should only have read access to terminal org units which oversee classrooms they teach.
    • A terminal org unit is one with no subordinate org units (“no child org units”).

Modifying classrooms

A teacher should only be able to modify their own classrooms.

We can model this specific rule / policy with the following Rego:

package classroom_write_access

default allow = false

classroom_teacher[teacher_id] {
    teacherClassrooms[tc]
    tc.teacherID == teacher_id
}

allow {
    input.action == "modify"
    input.resource.type == "classrooms"
    input.teacher_id == input.resource.teacher_id
    classroom_teacher[input.teacher_id]
}

Reading other classrooms

A teacher should only be able to read data about classrooms that they either directly teach, or that belong their school.

package classroom_read_access

default allow = false

classroom_belongs_to_teacher_classroom(classroomID, teacherID) {
    teacherClassrooms[tc]
    tc.teacherID == teacherID
    tc.classroomID == classroomID
}

classroom_belongs_to_teacher_org_unit(classroomID, teacherID) {
    classroom_belongs_to_teacher_classroom(classroomID, teacherID)
    classrooms[classroom]
    classroom.id == classroomID
    orgUnits[ou]
    ou.id == classroom.orgUnitID
    teaches_in_classroom[teacherID][classroom.id]
}

teaches_in_classroom[teacherID] = {classroomID | classroom_belongs_to_teacher_classroom(classroomID, teacherID)}

teaches_in_org_unit[teacherID] = {classroomID | classroom_belongs_to_teacher_org_unit(classroomID, teacherID)}

allow {
    input.action == "read"
    input.resource.type == "classrooms"
    input.teacher_id == input.resource.teacher_id
    (classroom_belongs_to_teacher_classroom(input.resource.id, input.teacher_id) or
    classroom_belongs_to_teacher_org_unit(input.resource.id, input.teacher_id))
}

Reading org units

A teacher should only be able to read data about schools to which their classrooms belong.

package org_unit_read_access

default allow = false

direct_parent(orgUnitID, parentID) {
    orgUnits[parent]
    parent.id == parentID
    orgUnitID == parent.parent
}

teaches_in_classroom(teacherID, classroomID) {
    teacherClassrooms[tc]
    tc.teacherID == teacherID
    classrooms[classroom]
    tc.classroomID == classroom.id
    classroom.orgUnitID == orgUnitID
}

allow {
    input.action == "read"
    input.resource.type == "orgUnits"
    input.teacher_id == input.resource.teacher_id
    teaches_in_classroom[input.teacher_id][input.resource.id]
    orgUnits[ou]
    ou.id == input.resource.id
    direct_parent(ou.id, classroom.orgUnitID)
}

Command Line Tool

Import

The import command is used to ingest policies from policy engines.

Supported Policy Languages

Supported Policy Engines

Several policy engines are supported out of the box, by virtue of the fact that the Rego language is somewhat of a standard.

For example, Aserto supports Rego.

Future Support

We might consider the following languages in the future.