Modern applications require complex authorization, that's where Relationship-Based Access Control (ReBAC) comes in. I went through this research paper: Relationship-based access control: protection model and policy language in order to understand, learn, and implement it. Here's everything I learned along the way.
Table of Contents
- The Paper
- Why I Implemented It
- Tech Stack
- What is Relationship-Based Access Control?
- Context in ReBAC
- How Authorization Works
- The Implementation
The Paper
The paper proposes a type of access control characterized by the relationships between users and resources, and control policies based on those relationships. This allows authorization logic to take into account the context of the relationships, giving a high level of control over how resources are accessed.
Why I Implemented It
While job hunting, I recently came across an assignment where I had to implement ReBAC. I searched what it was, found it genuinely interesting, and here we are. I was also inspired by a friend on Twitter to go through an actual research paper.
Tech Stack
- Node.js
- TypeScript
- Oso Cloud
What is Relationship-Based Access Control?
A simple way to visualize it:
Student → Courses → Professors
- Relationship 1: A student is enrolled in a course
- Relationship 2: A course is taught by a professor We can combine these relationships to answer queries like "Which professor teaches which student?" - even though there's no direct relationship between them. What we're really talking about here is transitivity:
A => B
B => C
∴ A => C
Importantly, it doesn't just track whether a relationship exists, it also tracks the type of that relationship. That distinction gives us more context to work with.
Some important components of ReBac are:-
- User
- Resource
- Resource Owner
- Relationship Identifiers
- Context
Context in ReBAC
An important concept in ReBAC is context. The context includes all the relationships in the network, which are matched against policies to arrive at an authorization decision.
The viewer must be in a specific kind of relationship with the owner in order to access a resource.
Associated with every resource is an access control policy, a ternary predicate (3 inputs):
- Owner
- Viewer
- A Social Network (the graph of relationships between users, modeled using Relationship Identifiers) This predicate returns a boolean.
Notably, each context might result in a different authorization decision even if the resource, user, and owner are the same. For example: a close relative of a patient wants to access a medical record, when they opt to view, they're authorized; when they opt to edit, they're not.
There are 3 sources an access control policy can come from:
- Mandatory — set by the sysadmin on all resources
- Discretionary — set by the resource owner
- Policy Vocabulary — predefined policies users can choose from
The social network is the graph of users and resources, recording the relations articulated in a given context.
How Authorization Works
Authorization is achieved by traversing relationships in the social network. Here's the flow:
- The viewer attempts to access a resource
- The system looks up the policy predicate associated with the resource and its owner
- The effective social network (the context) is derived
- The system applies the predicate to the owner, viewer, and social network to arrive at an authorization decision Two important rules govern this:
- Rule 1: Only the social network aspect of the context is significant
-
Rule 2: Effective social networks of descendant contexts can inherit relationships defined in ancestor contexts, a child context contains no fewer relationships than its parent
The root context is the root node of the contextual hierarchy tree. The
extendsrelation defines this hierarchy. A tree-shaped hierarchy corresponds to the nested structure of relationship scopes:
c1 extends c2 if and only if c1 is either c2 or one of the descendants of c2.
The Implementation
The paper references an EHR (Electronic Health Records) system as its example, so that's what I implemented in TypeScript, using Oso Cloud.
Gitub Repo - rebac-ts-implementation
Step 1: Define the Rules in Polar
Oso uses its own policy language called Polar. Here's the schema I defined in Oso's rules editor:
actor Physician {}
actor User {
relations = {
emergencyContact: User
};
}
resource Institution {
roles = ["nurse", "admin"];
}
resource Case {
permissions = ["edit", "view"];
roles = ["editor", "viewer"];
relations = {
institution: Institution,
patient: User,
doctor: Physician
};
"edit" if "editor";
"view" if "viewer";
"viewer" if "editor";
# admin permissions
"editor" if "admin" on "institution";
# patient permissions
"viewer" if "patient";
# emergency contact permissions
"viewer" if "emergencyContact" on "patient";
# doctor permissions
"editor" if "doctor";
# nurse permissions
"editor" if "nurse" on "institution";
}
resource Treatment {
permissions = ["edit", "view"];
roles = ["editor", "viewer"];
relations = {
case: Case,
patient: User,
physician: Physician
};
"edit" if "editor";
"view" if "viewer";
"viewer" if "editor";
"editor" if "editor" on "case";
"viewer" if "editor";
"editor" if "physician";
"viewer" if "patient";
"viewer" if "emergencyContact" on "patient";
}
Step 2: Initialize the Project
Set up a new TypeScript project and add your Oso Cloud API key to .env:
OSO_KEY="your_api_key"
Then initialize the Oso client:
import dotenv from 'dotenv'
import { Oso } from 'oso-cloud';
import { addCases, addInstitutionStaff, addPatientsAndEmergencyContacts, treatmentsPatient1, treatmentsPatient2 } from './data.js';
import type { DefaultPolarTypes } from 'oso-cloud/dist/src/helpers.js';
dotenv.config()
const apiKey = process.env.OSO_KEY as string;
const oso = new Oso('https://cloud.osohq.com', apiKey)
Step 3: Add Facts (Roles & Relations)
For this example, we use two types of facts — has_role and has_relation:
// User to User relationship (emergency contact)
await oso.insert(["has_relation", {type: "User", id: "patient_1"}, "emergencyContact", {type: "User", id: "contact_1"}]);
// Resource to Resource relationship (Case belongs to Institution)
await oso.insert(["has_relation", {type: "Case", id: "case_b"}, "institution", {type: "Institution", id: "general_hospital"}]);
// Resource to Actor relationship (Case belongs to patient)
await oso.insert(["has_relation", {type: "Case", id: "case_b"}, "patient", {type: "User", id: "patient_2"}]);
Step 4: Authorize
The authorize function returns a boolean:
// Dr. Strange tries to edit a case they're not assigned to
let isAllowed = await oso.authorize(
{ type: 'Physician', id: 'dr_strange' }, // Actor
'edit', // Action
{ type: 'Case', id: 'case_a' } // Resource
);
console.log(isAllowed); // false — not assigned to dr_strange
// An emergency contact tries to view a treatment
isAllowed = await oso.authorize(
{ type: 'User', id: 'contact_1' },
'view',
{ type: 'Treatment', id: 'treat_a1' }
);
console.log(isAllowed); // true — legitimate emergency contact
All permissions and roles are derived from the Polar schema defined in the Oso Cloud console. (Oso also has a neat section where you can inspect all facts and query logs — really helpful for debugging.)
The full code is on GitHub: rebac-ts-implementation
References
If you have questions or feedback, feel free to reach out!

Top comments (0)