---
title: "How to Implement Relationship-Based Access Control (ReBAC) in a Ruby On Rails API?"
description: "The way to implement an authorization system depends on your application's needs. Let's explore Relationship-Based Access Control (ReBAC) and implement it in a Rails API using OpenFGA."
authors:
  - name: "Carla Urrea Stabile"
    url: "https://auth0.com/blog/authors/carla-stabile/"
date: "Feb 5, 2025"
category: "Developers,Tutorial,FGA"
tags: ["ruby", "rails", "fga", "openfga"]
url: "https://auth0.com/blog/what-is-rebac-and-how-to-implement-rails-api/"
---

# How to Implement Relationship-Based Access Control (ReBAC) in a Ruby On Rails API?

<details>
<summary>Starting from here?</summary>

If you want to start this tutorial from the beginning, [start from this blog post.](https://auth0.com/blog/what-is-rbac-and-how-to-implement-it-rails-api/)

Otherwise, to continue from where we left off, clone the application repo and check out the `add-abac` branch:

``` bash  
git clone -b add-abac https://github.com/auth0-blog/rails-api-authorization 
```

Make the project folder your current directory:

``` bash  
cd rails-api-authorization
```

Then, install the project dependencies:

``` bash  
bundle install
```

Then, setup the database

``` bash
  rails db:setup
```

Finally, create a `.env` hidden file:

``` bash  
touch .env
```

Populate `.env` using the `.env.example` file:

``` bash  
cp .env.example .env
```

Add your Auth0 configuration variables to the `.env` file and run the project by executing the following command:

``` bash  
rails s
```

</details>

Previously, it was on your access control series (*read with TV show presenter voice*). You learned about [Attribute-Based Access Control and integrating it into your Rails API](https://auth0.com/blog/what-is-abac-and-how-to-implement-it-rails-api). In this blog post, you’ll continue learning about [different authorization systems](https://auth0.com/blog/whats-the-right-authorization-model-for-my-application/), this time about Relationship-Based Access Control and how to iterate from ABAC to ReBAC. You’ll also learn about FGA and how it fits into the picture.  

## What Is Relationship-Based Access Control (ReBAC)?

Relationship-Based Access Control (ReBAC) is an authorization model in which a subject’s access to a certain resource is defined by the relationships between the subject and the resource. Think, for example, about any social network. Usually, users can connect to each other by following each other or sending friend requests, and you can limit what each user can see based on these relationships. 

Note that you can use ReBAC along with RBAC and ABAC. Roles and attributes can coexist with relationships in this case; in fact, roles or attributes can be relations 🤯, but it’s up to your business case to define whether it makes sense to have them all or simply keep one of the other. 

## ReBAC and Fine-Grained Access Control

Fine-grained access Control or [Fine-Grained Authorization](https://auth0.com/fine-grained-authorization) refers to the ability to grant specific users permission to perform certain actions on specific resources. These types of authorization systems allow you to scale to millions of objects and users, where permissions can change very rapidly. 

Both ReBAC and ABAC can be fine-grained but ReBAC allows you to make decisions on a database that has up-to-date data. ABAC can do this too but in most traditional implementations there's no database. This helps in scenarios where the data that influences the decision changes often.

## A Wild OpenFGA Appears!

There are many ways you can implement a Fine-Grained Authorization system; [OpenFGA](https://a0.to/fga-content) is one of those. It’s open source, and it’s inspired by Google’s Zanzibar, Google’s internal authorization system. In a nutshell, the **OpenFGA service answers authorization checks by determining whether a relationship exists between an object and a user.**

OpenFGA relies on ReBAC. Not only can you define relationships between subjects and objects, but it also facilitates the implementation of RBAC and even ABAC!

## ReBAC Implementation with OpenFGA in Your Rails API

Once the concepts are clear, let’s go back to the last implementation of our expense management system. [The last thing you heard from stakeholders](https://auth0.com/blog/what-is-abac-and-how-to-implement-it-rails-api/#Oh--No--What-s-Next-) was: 

> Users who are managers can see the reports of their directs, plus the reports of their directs’ directs, and so on 😵‍💫

At the moment, the way to implement something like this would be to add a new attribute to the user’s table `manager_id` and recursively check for managers of managers. This is not impossible, but since you’ve learned about OpenFGA, you decide it’s worth a try since they happen to have a similar case in their examples of an [expenses management app.](https://play.fga.dev/sandbox/?store=expenses) 

### What You Have Implemented So Far 

- An `admin` role that can see everything. 
- A submitter user who can only see their reports or where their ID matches the attribute `submitter_id` of the report. 
- An approver user who can only see the reports where their ID matches the attribute `approver_id` of the report. 

### What You’re Changing

Since you’re going to introduce a new requirement, you make the following decisions 🤠:

- **Rely on OpenFGA to dictate access rights**, meaning every time you need to check for access, you’ll use OpenFGA and not the local relationships in your database; these can be used for querying or other tasks. 
- **Remove the `admin` role**. If you remember from the initial blog post, this role was created to allow users to approve expenses, but having more granular control allows you to get rid of the role and rely on relationships.
- You are going to **keep the attributes `submitter_id` and `approver_id` for local checks and querying**. If you see it’s necessary, you can implement a task that assures that the relations you have in your database match the ones that live in OpenFGA (this is out of the scope of this blog post). 
- You are going to **add a `manager_id` to the `users` table** to represent the manager relation so your app knows about this relation

Sounds like a lot of work, right? You might have to make some changes in the app, but you’ll end up with a better and more robust authorization system. 

## Defining Relations

You’ve already mentioned some of the relationships you’ll have, but let’s summarize them. First, you will have two types: `user` and `report`. Remember, OpenFGA works with users and objects in your case; users will be users (duh), and objects will be reports. You can define an authorization model as a YAML file, such as: 

```yaml
model
  schema 1.1

type user

type report

```

...which is incomplete, so you need to define relations for each type. For example, you know that a user can be a manager of another user, so the relation `manager` is a good candidate. Let’s not forget about the recursiveness we discussed earlier. Luckily for us, it’s easier to define in OpenFGA, and you’ll do it with an [implied relationship](https://openfga.dev/docs/concepts#what-are-direct-and-implied-relationships) that you’ll call `can_manage`. This relationship *implies* that a user can manage another user when they are their manager, or they can manage the manager (and so on recursively). Let’s write it down:

```yaml
model
 schema 1.1

type user
  relations
    define can_manage: manager or can_manage from manager
    define manager: [user]

type report
```

Now, for the report, you’re interested in knowing who the `submitter` and the `approver` are, so these are the two relations you’ll create. The `submitter` relation is straightforward, but the `approver` relation is also implied because if a user is a manager of the submitter, then they can approve it. So let’s model that:

```yaml
model
 schema 1.1

type user
 relations
  define can_manage: manager or can_manage from manager
  define manager: [user]

type report
 relations
  define approver: can_manage from submitter
  define submitter: [user]
```

Great! So what you just did here was to define your [authorization model](https://openfga.dev/docs/concepts#what-is-an-authorization-model), which, as you can see, is where you define the relationships between your users and objects to later on make checks and determine access rights. 

Next, let's see what to do with this YAML file and where to put it to have a proper authorization model integrated with OpenFGA. 

## Integrate OpenFGA into a Rails API

The first thing you need to do is to integrate OpenFGA into your Ruby on Rails API. The fastest way to start testing [OpenFGA and play around is using Docker](https://openfga.dev/docs/getting-started/setup-openfga/docker#step-by-step). After pulling the Docker image, you can run it with the following: 

```bash
docker run -p 8080:8080 -p 8081:8081 -p 3000:3000 openfga/openfga run
```

The playground will run in port `3000` and the API is available in port `8080` and voila! You have your own OpenFGA server running on your local machine. 

Now you can copy the authorization model you created in the previous section; you can paste it into the playground's editor to see it there. You can also transform it to JSON format using FGA CLI like so:

```bash
fga model transform --file openfga/authorization-model.fga --output-format json > openfga/authorization-model.json
```

The next thing you need to do is **create a store and an authorization model**. In OpenFGA, a [store](https://openfga.dev/docs/concepts#what-is-a-store) is an entity used to organize authorization check data, and each store contains one or more versions of an [authorization model](https://openfga.dev/docs/concepts#what-is-an-authorization-model), that’s the JSON file you copied from the playground earlier, and where all your relations and types are defined. 

First of all, let’s store that JSON file somewhere. Create a new file `config/openfga_authorization_model.json` and paste the content of that JSON file. This way you have your authorization model ready to go when it’s time to create it in your OpenFGA server. 

But how do you interact with your OpenFGA server? Well, you’ll need to interact with the [OpenFGA API](https://openfga.dev/api/service), which you’ll do next. 

### Implementing an OpenFGAService Class 

In order to keep this modular, let’s create a service class to interact with the OpenFGA API. This service class will implement calls for the following endpoints: 

- Create a store
- Create an authorization model
- Update a relation 
- Perform a check
- List objects

Let’s create a new class. Create a new folder, `app/services`, and a new file in it, `app/services/openfga_service.rb`, and add the following content: 

```ruby
require ‘net/http’
require ‘uri’
require ‘json’

class OpenfgaService

  def self.make_post_request(path, body:)
    uri = URI.parse("#{ENV['FGA_API_URL']}/#{path}")
    request = Net::HTTP::Post.new(uri)
    request.content_type = "application/json"
    request.body = body

    Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
 http.request(request)
    end
  end

  def self.create_store
    response = make_post_request("stores", body: { name: "expenses" }.to_json)

    JSON.parse(response.body)[“id”] if response.code.to_i == 201 
  end

  def self.create_authorization_model(store_id)
    response = make_post_request("stores/#{store_id}/authorization-models", 
    body: File.read(Rails.root.join('config', 'openfga_authorization_model.json')))

    JSON.parse(response.body)[“authorization_model_id”] if response.code.to_i == 201
  end

  def self.update_relation(user, relation, object)
    return unless authorization_data

    store_id, authorization_model_id = authorization_data

    body = {
      writes: {
        tuple_keys: [
          {
            user: user,
            relation: relation,
            object: object
          }
        ]
      },
      authorization_model_id: authorization_model_id
    }.to_json

    response = make_post_request("stores/#{store_id}/write", body: body)

    response.body
  end

  def self.check(user, relation, object)
    return false unless authorization_data
        
    store_id, authorization_model_id = authorization_data

    body = {
      authorization_model_id: authorization_model_id,
      tuple_key: {
        user: user,
        relation: relation,
        object: object
      }
    }.to_json
        
    response = make_post_request("stores/#{store_id}/check", body: body)

    response.code.to_i == 200 ? JSON.parse(response.body)["allowed"] : false
  end
  
  def self.list_objects(user, relation)
    return unless authorization_data

    store_id, authorization_model_id = authorization_data

    body = {
      authorization_model_id: authorization_model_id,
      type: “report”,
      relation: relation,
      user: "user:#{user.id}",
      context: {},
      consistency: “MINIMIZE_LATENCY”
    }.to_json

    response = make_post_request("stores/#{store_id}/list-objects", body: body)

    response.code.to_i == 200 ? JSON.parse(response.body)["objects"].map{|obj| obj.split(":")[1].to_i} : []
  end

  # atuhorized if at least one of the relations is allowed: true
  def self.batch_check_relations(user, relations, object)
    return unless authorization_data

 store_id, authorization_model_id = authorization_data
        
 checks = relations.map do |relation|
 {
        "tuple_key": {
          “user”: user,
          “relation”: relation,
          “object”: object,
 },
      "correlation_id": SecureRandom.uuid 
 }
    end

 response = make_post_request("stores/#{store_id}/batch-check", 
 body: {authorization_model_id: "#{authorization_model_id}", checks: checks}.to_json)

    # # Response: 
    # {
    # “results”: {
    #     { "886224f6-04ae-4b13-bd8e-559c7d3754e1": { "allowed": false }}, # submmiter
    #     { "da452239-a4e0-4791-b5d1-fb3d451ac078": { "allowed": true }}, # approver
    #   }
    # } 
    # in our case if at least one of those is true, then the user can view the report. This is VERY specific for this 
    # use case!
    response.code.to_i == 200 ? JSON.parse(response.body)[“result”].values.map{|e| e.values}.flatten.any? : false
  end

  private 
  def self.authorization_data
    authorization = Authorization.first
    [authorization&.store_id, authorization&.model_id] if authorization
  end

end
```

This class uses [`Net::HTTP`](https://ruby-doc.org/stdlib-2.7.0/libdoc/net/http/rdoc/Net/HTTP.html) to make requests to the OpenFGA API. It’s basically a Ruby wrapper for making calls to the OpenFGA API. Note that you need to add your `FGA_API_URL` to your `.env` file. If using localhost, then the value is `http://localhost:8080`. 

The `create_authorization_model` function uses your JSON file to create the authorization model. 

One thing in particular for this application is how to store the information about your OpenFGA Store and Authorization Model. The function `authorization_data` retrieves information about the authorization store and model. You’ve decided to store it locally using a table `authorizations`. To do that, generate a new model and migration using the following command:

```bash
rails generate model Authorization store_id:string model_id:string
```

This will generate a model `app/models/authorization.rb`, and you’re going to add some validations to make sure things stay consistent: 

```ruby
class Authorization < ApplicationRecord
  validates :store_id, presence: true, uniqueness: true
  validates :model_id, presence: true, uniqueness: true
end
```

And a migration `db/migrate/YYYYMMDDHHMMSS_create_authorizations.rb`:

```ruby
class CreateAuthorizations < ActiveRecord::Migration[7.0]
  def change
    create_table :authorizations do |t|
      t.string :store_id
      t.string :model_id

      t.timestamps
    end
  end
end
```

You’re probably wondering when you stored that authorization data, right? Well, to answer that, I’m going to use the good old: "It depends." 

You need to create a store and authorization model **only once.** It’s up to you and your business to decide how and when to do this. In your case, you’ve decided to use a [custom rake task](https://guides.rubyonrails.org/v4.2/command_line.html#custom-rake-tasks).

When you deploy your app and run your server for the first time, for example, you can run this task manually, and it will create the store and authorization model. Another way to do it could be using a [Rails Initializer](https://guides.rubyonrails.org/configuring.html#using-initializer-files) to run after the server runs. 

You went with the rake task option, so you create this lovely task file that not only creates a store and an authorization model but also updates relations and performs checks because you’re using it for testing it locally: 

```ruby
namespace :openfga do
 desc “Create a store and authorization model”
  task create_store_and_model: :environment do
  store_id = OpenfgaService.create_store
    if store_id
      authorization_model_id = OpenfgaService.create_authorization_model(store_id)
      Authorization.create!(store_id: store_id, model_id: authorization_model_id)
      puts "Store ID: #{store_id}, Authorization Model ID: #{authorization_model_id}"
    else
      puts “Failed to create store”
    end
  end

 desc “Update relation”
    task :update_relation, [:user, :relation, :object] => :environment do |_t, args|
    result = OpenfgaService.update_relation("user:#{args[:user]}", args[:relation], "report:#{args[:object]}")
    puts “Update relation result: #{result}”
  end

 desc “Check authorization”
 task check_authorization: [:user, :relation, :object] => :environment do |_t, args|
    user = "user:#{args[:user]}"
    relation = "#{args[:relation]}"
    object = "report:#{args[:object]}"
    allowed = OpenfgaService.check(user, relation, object)
    puts “Authorization check: #{allowed}”
  end
end
```

To create a store and model, you can run the following command in your terminal: `rails openfga:create_store_and_model`. 

Now, back to where you were. With the service class, you’ve encapsulated all interactions with the OpenFGA API, so let’s actually make calls to it! 

### Add the manager relation to your Rails API 

At this point, you’ve defined a manager relation for OpenFGA but also want to keep a record in your local database. For that, let’s create a new association `manager` in `app/models/user.rb`: 

```ruby
class User < ApplicationRecord
 has_many :expenses, foreign_key: :submitter_id
 has_many :submitted_reports, class_name: 'Report', foreign_key: 'submitter_id'
 has_many :reports_to_review, class_name: 'Report', foreign_key: 'approver_id'
  
 has_one  :manager, class_name: 'User', foreign_key: 'manager_id' # 👈 new code

 validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, presence: true, uniqueness: true
 validates :auth0_id, presence: true, uniqueness: true
```

Now, let’s create a migration to add the `manager_id` to the `users` table. In your terminal, run the following command: 

```bash
rails generate migration AddManagerIdToUsers 
```

This will generate a new file under `db/migrate/YYMMDDHHMMSS_add_manager_id_to_users.rb`, and you’ll need to add the following code:

```ruby
class AddManagerIdToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :manager_id, :integer  # 👈 new code
  end
```

Don’t forget to run `rails db:migrate` in your terminal right after!

## Managing Relations 

You need to create a new relation in your Rails API at two points: when a user is created or updated and when a user submits an expense report. 

Let’s start when a user is created or updated; this means that you can either pass the `manager_id` at creation time or update time. To implement this, you’ll need to make the following changes in your `app/controllers/users_controller.rb`: 

```ruby
class UsersController < ApplicationController
 before_action :authorize
 before_action :set_user, only: %i[ show update destroy ]
  
  # GET /users
  def index
    @users = User.all

    render json: @users
  end

  # GET /users/1
  def show
    render json: @user
  end

  # POST /users
  def create
    @user = User.new(user_params)

    if @user.save
      # 👇 new code
      update_authorization_manager(user_params[:manager_id], "manager", @user) if user_params[:manager_id]
      render json: @user, status: :created, location: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /users/1
  def update
    if @user.update(user_params)
      # 👇 new code
      update_authorization_manager(user_params[:manager_id], "manager", @user) if user_params[:manager_id]
      render json: @user
    else
 render json: @user.errors, status: :unprocessable_entity
    end
  end

  # DELETE /users/1
  def destroy
    @user.destroy
  end

  private
  # Use callbacks to share common setup or constraints between actions.
  def set_user
    @user = User.find(params[:id])
  end

  # Only allow a list of trusted parameters through.
  def user_params
    params.require(:user).permit(:email, :auth0_id, :manager_id) # 👈 new code
  end
end
```

At this point, you’re allowing a new field, `manager_id`, to be sent in the controller’s `params` and then using it to create the relation using a method called `update_authorization_manager`...which you haven’t defined yet, but let’s do it. 

Similarly to how you [implemented the `Secured` concern](https://auth0.com/blog/what-is-rbac-and-how-to-implement-it-rails-api/#Securing-Endpoints-with-Auth0) when using roles, it’ll be nice to have all the OpenFGA things in one place as well and available for all controllers, so let’s create a new concern in `app/controllers/concerns/authorized.rb`:

```ruby
# frozen_string_literal: true

module Authorized
  extend ActiveSupport::Concern

  def authorized?(user, relations, object)
    if relations.size == 1
      OpenfgaService.check("user:#{user.id}", relation, "report:#{object.id}")
    else
      OpenfgaService.batch_check_relations("user:#{user.id}", relations,"report:#{object.id}")
    end
  end

  def reports(user, relation)
    return [] unless %w[submitter approver].include?(relation)

    OpenfgaService.list_objects(user, relation)
  end

  def update_authorization_manager(manager_id, relation, object)
    OpenfgaService.update_relation("user:#{manager_id}", relation, "user:#{object.id}")
  end

  def update_authorization_submitter(submitter_id, object)
    OpenfgaService.update_relation("user:#{submitter_id}", "submitter", "report:#{object.id}")
  end
end
```

The `Authorized` concern is a proxy between your controllers and the `OpenfgaService` class. Make sure to also add it in your `ApplicationController` under `app/controllers/application_controller.rb` like so:

```ruby
class ApplicationController < ActionController::API
  include Secured
  include Authorized # 👈 new code

  ADMIN='admin’
end
```

Similarly, let’s create the submitter relation for when a user submits an expense report. This happens when a new expense is created under `app/controllers/expenses_controller.rb`: 

```ruby
class ExpensesController < ApplicationController
  before_action :authorize
  before_action :set_user
  before_action :set_expense, only: %i[ show update destroy ]

  # GET users/:user_id/expenses
  def index
    @expenses = Expense.where(submitter_id: @user.id)

    render json: @expenses
  end

  # GET users/:user_id/expenses/1
  def show
    render json: @expense
  end

  # POST users/:user_id/expenses
  def create
    ActiveRecord::Base.transaction do
      @expense = Expense.new(expense_params)
      @expense.submitter_id = @user.id

      if @expense.save
        report = Report.create(expense: @expense, submitter_id: @expense.submitter_id)
         # 👇 new code
        update_authorization_submitter(@user.id, report) if report.persisted?
        render json: @expense, status: :created
      else
        render json: @expense.errors, status: :unprocessable_entity
        raise ActiveRecord::Rollback # Rollback the transaction if saving the expense fails
      end
    end
  end

  # PATCH/PUT users/:user_id/expenses/1
  # ... 
end
```

At this point, if you create new users and expense reports and check your OpenFGA playground, you’ll see them in the Tuples section at the bottom left!

> Note that you should also make sure that when a user or report is deleted, you update the relation in OpenFGA. For that, you use [the same endpoint](https://openfga.dev/api/service#/Relationship%20Tuples/Write) but pass a `deletes` object instead of a `writes` object in the request body. This is out of the scope of this blog post.

## Perform Checks for Authorization 

There are three primary endpoints where you need to check for authorization, and they all live in the `ReportsController` in `app/controllers/reports_controller.rb`: 

- `GET users/:user_id/reports/review` — see reports that the user hasn’t approved yet
- `PUT users/:user_id/reports/:id/approve` — approve a report of a user’s directs
- `GET users/:user_id/reports/submitted` — see user’s submitted reports 

Open the `app/controllers/reports_controller.rb` file and add the following code:

```ruby
class ReportsController < ApplicationController
  before_action :set_report, only: %i[ show approve ]
  before_action :set_user
  before_action :authorize

  # GET users/:user_id/reports/submitted
  def submitted
    # You no longer need to make these checks but only check on OpenFGA
    # ➖ validate_ownership(@user) do # user is the owner of these reports
    # ➖ @reports = Report.where(submitter_id: @user.id) # list only reports where user is submitter
    # 👇 new code
    @reports = Report.find(reports(@user, "submitter")) 

    render json: @reports if @reports
  end

  # GET users/:user_id/reports/review
  def review
    # You no longer need to make these checks but only check on OpenFGA
    # ➖ validate_ownership(@user) do # user is the owner of these reports
    # ➖ @reports = Report.where(approver_id: @user.id) # list only reports where user is approver
    # 👇 new code
    @reports = Report.find(reports(@user, "approver")).select{|r| r.status != "approved"}

    render json: @reports if @reports   
  end

  # PUT users/:user_id/reports/1/approve
  def approve
    # validate_roles [ADMIN] do # if user is admin
    #   if @report.is_approver?(@user) # if user is the approver for this report
    #     if Date.current.on_weekday? # can only approve on weekdays
    #       @report.status = "approved"
    #       @report.save
    #       render json: @report
    #     else
    #       render json: {message: "Can only approve on weekdays"}, status: 401
    #     end
    # end

    # 👇 new code
    if authorized?(@user, ["approver"], @report)
      if Date.current.on_weekday? # can only approve on weekdays
        @report.status = "approved"
        @report.save
        render json: @report
      else
        render json: {message: “Can only approve on weekdays”}, status: 401
      end
    else
        render json: {message: “You don’t have permission to approve this report”}, status: 401
    end
  end

  # GET users/:user_id/reports/1
  def show
    # 👇 new code
    # user can view if they are a submitter or approver of the report 
    if authorized?(@user, ["submitter", "approver"], @report)
      render json: @report
    else
      render json: {message: “You don’t have permission to view this report”}, status: 401
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_report
      @report = Report.find(params[:id])
    end

    def set_user
      @user = User.find(params[:user_id])
    end
end
```

To perform authorization checks, you’re using two different approaches. Let’s go action by action: 

```ruby
# GET users/:user_id/reports/submitted
def submitted
  @reports = Report.find(reports(@user, "submitter")) 

  render json: @reports if @reports
end

# GET users/:user_id/reports/review
def review
  @reports = Report.find(reports(@user, "approver")).select{|r| r.status != "approved"}

  render json: @reports if @reports   
end
```

The `submitted` action queries and shows the reports where the user is a submitter. This check is performed using the `reports` method, which internally calls the [list objects endpoint from OpenFGA](https://openfga.dev/api/service#/Relationship%20Queries/ListObjects). This endpoint returns a list of all the objects of the given type that the user has a relation with, in this case, a `submitter` relation. Similarly, the `review` action performs the same check but for the `approver` relation. 

Then the `approve` action performs a check to verify the user has a relation `approver` with the report:  

```ruby
# PUT users/:user_id/reports/1/approve
def approve
  if authorized?(@user, ["approver"], @report)
    if Date.current.on_weekday? # can only approve on weekdays
      @report.status = "approved"
      @report.save
      render json: @report
    else
      render json: {message: “Can only approve on weekdays”}, status: 401
    end
  else
    render json: {message: “You don’t have permission to approve this report”}, status: 401
  end
end
```

Finally, the `show` action allows a user to see a given report if they have any relation with it, like being a `submitter` or an `approver`:

```ruby
# GET users/:user_id/reports/1
def show
  # 👇 new code
  # user can view if they are a submitter or approver of the report 
  if authorized?(@user, ["submitter", "approver"], @report)
    render json: @report
  else
    render json: {message: “You don’t have permission to view this report”}, status: 401
  end
end
```

## Cleaning Up 🧹

You can clean up some stuff because you got rid of the admin role and verified ownership using the access token. 

Let’s begin with the `Secured` concern in `app/controllers/concerns/secured.rb`; you can remove the following code:

```ruby
# frozen_string_literal: true
module Secured
  extend ActiveSupport::Concern
  REQUIRES_AUTHENTICATION = { message: ‘Requires authentication’}.freeze
  BAD_CREDENTIALS = {
 message: ‘Bad credentials’
 }.freeze
  MALFORMED_AUTHORIZATION_HEADER = {
 error: ‘invalid_request’,
 error_description: ‘Authorization header value must follow this format: Bearer access-token’,
 message: ‘Bad credentials’
 }.freeze
  # 🧹INSUFFICIENT_ROLES = {
  # 🧹  error: 'insufficient_roles',
  # 🧹  error_description: ‘The access token does not contain the required roles’,
  # 🧹  message: ‘Permission denied’
  # 🧹}.freeze
  # 🧹NOT_OWNER = {
  # 🧹  error: 'not_owner',
  # 🧹  error_description: ‘The access token does not belong to the current user’,
  # 🧹  message: ‘Permission denied’
  # 🧹}.freeze

  def authorize
    token = token_from_request
    validation_response = Auth0Client.validate_token(token)
        @decoded_token = validation_response.decoded_token
        return unless (error = validation_response.error)
    render json: { message: error.message }, status: error.status
  end

  # 🧹def validate_roles(roles)
  # 🧹  raise ‘validate_roles needs to be called with a block’ unless block_given?
  # 🧹  return yield if @decoded_token.validate_roles(roles)

  # 🧹  render json: INSUFFICIENT_ROLES, status: :forbidden
  # 🧹end

  # 🧹def validate_ownership(current_user)
  # 🧹  raise ‘validate_ownership needs to be called with a block’ unless block_given?
  # 🧹  return yield if @decoded_token.validate_user(current_user)

  # 🧹  render json: NOT_OWNER, status: :forbidden
  # 🧹end

  private

  def token_from_request
    authorization_header_elements = request.headers['Authorization']&.split
    render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements
        unless authorization_header_elements.length == 2
    render json: MALFORMED_AUTHORIZATION_HEADER, status: :unauthorized and return
        end
    scheme, token = authorization_header_elements
    render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer'
    token
  end
```

Because you no longer need to check the user’s access token, you can also remove the following code from the `app/lib/auth0_client.rb` file: 

```ruby
# frozen_string_literal: true
require ‘jwt’
require ‘net/http’
class Auth0Client
  # Class members
  Response = Struct.new(:decoded_token, :error)
  Error = Struct.new(:message, :status)
  # 🧹 Token = Struct.new(:token) do
  # 🧹  def validate_roles(roles)
  # 🧹    required_roles = Set.new roles
  # 🧹    token_roles = Set.new token[0][Rails.configuration.auth0.roles]
  # 🧹    required_roles <= token_roles
  # 🧹  end
  # 🧹  def validate_user(current_user)
  # 🧹    current_user.auth0_id == token[0]["sub"]
  # 🧹  end
  # 🧹end
  Token = Struct.new(:token)

  #... 
```

Finally, you can remove the `AUTH0_ROLES` environment variable from `config/auth0.yml`

```yaml
development:
  domain: <%= ENV.fetch('AUTH0_DOMAIN') %>
  audience: <%= ENV.fetch('AUTH0_AUDIENCE') %>
  # 🧹 roles: <%= ENV.fetch('AUTH0_ROLES') %>

production:
  domain: <%= ENV.fetch('AUTH0_DOMAIN') %>
  audience: <%= ENV.fetch('AUTH0_AUDIENCE') %>
  # 🧹 roles: <%= ENV.fetch('AUTH0_ROLES') %>
```

...and your `.env` file: 

```ruby
CLIENT_ORIGIN_URL=http://localhost:4040
AUTH0_AUDIENCE=
AUTH0_DOMAIN=
# 🧹 AUTH0_ROLES=
FGA_API_URL=
```

## Conclusion 

Throughout this series, you took an expense management application and iterated it to implement different authorization systems. 

Starting with no authorization at all, roles using Auth0 and an Auth0 Action are added to add a custom claim to the access token, access control over attributes is implemented, and finally, ReBAC with OpenFGA is implemented. 

Overall, you should use the authorization system that best fits your business needs, and it’s ok to mix them up and have roles with ReBAC if that’s what your app needs. 

Learn more about [OpenFGA](https://a0.to/fga-content) and [OktaFGA](https://a0.to/okta-fga-content) in the blog and our docs.