Reputation: 157
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
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
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
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