I have my rails app and trying to add authority by adding a method made with gem called cancancan. so there are two methods below.
def find_review
#review = Review.find(params[:id])
end
def authorize_user!
unless can? :manage, #review
redirect_to product_path(#review.product)
end
end
and there are two cases. case 1: calling the methods inside destroy action
def destroy
find_review
authorize!
#product = #review.product
#reviews = #product.reviews
#review.destroy
end
and case 2: calling the methods using before_action method
before_action :find_review, :authorize!, only: [:destroy]
def destroy
#product = #review.product
#reviews = #product.reviews
#review.destroy
redirect_to product_path(#product), notice: 'Review is deleted'
end
I get that using before_action(case 2) redirects unauthorized user even before calling the action so it makes sense. What I am wondering is in case 1, why authorize method doesn't interrupt and redirect the user before destroying the review ? It actually redirects, but after deleting the review. but I thought ruby is synchronous..
As others have noted, your authorize! does not interrupt the request, only adds a header and then carries on with the destruction and whatnot. A good reliable way to interrupt the flow from within a nested method call is to raise an exception. Something like this:
class ApplicationController
NotAuthorizedError = Class.new(StandardError)
rescue_from NotAuthorizedError do
redirect_to root_path
end
end
class ReviewsController < ApplicationController
def authorize!
unless can?(:manage, #review)
fail NotAuthorizedError
end
end
def destroy
find_review
authorize! # if user is not authorized, the code below won't run
# because of the exception
#product = #review.product
#reviews = #product.reviews
#review.destroy
end
end
Update
There must be something off with your integration of cancan, because it does include an exception-raising authorize!. You should be able to do this:
def destroy
find_review
authorize! :manage, #review # can raise CanCan::AccessDenied
#product = #review.product
#reviews = #product.reviews
#review.destroy
end
Or, better
load_and_authorize_resource
def destroy
#product = #review.product
#reviews = #product.reviews
#review.destroy
end
In case 1 you need to return from method when doing redirect to halt execution, and use ActionController::Metal#performed? to test whether redirect already happended:
def authorize_user!
unless can? :manage, #review
redirect_to product_path(#review.product) and return
end
end
def destroy
find_review
authorize!; return if performed?
#product = #review.product
#reviews = #product.reviews
#review.destroy
end
There's no "asynchronous behaviour". redirect_to actually only adds appropriate headers for the response object, it's not halt execution of your delete request.
In case 1, you need to return from method when doing redirect to halt execution
return redirect_to product_path(#review.product)
In case 2, you have to make some change in you authorize method.
scenario 1. For a specific user role can't manage Review model at all
unless can?(:manage, Review)
redirect_to product_path(#review.product)
end
replace the instance variable with model name
scenario 2, some users in the specific role can manage the review, but others in some role can't mange review.
In this case, you need to load the review object before calling can function
def authorize_user!
find_review
unless can?( :manage, #review)
redirect_to product_path(#review.product)
end
end
I think my answer will help you.
Related
I'm using Pundit with Rails, and I have a controller that I need to completely restrict from a specific user role. My roles are "Staff" and "Consumer." The staff should have full access to the controller, but the consumers should have no access.
Is there a way to do this that is more DRY than restricting each action one-by-one?
For instance, here is my policy:
class MaterialPolicy < ApplicationPolicy
attr_reader :user, :material
def initialize(user, material)
#user = user
#material = material
end
def index?
user.staff?
end
def show?
index?
end
def new?
index?
end
def edit?
index?
end
def create?
index?
end
def update?
create?
end
def destroy?
update?
end
end
And my controller:
class MaterialsController < ApplicationController
before_action :set_material, only: [:show, :edit, :update, :destroy]
# GET /materials
def index
#materials = Material.all
authorize #materials
end
# GET /materials/1
def show
authorize #material
end
# GET /materials/new
def new
#material = Material.new
authorize #material
end
# GET /materials/1/edit
def edit
authorize #material
end
# POST /materials
def create
#material = Material.new(material_params)
authorize #material
respond_to do |format|
if #material.save
format.html { redirect_to #material, notice: 'Material was successfully created.' }
else
format.html { render :new }
end
end
end
# PATCH/PUT /materials/1
def update
authorize #material
respond_to do |format|
if #material.update(material_params)
format.html { redirect_to #material, notice: 'Material was successfully updated.' }
else
format.html { render :edit }
end
end
end
# DELETE /materials/1
def destroy
authorize #material
#material.destroy
respond_to do |format|
format.html { redirect_to materials_url, notice: 'Material was successfully destroyed.' }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_material
#material = Material.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def material_params
params.require(:material).permit(:name)
end
end
Is there a way to do this that I'm not understanding, or is that how Pundit is designed, to require you to be explicit?
The first step is just to move the call to authorize to your callback:
def set_material
#material = Material.find(params[:id])
authorize #material
end
You can also write #material = authorize Material.find(params[:id]) if your Pundit version is up to date (previous versions returned true/false instead of the record).
Pundit has a huge amount of flexibility in how you choose to use it. You could for example create a separate headless policy:
class StaffPolicy < ApplicationPolicy
# the second argument is just a symbol (:staff) and is not actually used
def initialize(user, symbol)
#user = user
end
def access?
user.staff?
end
end
And then use this in a callback to authorize the entire controller:
class MaterialsController < ApplicationController
before_action :authorize_staff
# ...
def authorize_staff
authorize :staff, :access?
end
end
Or you can just use inheritance or mixins to dry your policy class:
class StaffPolicy < ApplicationPolicy
%i[ show? index? new? create? edit? update? delete? ].each do |name|
define_method name do
user.staff?
end
end
end
class MaterialPolicy < StaffPolicy
# this is how you would add additional restraints in a subclass
def show?
super && some_other_condition
end
end
Pundit is after all just plain old Ruby OOP.
Pundit doesn't require you to be explicit, but it allows it. If the index? method in your policy wasn't duplicated, you'd want the ability to be explicit.
You can start by looking at moving some of the authorization checks into the set_material method, that cuts down over half of the checks.
The other half could be abstracted out into other private methods if you wanted, but I think they're fine as-is.
You could also look at adding a before_action callback to call the authorizer based on the action name, after you've memoized #material via your other callback, but readability is likely to suffer.
Use the second argument for the authorize method. Eg:
authorize #material, :index?
You can now remove all the other methods that just calls index?
I don't know if I'm doing something wrong here but it seems like.
I use Pundit for authorization and I have set up a few models with it now.
Ive got a Category model which can only be created by admins. Also I don't want users to see the show/edit/destroy views either. I just want it to be accessed by admins. So far so good.
Will add some code below:
category_policy.rb
class CategoryPolicy < ApplicationPolicy
def index?
user.admin?
end
def create?
user.admin?
end
def show?
user.admin?
end
def new?
user.admin?
end
def update?
return true if user.admin?
end
def destroy?
return true if user.admin?
end
end
categories_controller.rb
class CategoriesController < ApplicationController
layout 'scaffold'
before_action :set_category, only: %i[show edit update destroy]
# GET /categories
def index
#category = Category.all
authorize #category
end
# GET /categories/1
def show
#category = Category.find(params[:id])
authorize #category
end
# GET /categories/new
def new
#category = Category.new
authorize #category
end
# GET /categories/1/edit
def edit
authorize #category
end
# POST /categories
def create
#category = Category.new(category_params)
authorize #category
if #category.save
redirect_to #category, notice: 'Category was successfully created.'
else
render :new
end
end
# PATCH/PUT /categories/1
def update
authorize #category
if #category.update(category_params)
redirect_to #category, notice: 'Category was successfully updated.'
else
render :edit
end
end
# DELETE /categories/1
def destroy
authorize #category
#category.destroy
redirect_to categories_url, notice: 'Category was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_category
#category = Category.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def category_params
params.require(:category).permit(:name)
end
end
application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
false
end
def create?
create?
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.all
end
end
end
Ive got Pundit included in my ApplicationController and rescue_from Pundit::NotAuthorizedError, with: :forbidden this set up there as well.
The authorization itself works, if I'm logged in with an admin account I have access to /categories/*. If I'm logged out I get the following: NoMethodError at /categories
undefined methodadmin?' for nil:NilClass`
While writing the question I think I found the problem- I guess Pundit looks for a User that is nil because I'm not logged in. What would the correct approach of solving this issue look like?
Best regards
The most common approach is to redirect users from pages that are not accessible by not logged in users. Just add a before action in your controller:
class CategoriesController < ApplicationController
before_action :redirect_if_not_logged_in
<...>
private
def redirect_if_not_logged_in
redirect_to :home unless current_user
end
end
(I assume here that you have current_user method which returns user instance or nil. Please change :home to wherever you want to redirect users)
There are multiple ways of achieving what you want.
The most obvious (but kind of dirty) and straightforward-looking way of doing that would be to add a check for user presence in every condition:
user && user.admin?
It won't fail with nil error as the second part of the condition won't get executed. But it doesn't look very nice, right? Especially if you have to copy this over to all methods you have in CategoryPolicy.
What you can do instead, is to make Pundit "think" that you passed a User, by creating a GuestUser class which responds with false to admin? method (https://en.wikipedia.org/wiki/Null_object_pattern):
In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof)
And use it when user is nil. In practice, it will look like this:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user || GuestUser.new
#record = record
end
# ...
end
class GuestUser
def admin?
false
end
end
This way you won't have to alter any of your other code, as the model you passed responds to the method which is expected by policy (admin?). You may want to define this GuestUser somewhere else (not in the policy file), depending if you want other parts of the app to reuse that behavior.
You can also proceed with the redirect approach from P. Boro answer. It's less flexible in some sense but can totally work fine if you don't need anything besides redirecting all non-logged in users.
What's the best way to check if the current object belongs to the current user?
I want to only allow the power of deletion to the owner, but I'm struggling to build a controller function to accomplish this.
before_filter :signed_in_user, only: [:create]
before_filter :correct_user, only: [:edit, :update, :destroy]
...
def destroy
#event = Event.find(params[:id])
if #event.present?
#event.destroy
end
redirect_to root_path
end
private
def signed_in_user
unless signed_in?
store_location
redirect_to signin_path, notice: "Please sign in."
end
end
def correct_user
#event = current_user.events.find_by_id(params[:id])
rescue
redirect_to root_path
end
My current def correct_user allows any logged in user to make deletions.
Your correct_user method is already doing what you need to do: fetching the event with current_user.events.find. You can just delete the event-finding code from your destroy method and it should work correctly.
When I wrote my comment you had yet to added the destroy action code. Look carefully at the correct_user method:
def correct_user
#event = current_user.events.find_by_id(params[:id])
rescue
redirect_to root_path
end
Right there you are retrieving the event and storing it in an instance variable. More so, you are retrieving the event through the current_user. So it's scoped to the current_user, and only events that are owned by the current_user are exposed.
#event = current_user.events.find_by_id(params[:id])
Remember, correct_user is set in a before_filter, so it runs before the destroy action.
So, by the time the request gets to the destroy action, the event is already stored in an instance variable (#event). So there is already an event object for you to work with. You can reference that. There is no need to retrieve it again.
def destroy
#event = Event.find(params[:id]) #<- This line is redundant (and dangerous in this case)
... # snip
end
Change the destroy method to this.
def destroy
redirect_to root_path if #event.destroy
end
I was just about to add similar functionality! You have to check if the current user is signed in, and if their ID = the user_id of the object.
Something like
event = Event.find(params[:id])
if event.user_id == current_user.id
event.destroy
else
redirect_to :root, :notice => "You do not have permissions."
end
Preface: I'm using devise for authentication.
I'm trying to catch unauthorized users from being able to see, edit, or update another user's information. My biggest concern is a user modifying the form in the DOM to another user's ID, filling out the form, and clicking update. I've read specifically on SO that something like below should work, but it doesn't. A post on SO recommended moving the validate_current_user method into the public realm, but that didn't work either.
Is there something obvious I'm doing wrong? Or is there a better approach to what I'm trying to do, either using devise or something else?
My UsersController looks like this:
class UsersController < ApplicationController
before_filter :authenticate_admin!, :only => [:new, :create, :destroy]
before_filter :redirect_guests
def index
redirect_to current_user unless current_user.try(:admin?)
if params[:approved] == "false"
#users = User.find_all_by_approved(false)
else
#users = User.all
end
end
def show
#user = User.find(params[:id])
validate_current_user
#user
end
def new
#user = User.new
end
def edit
#user = User.find(params[:id])
validate_current_user
#user
end
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
format.html { redirect_to #user, :notice => 'User was successfully created.' }
else
format.html { render :action => "new" }
end
end
end
def update
#user = User.find(params[:id])
validate_current_user
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to #user, :notice => 'User was successfully updated.' }
else
format.html { render :action => "edit" }
end
end
end
private
def redirect_guests
redirect_to new_user_session_path if current_user.nil?
end
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
return redirect_to(current_user)
end
end
end
The authenticate_admin! method looks like this:
def authenticate_admin!
return redirect_to new_user_session_path if current_user.nil?
unless current_user.try(:admin?)
flash[:error] = "Unauthorized access!"
redirect_to root_path
end
end
EDIT -- What do you mean "it doesn't work?"
To help clarify, I get this error when I try to "hack" another user's account:
Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most
once per action. Also note that neither redirect nor render terminate
execution of the action, so if you want to exit an action after
redirecting, you need to do something like "redirect_to(...) and
return".
If I put the method code inline in the individual controller actions, they do work. But, I don't want to do that because it isn't DRY.
I should also specify I've tried:
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
redirect_to(current_user) and return
end
end
If you think about it, return in the private method just exits the method and passes control back to the controller - it doesn't quit the action. If you want to quit the action you have to return again
For example, you could have something like this:
class PostsController < ApplicationController
def show
return if redirect_guest_posts(params[:guest], params[:id])
...
end
private
def redirect_guest_post(author_is_guest, post_id)
redirect_to special_guest_post_path(post_id) if author_is_guest
end
end
If params[:guest] is present and not false, the private method returns something truthy and the #show action quits. If the condition fails then it returns nil, and the action continues.
You are trying and you want to authorize users before every action. I would suggest you to use standard gems like CanCan or declarative_authorization.
Going ahead with this approach you might end up reinventing the wheel.
In case you decide on using cancan, all you have to do is add permissions in the ability.rb file(generated by rails cancan:install)
can [:read,:write,:destroy], :role => "admin"
And in the controller just add load_and_authorize_resource (cancan filter). It will check if the user has permissions for the current action. If the user doesnt have persmissions, then it will throw a 403 forbidden expection, which can be caught in the ApplicationController and handled appropriately.
Try,
before_filter :redirect_guests, :except => [:new, :create, :destroy]
should work.
This is because you are using redirect twice, in authenticate_admin! and redirect_guests for new, create and destroy actions.
"Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action."
That's the reason of the error. In show method, if you are neither the owner of this account nor the admin, you are facing two actions: redirect_to and render
My suggestion is to put all of the redirect logic into before_filter
My code for post controller is as follows. What I'm trying to do is user should be able to delete his own posts, and admin_user can delete any posts. The following code make admin_user can delete posts, but for normal user, when trying to delete his own post, it redirect to root_path
It seems do_authentication doesn't work properly, a normal user is trying to be authenticated as an admin instead of "correct_user"
What could be wrong?
Thanks!
class PostsController < ApplicationController
before_filter :signed_in_user, only: [:index, :create, :destroy]
before_filter :do_authentication, only: :destroy
.
.
.
def destroy
#post.destroy
flash[:success] = "Post deleted!"
redirect_back_or user_path(current_user)
end
private
def do_authentication
correct_user || admin_user
end
def correct_user
#post = current_user.posts.find_by_id(params[:id])
redirect_to user_path(current_user) if #post.nil?
end
def admin_user
#post = Post.find_by_id(params[:id])
redirect_to(root_path) unless current_user.admin?
end
First of all I would suggest using cancan for authorization.
I think your problem is the return value of correct_user. You don't control that. If that method returns something that evaluates to false the do_authentication method will also call admin_user. Also looking at your code it seems that the admin authorization won't work also ...
try this:
def do_authentication
#post = Post.find_by_id(params[:id])
redirect_to redirect_location unless current_user.admin? or #post.user == current_user
end
def redirect_location
return "redirect_location_for_admin" if current_user.admin?
return "redirect_location_for_non_admins"
end
The methods correct_user,admin_user will be executed for all the users regardless of their role because you have not checked any condition while calling the methods.The code need to be improved to solve your problem.
def do_authentication
if current_user.admin?
#post = Post.find_by_id(params[:id])
else
#post = current_user.posts.find_by_id(params[:id])
redirect_to user_path(current_user), :notice => "Access denied" unless #post
end
end