Search
⌘K
    to navigateEnterto select Escto close

    Authorization

    What is authorization?

    Authorization is a process in which we allow or restrict access to certain resources.

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

    So, let us introduce authorization to our application.

    Features

    By the end of the chapter, our application will have the following changes.

    • Only the tasks assigned to, or created by the logged in user, will be displayed in their dashboard.
    • Only the creator or assignee will be allowed to view the details of a Task. For others, the request will result in authorization error.
    • Only the creator of a task will be allowed to delete it.
    • Only the creator is allowed to change title or to re-assign the task.

    Technical design

    We will be using Pundit gem to do authorization work.

    Pundit helps us in managing role based authorization using policies defined in simple Ruby Classes.

    • We will create a new Ruby class named TaskPolicy and define the authorization conditions in it.
    • Pundit associates methods in this class with our controller actions. It executes the method corresponding to the action when a request is received. If its result is false, access is denied and an error is thrown. Otherwise the action will be executed as normal.
    • This approach don't work for our index action. We need it to run for every authenticated user. The requirement is to narrow down the results to only the ones that are relevant to the user.
    • We can use the policy scopes of pundit to filter the tasks. It will let us add additional conditions on the method we use to fetch tasks from database. Therefore the result will contain only those tasks that the requesting user is authorized to see.

    Let us see this in action.

    Pundit gem installation

    Add the this line to the Gemfile:

    1gem "pundit"

    Now, 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.task_owner_id == user.id || task.assigned_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  # Only owner is allowed to update a task.
    24  def update?
    25    task.task_owner_id == user.id
    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.task_owner_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 = Task.all.as_json(include: { assigned_user: { only: %i[name id] } })
    7    render status: :ok, json: { tasks: tasks }
    8  end
    9
    10  def create
    11    task = current_user.created_tasks.new(task_params)
    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.to_sentence
    18      render status: :unprocessable_entity, json: { error: errors }
    19    end
    20  end
    21
    22  def show
    23    authorize @task
    24  end
    25
    26  def update
    27    authorize @task
    28    if @task.update(task_params)
    29      render status: :ok, json: {}
    30    else
    31      render status: :unprocessable_entity,
    32        json: { error: @task.errors.full_messages.to_sentence }
    33    end
    34  end
    35
    36  def destroy
    37    authorize @task
    38    if @task.destroy
    39      render status: :ok, json: {}
    40    else
    41      render status: :unprocessable_entity,
    42        json: { error: @task.errors.full_messages.to_sentence }
    43    end
    44  end
    45
    46  private
    47
    48    def task_params
    49      params.require(:task).permit(:title, :assigned_user_id)
    50    end
    51
    52    def load_task
    53      @task = Task.find_by(slug: params[:slug])
    54      unless @task
    55        render status: :not_found, json: { error: t('task.not_found') }
    56      end
    57    end
    58end

    Let's define authorization.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, assigned_user_id: userId }
    10      }
    11    });
    12    setLoading(false);
    13    history.push("/");
    14  } catch (error) {
    15    setLoading(false);
    16    logger.error(error);
    17  }
    18};

    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.

    From a high level overview, pundit authorize method in TasksController#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_authorization_error
    5#------previous code -------
    6
    7  private
    8
    9  #------previous code -------
    10    def handle_authorization_error
    11      render status: :forbidden, json: { error: t("authorization.denied") }
    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_authorization_error.

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

    Since Oliver 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_authorization_error.

    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(task_owner_id: user.id).or(scope.where(assigned_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.

    Now, replace your index method with the following lines:

    1def index
    2  tasks = policy_scope(Task)
    3  tasks_with_assigned_user = tasks.as_json(include: { assigned_user: { only: %i[name id] } })
    4  render status: :ok, json: { tasks: tasks_with_assigned_user }
    5end

    Now, we have added authorization to all our actions in task controller. But what if we forgot to call authorize function in every action? Or if someone changed the code and removed it by accident?

    It would be a serious security loophole.

    To catch such errors in a fail-fast way, Pundit provides us some helper methods.

    Add these lines to our TasksController:

    1class TasksController < ApplicationController
    2  after_action :verify_authorized, except: :index
    3  after_action :verify_policy_scoped, only: :index
    4  before_action :authenticate_user_using_x_auth_token
    5  before_action :load_task, only: %i[show update destroy]
    6  # previous code...

    In addition to before filters, you can also run filters after an action has been executed. The after filters are registered via after_action.

    These Rails filters help us keep the code DRY.

    The above code will ensure we get an error if policy_scope or authorize methods were not called during execution of the action. Refer the official documentation of verify_authorized and verify_policy_scoped for better insight.

    The reason we call verify_authorized and verify_policy_scoped with after_action is because we can only check whether policies have been applied or not, only after the policies have had an opportunity to act i.e. that the controller action has run completely. Thus we can only use the after filter.

    You can learn more about how Pundit helps in ensuring policies and scopes are used from the official documentation.

    We have successfully implemented authorization in our application.

    Now let's commit these changes:

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