RBAC to Relationship-Based Authorization

MD
R
Markdown

Modern applications require sophisticated authorization systems that go beyond traditional role-based access control (RBAC). Here's how relationship-based access control is changing the landscape. Application DB = "What exists" OpenFGA = "Who can do what" -- // RBAC can't easily handle: "Users can edit documents in their department" "Managers can approve expenses under $1000" "Share document with external collaborator" RBAC: System features, admin areas, broad permissions Relationship: Resource sharing, team access, fine-grained control

The Problem: Authorization in Code Most applications handle permissions like this:

// Traditional approach: Authorization mixed with business logic
async function canEditDocument(userId, docId) {
  const doc = await db.documents.findById(docId);
  const user = await db.users.findById(userId);
  return user.isEditor && doc.departmentId === user.departmentId;
}

Problems:

Multiple database queries Logic scattered across codebase Hard to maintain and modify Scales poorly

The Solution: Relationship-Based Authorization

  1. Define Your Model
// One-time setup: Define relationships
const model = {
  schema_version: "1.1",
  type_definitions: [{
    type: "document",
    relations: {
      editor: {
        this: {
          computedUserset: {
            object: "department",
            relation: "member"
          }
        }
      }
    }
  }]
};
// When creating a document
await fgaClient.write({
  tuples: [{
    // Document belongs to department
    object: `document:${docId}`,
    relation: "department",
    user: `department:${departmentId}`
  }]
});

// When assigning user to department
await fgaClient.write({
  tuples: [{
    // User is member of department
    user: `user:${userId}`,
    relation: "member",
    object: `department:${departmentId}`
  }]
});

// Your application code becomes this simple
async function canEditDocument(userId, docId) {
  const check = await fgaClient.check({
    user: `user:${userId}`,
    relation: 'editor',
    object: `document:${docId}`
  });
  return check.allowed;
}

Benefits Cleaner Code Single API call for checks Authorization logic centralized Easy to audit and modify Better Performance Optimized permission checking Reduced database load Built for scale Easier Maintenance Change rules without changing code Clear relationship modeling Consistent across applications Getting Started Choose a solution (OpenFGA, Oso, Casbin) Model your basic relationships Migrate highest-value permissions first Gradually expand usage

Real-World Example

// Creating a document with proper authorization
async function createDocument(title, departmentId, creatorId) {
  // 1. Create document in your database
  const doc = await db.documents.create({ title });

  // 2. Store authorization relationships
  await fgaClient.write({
    tuples: [{
      // Creator owns document
      user: `user:${creatorId}`,
      relation: "owner",
      object: `document:${doc.id}`
    }, {
      // Document belongs to department
      object: `document:${doc.id}`,
      relation: "department",
      user: `department:${departmentId}`
    }]
  });

  return doc;
}

Remember: Think of tuples as "facts" about who can do what. The authorization service uses these facts to answer permission questions.

=========== EXAMPLE 2 =============

// Application DB - documents table
{
  id: "doc123",
  title: "Q4 Report",
  created_by: "user456",  // ← Keep this minimal reference
  content: "..."
}
// OpenFGA tuples
[
  // Authorization relationships
  {
    user: "user:456",
    relation: "owner",
    object: "document:123"
  },
  {
    user: "user:456",
    relation: "editor",
    object: "document:123"
  }
]

// Permission check
const canEdit = await fgaClient.check({
  user: "user:456",
  relation: "editor",
  object: "document:123"
});

Created on 1/24/2025