DEV Community

Dinesh
Dinesh

Posted on

Implementing Relationship Based Access Control (ReBac) using Node and Oso

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

  1. The Paper
  2. Why I Implemented It
  3. Tech Stack
  4. What is Relationship-Based Access Control?
  5. Context in ReBAC
  6. How Authorization Works
  7. 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


What is Relationship-Based Access Control?

A simple way to visualize it:

Student → Courses → Professors
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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:-

  1. User
  2. Resource
  3. Resource Owner
  4. Relationship Identifiers
  5. 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):

  1. Owner
  2. Viewer
  3. 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:

  1. Mandatory — set by the sysadmin on all resources
  2. Discretionary — set by the resource owner
  3. 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:

  1. The viewer attempts to access a resource
  2. The system looks up the policy predicate associated with the resource and its owner
  3. The effective social network (the context) is derived
  4. 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 extends relation 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

EHR Auth Tree

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";
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"}]);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)