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:
use policy_scope for index action
use authorize 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.
Pundits docs regarding scopes state that you can indeed use them for the show action:
def index
#posts = policy_scope(Post)
end
def show
#post = policy_scope(Post).find(params[:id])
end
Just using authorize may not be enough if a User (manually) opens a URL with a id param of an instance, that she should not be able to view.
To avoid a RecordNotFound error, I used the recommended NilClassPolicy:
class NilClassPolicy < ApplicationPolicy
class Scope < Scope
def resolve
raise Pundit::NotDefinedError, "Cannot scope NilClass"
end
end
def show?
false # Nobody can see nothing
end
end
Related
So i am trying to set a validation in post model if someone tries to edit in the url and enter another post number like:
from this
http://localhost:3000/posts/1
to this
http://localhost:3000/posts/112318278
and when user enter it should show some error msg or redirect to some other page or stuff...
how to do that ?
First, you can't. Take StackOverflow for example
https://stackoverflow.com/questions/73189649 if I want to change this to a different id, I can and it will work https://stackoverflow.com/questions/73189659
Second, your backend will attempt at finding the post with the given id, from your controller. One thing you could do is put a rescue clause in the finder.
(this is one of many ways to do it, but it's the simplest)
def find_post
#post = Post.find(params[:id]) # => This will raise an error if the post does not exist
rescue ActiveRecord::RecordNotFound => e # => This will rescue that error, and send the user to a different page, and not the Rails' 404 page
redirect_to(some_path, notice: 'This post does not exist')
end
I was handed a project that another developer worked on, without leaving any documentation behind. The code fetches some purchases from a shopping website, looks for a price and notifies the user.
The app may encounter errors like "no results found" and then I raise a standarderror.
I want to redirect the user to the error page and notify them about it but I can't do that because it isn't a controller, so the redirect_to option doesn't work.
services/purchase_checker.rb is called once an hour:
def call
user.transaction do
store_purchase
if better_purchase?
update_purchase
end
end
rescue MyError=> e
store_error(e)
end
def store_error(error)
user.check_errors.create!(error_type: error.class.name, message: error.message)
end
services/my_error.rb:
class MyError< StandardError
def initialize(error_type, error_message)
super(error_message)
#error_type = error_type
end
attr_reader :error_type
end
services/purchase_fetcher.rb:
def parse_result_page
raise purchase_form_page.error if purchase_form_page.error.present?
offer = purchase_page.map{|proposal_section|
propose(proposal_section, purchase) }
.min_by(&:price)
offer or raise MyError.new("No results", "No results could be found")
end
you should create another err class, eg NotFoundError:
offer or raise NotFoundError.new("No results", "No results could be found")
then in your controller:
begin
parse_result_page
rescue NotFoundError => e
redirect_to err_page, :notice => e.message
end
Since this is running in a job, the best way to notify the user would be by email, or some other async notification method. When an error is detected, an email is sent.
If that's not an option for some reason, you can check if a user has check_errors in any relevant controllers. Looking at the store_error(error) method that is called when an error is found, it seems it's creating a new record in the Database to log the error. You should be able to check if a user has any error logged via the user.check_errors relationship.
You could do it like this, for example:
class SomeController < ActionController::Base
# ...
before_action :redirect_if_check_errors
# ...
def redirect_if_check_errors
# Assuming you're using Devise or something similar
if current_user && current_user.check_errors.exists?
redirect_to some_error_page_you_create_for_this_path
end
end
end
This will check for these errors in every action of SomeController and redirect the user to an error page you should create, where you render the errors in the user.check_errors relationship.
There are multiple ways to do this, but I still think sending an email from the Job is a better option if you want to actively notify the user. Or perhaps add an interface element that warns the user whenever user.check_errors has stuff there, for example.
I propose that you do this synchronously so that the response can happen directly in the request/response cycle. Perhaps something like this:
# controller
def search
# do your searching
# ...
if search_results.blank?
# call model method, but do it synchrously
purchase_check = PurchaseChecker.call
end
if purchase_check.is_a?(MyError) # Check if it's your error
redirect_to(some_path, flash: { warning: "Warn them"})
end
end
# model, say PurchaseChecker
def call
# do your code
rescue MyError => e
store_error(e)
e # return the error so that the controller can do something with it
end
I am trying to create a module which houses the standard crud functions. whether this can be done, is wise, stand practice, or not, i would like to find out for myself. So far I have created the standard get requests no problem. However I am trying to implement a create action and am encountering a 'stack level too deep error'.
class FlagsController < ApplicationController
include CrudConcern
before_action lambda { crud_index(Flag.all) }, only: :index
before_action lambda { crud_new(Flag.new) }, only: :new
before_action lambda { crud_create(Flag.new, flags_path) }, only: :create
def create
end
end
crud module
def crud_create(model, route)
variable = model(params)
if variable.save
flash[:notice] = "Saved!"
redirect_to route
else
flash[:error] = "Try again"
render :new
end
end
Why would this occur? Is there a way around it? There is a Gem called Crudify which offers this so i assume it can be done.
Thanks
It looks like you are passing in an instance of a model, rather than the model class your method expects. I think you mean the following instead:
# in controller
before_action lambda { crud_create(Flag, flags_path) }, only: :create
# in crud module
def crud_create(model, route)
variable = model.new(params) # change is here
if variable.save
flash[:notice] = "Saved!"
redirect_to route
else
flash[:error] = "Try again"
render :new
end
end
UPDATE
It may be also a redirect loop. flags_path may be hitting the same create method (as opposed to the index), which will continually hit the crud_create before_action, causing the stack level too deep error. Try changing the redirect to test :)
To avoid hitting create again, you may have to set the status to 303. From the APIDock entry for redirect_to:
If you are using XHR requests other than GET or POST and redirecting
after the request then some browsers will follow the redirect using
the original request method. This may lead to undesirable behavior
such as a double DELETE. To work around this you can return a 303 See
Other status code which will be followed using a GET request.
example: redirect_to route, status: 303
I have a model called Project, which is a collection of information stored by a Company. This company can create projects two ways.
The first is the standard RESTful way - the company goes to the Project index, then clicks 'New Project', and upon creation is returned to the index with a flash message.
The second is a 'quick create' that can be accessed when a company is looking at a client's profile. From here, the company can enter some basic information and send this off to create a project with that client (the client is specified automatically here).
The second of these two scenarios has a project being accessed from clients/show. Sending this data to projects/create would ordinarily route the company to projects/index, but I don't want that. In this case, the create action is meaningfully different in that certain fields are treated differently, and the redirect is also different. What would you suggest I do?
Build an alternative 'create_from_client' action in projects.
Build a 'create_project' action in clients.
Send a parameter to projects/create and set client_id and redirect to client/show if that parameter exists.
Something else I'm not aware of.
Thanks!
You can leverage the referrer directly from the Request object and fork based on that, similar to how redirect_to :back works.
From the Rails API docs for the redirect_to options hash:
:back - Back to the page that issued the request.
Useful for forms that are triggered from multiple places.
Short-hand for redirect_to(request.env["HTTP_REFERER"])
So you can simply do something like this:
def create
#project = Project.new( params[:project] )
#project.save
respond_with #project, location: get_location!
end
private
def get_location!
case request.env["HTTP_REFERER"]
# Your routing logic here.
end
This is nice and easy to unit test, too, if you're into that. :)
context "if the user came from the regular form" do
before { controller.request.env["HTTP_REFERER"] = "regular_form_url" }
it "redirects to the index path" do
post :create
response.should redirect_to :index
end
end
context "if the user came from the quick-create form" do
before { controller.request.env["HTTP_REFERER"] = "quick_create_url" }
it "redirects to some other path" do
post :create
response.should redirect_to some_other_path
end
end
I would just add another action to the controller, 'quick_create' or whatever. You can dry out the form with partials and parameters to the partial to tell how to render things...This just seems like the easiest way.
I've got this semi-rational (or is that semi-irrational) hang up against leveraging the referrer...
I ussualy add hidden referer field with current URL then redirect to it
For example
def create
#project = Project.new params[:project]
#project.save
respond_with #project, :location => params[:referer] || [:index, :projects]
end
I am using Authlogic-connect to connect various service providers. There is a method in user.rb
def complete_oauth_transaction
token = token_class.new(oauth_token_and_secret)
old_token = token_class.find_by_key_or_token(token.key, token.token)
token = old_token if old_token
if has_token?(oauth_provider)
self.errors.add(:tokens, "you have already created an account using your #{token_class.service_name} account, so it")
else
self.access_tokens << token
end
end
When a service provider is already added it gives the error as stated in the has_token? method and the page breaks. I need to redirect the app to the same page and flash the error. How do i do this? I have overridden the method in my own user.rb so that I can change the code.
Hmm, well you could put a method that handles the error that has_token? throws, and tell your controller to redirect that exact error. something like this in your controller:
rescue_from OauthError::RecordNotFound, :with => :deny_access
then you can put
def deny_access
redirect_to your_view_path, :alert => "Too bad sucker" #some flash message
end
Or you could do something like this in the controller:
if complete_oauth_transaction.errors.present?
redirect_to your_view_path
else
# continue on with the normal code here
end
This is how you could generically handle errors. Your exact code will vary, as this is all we have to go off of.