What is Authorization?

Let's assume that task-1 is assigned to John and task-2 is assigned to Mary. The way the code is written, John can see Mary's tasks and Mary can see John's task. That's not right.

Authorization is all about what all things a user can do.

Let's set a simple rule in our application. A user is allowed to see a task only if the task is assigned to that user or if that task is created by that user. In this chapter we will work towards enforcing this rule.

Introducing Pundit gem

We will be using Pundit gem to do authorization work.

Pundit helps us in creating role based authorization. It helps us to create policies with simple Ruby Classes.

Pundit gem installation

Add the gem to the Gemfile.

1gem "pundit"

Install the gem.

1bundle install

Include Pundit in the application controller:

1class ApplicationController < ActionController::Base
2  include Pundit
3end

Introducing Policies

Policies contain the authorization for an action. Let's create a policy.

1mkdir app/policies
2touch app/policies/task_policy.rb

Open task_policy.rb and add following lines of code.

1class TaskPolicy
2  attr_reader :user, :task
3
4  def initialize(user, task)
5    @user = user
6    @task = task
7  end
8
9  def show?
10      # some condition which returns a boolean value
11  end
12end

Pundit makes the following assumptions about this class.

  • The class has the same name as that of model class, only suffixed with the word "Policy". Hence, the name TaskPolicy.
  • The first argument is a user. In your controller, Pundit will call the current_user method, which we had defined in ApplicationController, to retrieve what to send into this argument.
  • The second argument is that of a model object, whose authorization you want to check.
  • The class implements some kind of query method, in this case show?. Usually, this will map to the name of a particular controller action.

Adding policy check in TaskPolicy

Now let's look at the required code for class TaskPolicy

1class TaskPolicy
2  attr_reader :user, :task
3
4  def initialize(user, task)
5    @user = user
6    @task = task
7  end
8
9  # The show policy check is invoked when we call `authorize @task`
10  # from the show action of tasks controller.
11  # Here the condition we want to check is that
12  # whether the record's creator is current user or record is assigned to the current user.
13  def show?
14    task.creator_id == user.id || task.user_id == user.id
15  end
16
17  # The condition for edit policy is the same as that of the show.
18  # Hence, we can simply call `show?` inside the edit? policy here.
19  def edit?
20    show?
21  end
22
23  # Similar in the case for update? policy.
24  def update?
25    show?
26  end
27
28  # Every user can create a task, hence create? will always returns true.
29  def create?
30    true
31  end
32
33  # Only the user that has created the task, can delete it.
34  def destroy?
35    task.creator_id == user.id
36  end
37end

Now, let's add the authorize method to our controller actions. The required code is added as follows.

1class TasksController < ApplicationController
2  before_action :authenticate_user_using_x_auth_token
3  before_action :load_task, only: %i[show update destroy]
4
5  def index
6    tasks = policy_scope(Task)
7    render status: :ok, json: { tasks: tasks }
8  end
9
10  def create
11    @task = Task.new(task_params.merge(creator_id: @current_user.id))
12    authorize @task
13    if @task.save
14      render status: :ok,
15             json: { notice: t('successfully_created', entity: 'Task') }
16    else
17      errors = @task.errors.full_messages
18      render status: :unprocessable_entity, json: { errors: errors }
19    end
20  end
21
22  def show
23    authorize @task
24    task_creator = User.find(@task.creator_id).name
25    render status: :ok, json: { task: @task, assigned_user: @task.user,
26                                task_creator: task_creator }
27  end
28
29  def update
30    authorize @task
31    is_not_owner = @task.creator_id != current_user.id
32
33    if task_params[:authorize_owner] && is_not_owner
34      render status: :forbidden, json: { error: t('authorization.denied') }
35    end
36
37    if @task.update(task_params.except(:authorize_owner))
38      render status: :ok, json: {}
39    else
40      render status: :unprocessable_entity,
41             json: { errors: @task.errors.full_messages.to_sentence }
42    end
43  end
44
45  def destroy
46    authorize @task
47    if @task.destroy
48      render status: :ok, json: {}
49    else
50      render status: :unprocessable_entity,
51             json: { errors: @task.errors.full_messages }
52    end
53  end
54
55  private
56
57  def task_params
58    params.require(:task).permit(:title, :user_id, :authorize_owner)
59  end
60
61  def load_task
62    @task = Task.find_by_slug!(params[:slug])
63  rescue ActiveRecord::RecordNotFound => e
64    render json: { errors: e }, status: :not_found
65  end
66end

Let's define authorizatin.denied message in our en.yml file to access it in the update action:

1en:
2  authorization:
3    denied: "Access denied. You are not authorized to perform this action."
4  session:
5    could_not_auth: "Could not authenticate with the provided credentials."
6    incorrect_credentials: "Incorrect credentials, try again."
7  successfully_created: "%{entity} was successfully created!"

And in our app/javascript/src/components/Tasks/EditTask.jsx file, whenever we are making the API call, send the authorization_owner param as true, so that in backend we can verify whether the one who is modifying the task is the owner itself or not:

1// --------previous code --------
2
3const handleSubmit = async event => {
4  event.preventDefault();
5  try {
6    await tasksApi.update({
7      slug,
8      payload: {
9        task: { title, user_id: userId, authorize_owner: true },
10      },
11    });
12    setLoading(false);
13    Toastr.success("Successfully updated task.");
14    history.push("/");
15  } catch (error) {
16    setLoading(false);
17    logger.error(error);
18  }
19}

We have added the line authorize @task to show, update and destroy actions after initializing our @task instance variable.

The authorize method automatically infers that Task will have a matching TaskPolicy class, and instantiates this class, handing in the current user and the given record (@task in this case). It then infers from the action name, that it should call the respective action of class TaskPolicy. For example, the instance created by the authorize @task inside the show action, will call the show? action of Policy class.

Rest of the code is straightforward. But in TaskController#update, we are executing update action fully, as part of a request from EditTask page, only when the current_user is the creator of that task. Otherwise we are raising an authorization_error. Whenever we make an API post call to backend from EditTask page, we are passing an authorize_owner param that triggers an authorization check to make sure that the owner is the one who is trying to edit the task.

Now, we can remove the conditional check for owner from client side and delegate it to backend TaskController#update action. Therefore update the updateTask handler in app/javascript/src/components/Tasks/ShowTask.jsx, like so:

1const updateTask = () => {
2  history.push(`/tasks/${taskDetails.slug}/edit`);
3};

From a high level overview, pundit authorize method in TaskController#show action, works something similar to below mentioned code:

1unless TaskPolicy.new(current_user, @task).show?
2  raise Pundit::NotAuthorizedError, "not allowed to show? this #{@task.inspect}"
3end

This raises an exception. But Pundit allows us to rescue the exception with a method of our choice. So, let's add the code for that in our application_controller.rb.

1class ApplicationController < ActionController::Base
2  protect_from_forgery with: :exception
3  include Pundit
4  rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized_user
5#------previous code -------
6
7  private
8
9#------previous code -------
10  def handle_unauthorized_user
11      render json: { error: "Permission Denied" }, status: :forbidden
12  end
13end

So whenever an action in TaskPolicy returns false it means the authorization has failed. When an authorization fails then we are raising an exception. The exception is rescued by method handle_unauthorized_user.

Let's say there exists a task with slug of task-slug and a user with name "John". Task with slug of task-slug is neither created by John nor assigned to John. John logs in by entering his email and password. Now John, enters the url localhost:3000/tasks/task-slug/show.

Since John is not authorized to view the task because of the above assumptions, rather than throwing him an error (the red page), Pundit raises an exception and rescues it by calling the method handle_unauthorized_user.

Introducing Policy scope

If you closely observe, we did not make a change to our index action. As index action returns a collection of records, we need to apply a condition on a collection, and we do that by using Policy Scope.

As we want to have our index view to display only the tasks which are either created by the current_user or assigned to the current_user, we will define a class called a policy scope. This class is nested inside the class TaskPolicy

1class TaskPolicy
2
3 #------previous code -------
4
5 #------add new lines here----
6  class Scope
7    attr_reader :user, :scope
8
9    def initialize(user, scope)
10      @user = user
11      @scope = scope
12    end
13
14    def resolve
15      scope.where(creator_id: user.id).or(scope.where(user_id: user.id))
16    end
17  end
18end

Pundit makes the following assumptions about this class:

  • The class has the name Scope and is nested under the policy class.
  • The first argument is a user. In your controller, Pundit will call the current_user method to retrieve what to send into this argument.
  • The second argument is a scope which is a collection of records.
  • Instances of this class respond to the method resolve.
  • This method contains the query run on the scope defined and then returns a result which is a collection and can be iterated over.

The corresponding change we make in the index action of Tasks controller is as follows:

1def index
2  #------new line added here------
3  @tasks = TaskPolicy::Scope.new(current_user, Task).resolve
4  #-----end of added line-------
5end

Let's observe what's going on here-

  • Pundit creates an instance of class Scope (nested inside the class TaskPolicy) passing along the current_user and the Task model (our scope in this case) as parameters which get set in the @user and @scope instance variables respectively inside the initialize method.

  • Now this instance calls the method resolve where we run a query on our scope and it returns a collection of tasks which only have the tasks that are either created by or assigned to the current_user.

Now to make it easier Pundit provides syntactic sugar where we can replace the line TaskPolicy::Scope.new(current_user, Task).resolve simply with the syntax policy_scope(Task). It works exactly the same way as described before.

So now our code looks like:

1def index
2  #------new line added here------
3  @tasks = policy_scope(Task)
4  #-----end of added line-------
5end

This way we have authorized our user for all the controller actions and hence you can try running this in your application.

Now let's commit these changes:

1git add -A
2git commit -m "Added pundit task policy"