Blog

From RBAC to Fine-Grained Authorization part I: design your model

Migrate your RBAC implementation to Fine-Grained Authorization (FGA) using WorkOS. Learn what is FGA, how to define resources, relationships, and inheritance rules, and how to test and validate the access model.


Role-based access control (RBAC) has been around for decades. It’s a model that simplifies access control by assigning users to roles and roles to permissions. A user can be assigned the role of admin, while the role of admin is assigned full view and edit permissions on an app, page, or database table.

However, RBAC has limitations and is often insufficient for modern applications. Challenges like maintenance complexity, role explosion, lack of flexibility, and impact on user and developer experience, are often impossible to overcome. Fine-Grained Authorization (FGA) solves all of these problems.

In this post, we will see what FGA is and how you can use it to cover your authorization needs. To do so, we will present a standard RBAC model and see how we can migrate it to FGA using WorkOS.

We will:

  • Design resource types that model the application’s authorization requirements.
  • Define the relationships between resources.
  • Test and validate the access model.

In the second part of this post, we will see how you can:

  • Query which resources users have access to.
  • Make fast access checks for a specific user.
  • Manage your FGA implementation.
  • Favor performance or consistency on a per request basis depending on your application's consistency requirements.

FGA 101

Fine-grained authorization (FGA) is an advanced access control model that considers several factors to decide whether a user should have access to a resource. These factors may include a user’s role, seniority, location, or the time of day. This allows you to tailor each user's access to the resources they need and thus tighten security.

For example, while a RBAC rule might specify that [admins] can [edit] [reports], a fine-grained rule can specify that [user:1] can [edit] [report:xsd34] on the last day of every month.

FGA can go even deeper and decide not only if the user should be allowed to access a resource, but which data in a resource they can access. For example, a user might be allowed to view some data of a report, but not other.

A user's access is determined by evaluating all the permissions that apply to them and verified with each request to the system.

WorkOS FGA uses the following basic concepts:

  • Resource types: They represent different types of resources and their possible relationships in your application. Think users and documents.
  • Warrants: Access rules that specify relationships between the resources in your application. They specify a relationship between a resource and a specific subject, e.g., [user:1] is a [member] of [role:admin].
  • Schema language: A domain-specific language (DSL) designed to express authorization models. It transpiles to JSON, making it possible to use alongside existing JSON resource-types.

With WorkOS FGA you can define your authorization model once and enforce it across microservices, applications, cloud environments, and more. You can define and manage your resources, hierarchies, access policies, and inheritance rules, from the FGA dashboard or programmatically with the FGA API or the WorkOS CLI.

You can also protect access to your apps and systems using one of the FGA SDKs. Currently, we support FGA on the WorkOS Node, Go, Kotlin, and Python SDKs, but we are working hard to add support in many more. If you need an SDK you don’t see, contact us!

Set up your environment

Before we dive in, let’s set up the WorkOS environment:

  • Sign up for a WorkOS account.
  • Copy your WorkOS API key from the dashboard.
  • Install the WorkOS SDK you will be using. In this post we will be using Node. To follow along this example install the Node SDK with npm install @workos-inc/node or yarn add @workos-inc/node.

If you want to follow along using a different SDK, like Go or Python, you can find code samples in the FGA API.

Our RBAC model

We will implement authorization for a simplified version of an Applicant Tracking System.

Our system will have four types of resources: applicants, feedback, teams, and users. Our authorization model should meet the following requirements:

  1. Users can be owners, editors, or viewers of an applicant.
  2. Teams can be owners, editors, or viewers of an applicant.
  3. The owner of an applicant can also edit the applicant’s data.
  4. The editor of an applicant can also view the applicant’s data.
  5. Every user belongs to a team.
  6. If a team is the owner, editor, or viewer of an applicant, then all members of the team inherit the same privileges.
  7. Every applicant might have associated feedback, if the applicant reached the interview stage.
  8. Every applicant’s feedback has an owner (who is the user who wrote the feedback for the applicant) and viewers
  9. The owners of an applicant can see that applicant’s feedback.

Define the resource types

The first step is to define the application’s resources as resource types.

The resource types are the basic building blocks of any authorization scheme in FGA. They represent resources (like users or documents) and their possible relationships with other resources in your application.

We will define the resource types using the schema language. It is simpler, more human-readable, transpiles to JSON, and can be used alongside existing JSON resource-types. You can use the schema editor in the FGA dashboard to apply a new schema to your environment or convert JSON syntax to schema language syntax.

Each schema must start with a version declaration which dictates the version of the schema language the transpiler will use to convert the schema into its JSON representation.

To define a resource type, use the type keyword followed by the name of the resource type. Let’s start by defining the resource types for our application:

    
version 0.2

type user

type team

type applicant

type feedback
  

Go to the FGA dashboard and copy paste the resource types. Click Apply.

Every time you want to make changes to the schema, go back to this screen, edit the schema, and click Apply.

Define relationships for each resource

Next, we need to define the relationships available on a resource of that type. For example, if we want to specify that [user:A] is a [member] of [team:B], we must add a member relation to the team resource type.In our application, the following relationships apply:

  • applicants can have owners, editors, and viewers
  • owners, editors, and viewers of an applicant can be either a user or a team
  • a team has members
  • feedback has a parent applicant (the applicant for whom the feedback is for)
  • feedback has an owner who is the user that added that feedback for the applicant
  • feedback has viewers but not editors (not everyone can view the feedback, and the feedback cannot be edited)

Let’s add these relationships to our resource types. To do so, we will use the relation keyword followed by the name of the relation. We will use brackets [] after the relation type to define the resource type of the relation. For example, the relation editor [user] means that only a user can be an editor.

    
version 0.2

type user

type team
   relation member [user]

type applicant
   relation owner [user, team]
   relation editor [user, team]
   relation viewer [user, team]

type feedback
   relation parent [applicant]
   relation owner [user]
   relation viewer [user]
  

With these resource types, we can now create authorization rules that specify exactly which users are owners, editors, and viewers of each applicant. We can also assign users as members of teams.

Define inheritance rules

While using explicitly assigned relations to build your authorization model can be powerful, doing it for each and every relationship in an application can become tedious or infeasible for complex use cases. To avoid this situation, we can define rules under which resources inherit relations. For example, we can define a rule that automatically sets a user as an editor of an applicant if they're also the applicant's owner.

Relations can be inherited with relation inheritance or resource inheritance.

Relation inheritance

Relation inheritance makes use of the fact that relations often overlap. For example, in many applications a user with write privileges inherits read privileges too. In our example application, an owner will inherit both the editor and the viewer relations, and an editor will inherit the viewer relation. Instead of explicitly assigning these relations, we can specify an inheritance hierarchy using the inherit keyword.

Let’s add inherit rules to our applicant resource type specifying that:

  • members of the owner team are also owners
  • owners are also editors
  • members of the editor team are also editors
  • editors are also viewers
  • members of the viewer team are also viewers
    
version 0.2

type user

type team
    relation member [user]

type applicant
    relation owner [user, team]
    relation editor [user, team]
    relation viewer [user, team]

    // members of the owner team are also owners 
    inherit owner if
        relation member on owner [team]

    inherit editor if
        any_of
            // owners are also editors
            relation owner
            // members of the editor team are also editors 
            relation member on editor [team]

    inherit viewer if
        any_of
            // editors are also viewers
            relation editor
            // members of the viewer team are also viewers
            relation member on viewer [team]

type feedback
   relation parent [applicant]
   relation owner [user]
   relation viewer [user]
  

Resource inheritance

Resource inheritance is used when there are hierarchical relationships between responses and this should be depicted in the access rules. For example, the owner of a folder is the owner of any document in that folder. In this case we have two different resources (folder, document) that are linked hierarchically. To define a resource inheritance rule, specify the relation, the resource type to inherit from, and the relation to inherit based on.

Let’s add a resource inheritance rule to our feedback resource type specifying that the owners of the parent applicant can see that applicant’s feedback.

    
type feedback
    relation parent [applicant]
    relation owner [user]
    relation viewer [user]

    inherit viewer if
        relation owner on parent [applicant]
  

Note that you can also compose multiple inheritance rules together using logical operators. For more details on this, refer to the inheritance rules docs.

The final schema

Now that we finished defining our schema, let’s see how the final version looks and how it maps to our initial requirements.

Our initial requirements were:

  1. Users can be owners, editors, or viewers of an applicant.
  2. Teams can be owners, editors, or viewers of an applicant.
  3. The owner of an applicant can also edit the applicant’s data.
  4. The editor of an applicant can also view the applicant’s data.
  5. Every user belongs to a team.
  6. If a team is the owner, editor, or viewer of an applicant, then all members of the team inherit the same privileges.
  7. Every applicant might have associated feedback, if the applicant reached the interview stage.
  8. Every applicant’s feedback has an owner (who is the user who wrote the feedback for the applicant) and viewers
  9. The owners of an applicant can see that applicant’s feedback.

Our final schema is:

    
version 0.2

type user

type team
   // Req. #5: Every user belongs to a team.
   relation member [user]

type applicant
    // Req. #1: Users can be owners, editors, or viewers of an applicant.
    // Req. #2: Teams can be owners, editors, or viewers of an applicant.
    relation owner [user, team]
    relation editor [user, team]
    relation viewer [user, team]

    // Req. #6: If a team is the owner of an applicant, all team members are too.
    inherit owner if
        relation member on owner [team]

    inherit editor if
        any_of
            // Req. #3: The owner is also an editor.
            relation owner
            // Req. #6: If a team is the editor of an applicant, all team members are too.
            relation member on editor [team]

    inherit viewer if
        any_of
            // Req. #4: The editor is also a viewer.
            relation editor
            // Req. #6: If a team is the viewer of an applicant, all team members are too.
            relation member on viewer [team]

type feedback
    // Req. #7: Every applicant might have associated feedback.
    relation parent [applicant]
    // Req. #8: Every applicant’s feedback has an owner.
    relation owner [user]
    // Req. #8: Every applicant’s feedback has viewers.
    relation viewer [user]

    // Req. #9: The owners of an applicant can see that applicant’s feedback.
    inherit viewer if
        relation owner on parent [applicant]
  

Apply the final schema before you proceed to the next step. Go to FGA dashboard, copy and paste the final schema, and click Apply.

Define the access rules

To define the access rules for our model we will use warrants. Warrants specify relationships between resources (e.g. user:A is a member of team:B). WorkOS FGA uses the defined warrants and resource types for an application to answer access checks and queries.

Each warrant is composed of three core attributes and an optional policy:

  • Resource: The resource the warrant specifies a relationship on. The resource is broken down into resource_type and resource_id and is represented with the format resource_type:resource_id. For example, user:123 refers to the resource_type=user  and resource_id=123.
  • Relation: The relationship the warrant specifies between the resource and the subject. Must be one of the defined relations on the referenced resource_type, e.g. editor, viewer, etc.
  • Subject: The resource being granted the specified relation. Like the resource, it’s broken down into resource_type and resource_id, and optionally a relation (to specify a group of subjects).  
  • Policy: The policy specifies an additional boolean expression to be evaluated at the time of each access control check. The provided expression can reference arbitrary variables which can be provided in check or query requests as context. It can be used to implement a form of attribute-based access control (ABAC).

Let’s create the following warrant for our example using the Node SDK: user:r8iuk is the owner of applicant:e7max .

Note that creating warrants also creates the resources if they don’t already exist. So in this example, if user:r8iuk and/or applicant:e7max do not exist, they will be created.

    
import { WorkOS, WarrantOp } from '@workos-inc/node';

const workos = new WorkOS('WORKOS_API_KEY');

const warrantResponse = await workos.fga.writeWarrant({
  op: WarrantOp.Create,
  resource: {
    resourceType: 'user',
    resourceId: 'r8iuk',
  },
  relation: 'owner',
  subject: {
    resourceType: 'applicant',
    resourceId: 'e7max',
  },
});
  

If you want to use a different SDK, check out the API for more code samples.

Now let’s create a warrant that assigns all members of team HR as editors of applicant:e7max .

    
import { WorkOS, WarrantOp } from '@workos-inc/node';

const workos = new WorkOS('WORKOS_API_KEY');

const warrantResponse = await workos.fga.writeWarrant({
  op: WarrantOp.Create,
  resource: {
    resourceType: 'team',
    resourceId: 'HR',
  },
  relation: 'editor',
  subject: {
    resourceType: 'applicant',
    resourceId: 'e7max',
  },
});
  

Remember that when we defined our model we added this relation inheritance rule:

    
inherit editor if
    any_of
        relation owner
        relation member on editor [team]
  

So according to this, assigning a team as editor of an applicant automatically assigns as editors all resources that are members of the team.

We can also create a batch of warrants:

  • user:mexjc is the editor of applicant:q1xba
  • user:a9zz4 is the viewer of applicant:q1xba
  • user:mexjc is a member of team:wnuhf
  • user:a9zz4 is a member of team:wnuhf
    
import { WorkOS, WarrantOp } from '@workos-inc/node';

const workos = new WorkOS('WORKOS_API_KEY');

const warrantResponse = await workos.fga.batchWriteWarrants([
  {
    op: WarrantOp.Create,
    resource: {
      resourceType: 'user',
      resourceId: 'mexjc',
    },
    relation: 'editor',
    subject: {
      resourceType: 'applicant',
      resourceId: 'q1xba',
    },
  },
  {
    resource: {
      resourceType: 'user',
      resourceId: 'a9zz4',
    },
    relation: 'viewer',
    subject: {
      resourceType: 'applicant',
      resourceId: 'q1xba',
    },
  },
  {
    resource: {
      resourceType: 'user',
      resourceId: 'mexjc',
    },
    relation: 'member',
    subject: {
      resourceType: 'team',
      resourceId: 'wnuhf',
    },
  },
  {
    resource: {
      resourceType: 'user',
      resourceId: 'a9zz4',
    },
    relation: 'member',
    subject: {
      resourceType: 'team',
      resourceId: 'wnuhf',
    },
  },
]);
  

When we want to revoke access we can delete warrants using WarrantOp.Delete. In this example, we remove user:mexjc from the editors of applicant:q1xba .

    
import { WorkOS, WarrantOp } from '@workos-inc/node';

const workos = new WorkOS('WORKOS_API_KEY');

const warrantResponse = await workos.fga.writeWarrant({
  op: WarrantOp.Delete,
  resource: {
    resourceType: 'applicant',
    resourceId: 'q1xba',
  },
  relation: 'editor',
  subject: {
    resourceType: 'user',
    resourceId: 'mexjc',
  },
});
  

For more details and code samples, check the Warrants API Reference.

Test and validate the access model

Before we start using our access model, we need to test it. Validating changes will also be an important step as you're iterating on the access model. In this section, we will see:

  • How to test our access model (resource types) and its access rules (warrants) with the WorkOS CLI using assertions.
  • How to create a test script for repeatable, automated testing.

Test assertions

If you haven’t used the WorkOS CLI before, check our docs for instructions on how to install and configure it.

Let’s assume that in our access model, the user with Id 1 should have the right to view applicant with Id john-doe but not to edit it.

The following should return true since user:1 is a viewer of applicant:john-doe.

    
workos fga check user:1 viewer applicant:john-doe --assert true
  

Similarly, we can check false assertions. The following should return true since user:1 is not an editor of applicant:john-doe.

    
workos fga check user:1 owner applicant:john-doe --assert false
  

Create a test script

Assertions are an easy way to quickly validate your schema using simple test cases. As you're iterating on your object types schema, it might be helpful to manually run assertions. However, as your schema becomes more complex, or if you need to create a regression test suite, automating assertions via a script is the best option.

Building on the assertion examples from above, we can create a basic shell script that:

  • sets up test data
  • runs assertions, and
  • tears down the test data
    
#!/bin/sh

# Set up the relations of the users with the applicant (1 owner, 1 editor, 1 viewer)
workos fga warrant create user:1 owner applicant:x
workos fga warrant create user:2 editor applicant:x
workos fga warrant create user:3 viewer applicant:x

# User 1 is owner, editor, viewer of applicant:x
workos fga check user:1 owner applicant:x --assert true -w latest
workos fga check user:1 editor applicant:x --assert true -w latest
workos fga check user:1 viewer applicant:x --assert true -w latest

# User 2 is only editor, viewer of applicant:x
workos fga check user:2 owner applicant:x --assert false -w latest
workos fga check user:2 editor applicant:x --assert true -w latest
workos fga check user:2 viewer applicant:x --assert true -w latest

# User 3 is only viewer of applicant:x
workos fga check user:3 owner applicant:x --assert false -w latest
workos fga check user:3 editor applicant:x --assert false -w latest
workos fga check user:3 viewer applicant:x --assert true -w latest

# Cleanup
workos fga resource delete user:1
workos fga resource delete user:2
workos fga resource delete user:3
workos fga resource delete applicant:x
  

Note that we use -w latest after each assertion. This makes sure that the assertion will use the most up-to-date data and can come in handy in scripts where reads follow immediately after writes. To learn more about consistency, see our docs.

Save the script as test.sh and run it:

    
chmod +x test.sh 
./test.sh
  

A test script like this can be used to manually run a test suite or as part of a CI workflow for managing your object types schema.

Next steps

In this post, we talked about what FGA is and how you can use it to cover your app’s authorization needs. We went through a simple RBAC model and saw how we can migrate it to FGA using WorkOS, and how you can test it.

Once we have our model in place, we need to start using it. The next steps are:

  • Make access checks that determine whether a user should have access to a resource.
  • Check for particular permissions on resources.
  • Manage your FGA implementation.
  • Favor performance or consistency on a per request basis depending on your application's consistency requirements.

Check out the second part of this guide to learn how to do all of the above with WorkOS FGA: From RBAC to Fine-Grained Authorization part II: integrate with your app.

In this article

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.