Suppose I have a scenario where we have Users
and each user can create their own Projects
.
I'm trying to limit the Show
action of my Rails controller to only allow admin or the owner of the project to be able to go through Show
action.
The problem I am facing is, perhaps I'm misunderstanding on how to use Scopes in Pundit.
My Show
action looks like this:
def show
project = policy_scope(Project).find_by({id: project_params[:id]})
if project
render json: project
else
render json: { error: "Not found" }, status: :not_found
end
end
My Pundit Scope class looks like this:
class Scope < Scope
def resolve
if @user.admin?
scope.all
else
# obviously, if non-matching user id, an ActiveRelation of
# empty array would be returned and subsequent find_by(...)
# would fail causing my controller's 'else' to execute
# returning 404 instead of 403
scope.where(user_id: @user.id)
end
end
end
In my Rails test, I am trying to assert that non-project owner should receive a 403 forbidden:
test "show project should return forbidden if non admin viewing other user's project" do
# "rex" here is not the owner of the project
get project_path(@project.id), headers: @rex_authorization_header
assert_response :forbidden
end
My test is failing. I am getting the error:
Failure:
ProjectsControllerTest#test_show_project_should_return_forbidden_if_non_admin_viewing_other_user's_project [/Users/zhang/App_Projects/LanceKit/Rails_Project/LanceKit/test/controllers/projects_controller_test.rb:40]:
Expected response to be a <403: forbidden>, but was a <404: Not Found>.
Expected: 403
Actual: 404
I don't quite feel like I'm using Pundit correctly.
Should I be using Pundit's authorize project
instead of using policy_scope(Project)...
for the Show
action?
I was expecting the scope.where(...)
to detect the incorrect user id and return some error saying 'you are not authorized to view this resource' rather than returning results.
From what my test results are indicating to me, using scope for show
action is wrong.
My finding is telling me Pundit scope are only used for filtering a set of data to only return those that matches a condition, it does NOT check whether the current_user is the owner of the resource. Pundit scope does NOT raise a 403 Forbidden
error.
In other words, using scoping only in the show
action will lead to a semantic bug that's saying this project with id 3 does not exist in the database
for example instead of saying you are not authorized to view this project because it belongs to a different user
.
A summary for myself:
policy_scope
for index
actionauthorize
for show
, create
, update
, delete
use authorize
AND policy_scope
if you're not resource owner and trying to access some funky plural resource route like
get "/user/1/projects" => "Project.index"
in case you want to check if user is say a "project manager" or "collaborator" who is allowed to view your project. In this case, you would probably need to modify your scope code with an extra elsif
clause.
In relation to my above question, I modified my project to use authorize
inside my show
action:
def show
project = Project.find_by({id: project_params[:id]})
authorize project
if project
render json: project
else
render json: { error: "Not found" }, status: :not_found
end
end
This then raises the expected 403 Forbidden error that my tests is expecting and thus my test passes.