Tekko

Bahasa

Hubungi Kami

Biasanya merespons dalam 24 jam

Kembali ke BlogSecurity

Scaling SaaS Authorization: Implementing ReBAC with OpenFGA and Go

7 mnt baca
OpenFGAGoAuthorizationReBACMicroservices
Scaling SaaS Authorization: Implementing ReBAC with OpenFGA and Go

For years, Role-Based Access Control (RBAC) was the gold standard for application security. It’s simple: a user is an 'Admin' or an 'Editor,' and those roles have a predefined list of permissions. But as SaaS platforms evolve into complex ecosystems of nested folders, shared workspaces, and granular resource sharing, RBAC begins to crumble under its own weight.

If you have ever found yourself creating roles like Project_A_Lead_With_View_Access_To_Folder_B, you are experiencing 'role explosion.' This is where Relationship-Based Access Control (ReBAC) comes in. Inspired by Google’s Zanzibar paper, ReBAC shifts the focus from what a user is to how a user relates to a resource.

In this article, we will explore how to implement a scalable ReBAC system using OpenFGA (Fine-Grained Authorization) and Go, specifically tailored for the demands of multi-tenant SaaS architectures.

The Shift from RBAC to ReBAC

In a traditional RBAC system, permissions are usually stored as flags on a user record or in a join table. This works until a customer says, "I want to share this specific document with this specific external contractor, but only if they are part of the 'Reviewers' group in this specific project."

Trying to model this with RBAC requires either complex application logic or a messy proliferation of roles. ReBAC solves this by modeling authorization as a graph. In ReBAC, access is determined by a chain of relationships:

  1. User A is a member of Team B.
  2. Team B is an owner of Project C.
  3. Project C contains Document D.
  4. Therefore, User A has owner access to Document D.

OpenFGA, an open-source implementation of the Zanzibar concepts, provides a specialized engine to store these relationships (as 'tuples') and query them with sub-millisecond latency.

Core Concepts of OpenFGA

Before we dive into the Go implementation, we need to understand the three pillars of an OpenFGA model:

1. The Authorization Model (DSL)

This defines your types and their relationships. It’s the schema for your permissions. For a SaaS app, you might define types like user, workspace, and document.

2. Relationship Tuples

These are the facts stored in the system. A tuple looks like: (object) is related to (user) via (relation). Example: document:budget_2024#viewer@user:anne means Anne is a viewer of the budget document.

3. The Check API

This is the query: "Can user X perform action Y on resource Z?" OpenFGA traverses the graph of tuples and the model logic to return a boolean allowed result.

Modeling a Multi-Tenant SaaS

Let’s design a model for a project management tool. We have organizations, projects, and tasks. We want inheritance: if you are an admin of an organization, you should have access to all projects within it.

model schema 1.1 type user type organization relations define admin: [user] define member: [user] or admin type project relations define parent: [organization] define owner: [user] define viewer: [user] or owner or admin from parent define can_edit: owner or admin from parent type task relations define parent: [project] define assignee: [user] define viewer: assignee or viewer from parent

In this model, notice the viewer from parent syntax. This is the power of Zanzibar. We don't need to write a tuple for every user for every task. We simply define the relationship between a task and its parent project, and OpenFGA handles the recursive lookup.

Implementing ReBAC in Go

Now, let's look at how to integrate this into a Go backend. We will use the official OpenFGA Go SDK.

Setting up the Client

First, initialize the client. In a production environment, you would typically run OpenFGA as a separate service (or sidecar) and connect via gRPC or HTTP.

import ( fga "github.com/openfga/go-sdk" "context" "os" ) func NewFgaClient() (*fga.APIClient, error) { configuration, err := fga.NewConfiguration(fga.UserConfiguration{ ApiUrl: os.Getenv("FGA_API_URL"), // e.g., http://localhost:8080 StoreId: os.Getenv("FGA_STORE_ID"), ModelId: os.Getenv("FGA_MODEL_ID"), }) if err != nil { return nil, err } return fga.NewAPIClient(configuration), nil }

Writing Relationship Tuples

When a user creates a new project in your SaaS app, you need to inform OpenFGA about this relationship. This is usually done within the same database transaction that creates the project (or immediately after via an outbox pattern).

func (s *ProjectService) CreateProject(ctx context.Context, projectID, userID, orgID string) error { // 1. Save project to your primary DB (PostgreSQL/MySQL) // ... db logic ... // 2. Write relationship tuples to OpenFGA body := fga.WriteRequest{ Writes: &fga.TupleKeys{ TupleKeys: []fga.TupleKey{ { User: fga.PtrString(fmt.Sprintf("user:%s", userID)), Relation: fga.PtrString("owner"), Object: fga.PtrString(fmt.Sprintf("project:%s", projectID)), }, { User: fga.PtrString(fmt.Sprintf("organization:%s", orgID)), Relation: fga.PtrString("parent"), Object: fga.PtrString(fmt.Sprintf("project:%s", projectID)), }, }, }, } _, _, err := s.fgaClient.OpenFgaApi.Write(ctx).Body(body).Execute() return err }

Performing Permission Checks

In your middleware or service layer, you can now perform a check. This replaces the old if user.Role == "admin" logic.

func (s *ProjectService) CanUserEditProject(ctx context.Context, userID, projectID string) (bool, error) { body := fga.CheckRequest{ TupleKey: &fga.CheckRequestTupleKey{ User: fga.PtrString(fmt.Sprintf("user:%s", userID)), Relation: fga.PtrString("can_edit"), Object: fga.PtrString(fmt.Sprintf("project:%s", projectID)), }, } resp, _, err := s.fgaClient.OpenFgaApi.Check(ctx).Body(body).Execute() if err != nil { return false, err } return *resp.Allowed, nil }

Architecting for Scale and Multi-Tenancy

When implementing ReBAC at scale, there are three critical considerations: consistency, latency, and tenant isolation.

1. Store-per-Tenant vs. Shared Store

OpenFGA supports multiple "Stores." In a multi-tenant SaaS, you have two choices:

  • Store-per-tenant: Provides hard isolation. Good for compliance. However, it makes global reporting or cross-tenant features difficult.
  • Shared Store with Prefixed IDs: All tenants share a store, and you prefix objects with tenant IDs (e.g., tenant1:project:abc). This is usually easier to manage and allows for global admin features.

2. The "New Stack" Problem

Zanzibar systems introduce the concept of "Zookies" (consistency tokens). When you write a relationship, OpenFGA returns a token. If you perform a Check immediately after a Write, you should pass this token to ensure the check is performed against a version of the data that includes your recent write. This prevents the "I just shared this with you, why can't you see it?" support tickets.

3. Performance and Caching

OpenFGA is designed to be fast, but network round-trips add up.

  • Parallel Checks: If you are rendering a list of 50 projects, don't call Check 50 times in a loop. Use the ListObjects API or perform checks in parallel.
  • Local Caching: For high-frequency checks (like "is this user authenticated?"), use a short-lived local cache. However, be wary of cache invalidation for authorization—security is better served by fresh data.

Dealing with Complexity: The Trade-offs

While ReBAC is powerful, it is not a silver bullet. It introduces an external dependency for every authorization decision. If OpenFGA is down, your application is effectively locked.

Reliability Strategy: Use a highly available deployment of OpenFGA (e.g., on Kubernetes with a distributed database like CockroachDB or Postgres). Implement a fallback mechanism or a "fail-closed" policy in your Go code.

Modeling Overhead: Designing a ReBAC model requires more upfront thought than RBAC. You must think in terms of graph relationships. Start simple: model your core resources first, then expand as requirements grow.

Conclusion

Moving to ReBAC with OpenFGA and Go allows you to decouple your authorization logic from your core business logic. This leads to a cleaner codebase and a much more flexible product. Instead of hardcoding permissions, you provide a platform where users can define their own complex sharing structures.

Actionable Next Steps:

  1. Audit your current roles: Identify where you are using logic like if user.id == resource.owner_id. These are prime candidates for ReBAC.
  2. Experiment with the FGA Playground: Use the OpenFGA Playground to model your domain before writing any code.
  3. Start with a Hybrid Approach: You don't have to migrate everything. Start by moving one complex resource (like a shared folder system) to OpenFGA while keeping simple global roles in your primary database.