dashb
dashb

Reputation: 157

Returning a 403 when a 404 occurs in Rails API

I have a Rails API with a controller that has a before_action on the create function that checks if the current_user creating the "paste" is also the owner of the specified room_id in paste_params.

If current_user.rooms.find(params[:paste][:room_id]).user_id isn't found or if it is found but isn't equal to current_user.id the server always returns a 404.

How can I go about making it return a 403 instead of a 404? Because this check is meant to determine whether the user creating the paste is also the owner of the room the paste will be linked to, if the user isn't the owner of that room it means they aren't authorized to create the paste under that room.

Here's the relevant parts of the controller:

class Api::V1::PastesController < ApplicationController
  before_action :check_room_owner, only: %i[create update destroy]

  def create
    paste = current_user.pastes.build(paste_params)
    if paste.save
      render json: PasteSerializer.new(paste).serializable_hash, status: :created
    else
      render json: { errors: paste.errors }, status: :unprocessable_entity
    end
  end

  private

  # Convert to 403 forbidden if not found
  def check_room_owner
    head :forbidden unless current_user.rooms.find(params[:paste][:room_id]).user_id === current_user.id
  end

end

Cheers!

Upvotes: 1

Views: 2861

Answers (3)

max
max

Reputation: 102046

This is known as authorization and if you really going to reinvent the wheel at least do it right:

# app/errors/authentication_error.rb
class AuthenticationError < StandardError
end
class ApplicationController
  rescue_from 'AuthenticationError', with: :deny_access

  def deny_access
    head :forbidden
  end
end
# Do not use :: when declaring classes!
module API
  module V1
    class PastesController < ApplicationController
      before_action :find_and_authenticate_room!

      def create
        paste = current_user.pastes.build(paste_params)
        if paste.save
          render json: PasteSerializer.new(paste).serializable_hash, status: :created
        else
          render json: { errors: paste.errors }, status: :unprocessable_entity
        end
      end

      private
      def find_and_authenticate_room!
        # This smells really bad - use a nested route instead!
        @room = Room.find(params[:paste][:room_id])
        raise AuthenticationError unless @room.user == current_user
      end
    end
  end
end

This separates the logic of responding from determining what is allowed and uses inheritance to DRY the whole process. Better yet would be to not reinvent the wheel and use Pundit or CanCanCan which separates the authorization rules from your controller which keeps it skinny.

Upvotes: 3

dashb
dashb

Reputation: 157

Mark's answer perfect, but I also found another solution that works quite well.

  def check_room_owner
    selected_room = current_user.rooms.find(params[:paste][:room_id]).user_id
    rescue ActiveRecord::RecordNotFound
     head :forbidden
  end

If selected_room doesn't get found rails will raise an ActiveRecord::RecordNotFound exception instead of leaving the variable undefined or nil. So we can use rescue to handle the exception and return a 403 instead of letting rails default to returning a 404.

Upvotes: 0

Mark
Mark

Reputation: 6455

I think you're looking for something like this:

class Api::V1::PastesController < ApplicationController

  def create
    paste = current_user.pastes.build(paste_params)
    return head :forbidden unless paste.room.user_id === current_user.id
    if paste.save
      render json: PasteSerializer.new(paste).serializable_hash, status: :created
    else
      render json: { errors: paste.errors }, status: :unprocessable_entity
    end
  end
end

Upvotes: 0

Related Questions