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:
- We parse external policies (e.g., written in Rego) and transforms them into an internal structure.
- We convert that internal structure into a GraphQL AST.
- We convert that GraphQL AST into JSON for Hasura Permissions Rules.
- (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
- OpenPolicyAgent’s Rego language
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.