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.