In a fit of unoriginality, I'm writing a blog application using Ruby on Rails. My PostsController contains some code that ensures that the logged in user can only edit or delete their own posts.
I tried factoring this code out into a private method with a single argument for the flash message to display, but when I did this and tested it by editing another author's post, I got an ActionController::DoubleRenderError - "Can only render or redirect once per action".
How can I keep these checks DRY? The obvious approach is to use a before filter but the destroy method needs to display a different flash.
Here's the relevant controller code:
before_filter :find_post_by_slug!, :only => [:edit, :show]
def edit
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def update
#post = Post.find(params[:id])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def destroy
#post = Post.find_by_slug(params[:slug])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot delete another author’s posts."
redirect_to root_path and return
end
...
end
private
def find_post_by_slug!
slug = params[:slug]
#post = Post.find_by_slug(slug) if slug
raise ActiveRecord::RecordNotFound if #post.nil?
end
The before filter approach is still an ok option. You can gain access to which action was requested using the controller's action_name method.
before_filter :check_authorization
...
protected
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user != current_user
flash[:notice] = (action_name == "destroy") ?
"You cannot delete another author’s posts." :
"You cannot edit another author’s posts."
redirect_to root_path and return false
end
end
Sorry for that ternary operator in the middle there. :) Naturally you can do whatever logic you like.
You can also use a method if you like, and avoid the double render by explicitly returning if it fails. The key here is to return so that you don't double render.
def destroy
#post = Post.find_by_slug(params[:slug])
return unless authorized_to('delete')
...
end
protected
def authorized_to(mess_with)
if #post.user != current_user
flash[:notice] = "You cannot #{mess_with} another author’s posts."
redirect_to root_path and return false
end
return true
end
You could simplify it more (in my opinion) by splitting out the different parts of behavior (authorization, handling bad authorization) like this:
def destroy
#post = Post.find_by_slug(params[:slug])
punt("You cannot mess with another author's post") and return unless author_of(#post)
...
end
protected
def author_of(post)
post.user == current_user
end
def punt(message)
flash[:notice] = message
redirect_to root_path
end
Personally, I prefer to offload all of this routine work to a plugin. My personal favorite authorization plugin is Authorization. I've used it with great success for the last several years.
That would refactor your controller to use variations on:
permit "author of :post"
The simple answer is to change the message to something that fits both: "You cannot mess with another author's posts."
If you don't like the ugly* return in that last solution, you can use an around filter and conditionally yield only if the user is authorized.
around_filter :check_authorization, :only => [:destroy, :update]
private
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user == current_user
yield
else
flash[:notice] = case action_name
when "destroy"
"You cannot delete another author's posts."
when "update"
"You cannot edit another author's posts."
end
redirect_to root_path
end
end
*-- that's my preference, though code-wise it's perfectly valid. I just find that style-wise, it tends to not fit.
I also should add I haven't tested this and am not 100% certain it would work, though it should be easy enough to try.
Related
I'm trying to make sure that people can't submit a create action if they submit an entry with an ID other than their own. For this, I have set up the test as following:
entries_controller_test.rb
def setup
#user = users(:thierry)
#other_user = users(:steve)
end
...
test "should redirect create action on entry with id that doesn't belong to you" do
log_in_as(#user)
assert_no_difference 'Entry.count' do
post :create, entry: { content: "Lorem Ipsum"*10, id: #other_user }
end
end
The outcome of the test is that Entry.count increases by one, therefore #user can create a post with ID #other_user (is the code correct to create an entry with ID of the other user?)
entries_controller.rb: My create action currently looks like this.
def create
#entry = #entries.build(entry_params)
if #entry.save
flash[:success] = "Your entry has been saved."
redirect_to root_path
else
flash.now[:danger] = "Your entry has not been saved."
render 'index'
end
end
The instance variable is being passed in to the action by calling before_action :correct_user on the action. Here's the correct_user method.
def correct_user
#entries = current_user.entries
redirect_to root_url if #entries.nil?
end
By the way, the create action is being called from the index page. I suspect the problem is indeed with authorization since my test can log in the user and create an actual entry.
Can anyone spot an issue?
Your code is only checking whether the current_user has some entries, but there is no validation on the user_id of the entry being submitted to the create action. Moreover, even if the user has no entries, the #entries variable will be [], which is not nil (so correct_user will never redirect to root). The correct check would have been #entries.empty?, but still the object would be created with an incorrect user, as long as the current_user already has some entries belonging to them.
The way I usually go about this is not to permit the user_id parameter (with strong_parameters), and by setting the ownership of new objects to the current_user. If you want to perform the check, your correct_user should look more like this:
def correct_user
unless current_user.id == params[:entry][:user_id]
flash[:alert] = "Some error message"
sign_out # This action looks like a hack attempt, thus it's better to destroy the session logging the user out
redirect_to root_url
end
end
I think this might work.
In your entries controller.
class EntriesController < ApplicationController
before_action :correct_user, only: [:edit, :update]
def correct_user
unless correct_user.id == params[:entry][:user_id]
else
redirect_to root_url
end
end
end
I'm in the process of creating a website similar to Reddit. I would like to allow a moderator to be able to update a topic, but not be able to create or delete topic. I'm aware that I need to update TopicsController but I'm not sure how. My main problem is that I'm not sure how to make the code specific enough to ensure that a moderator can only update; not delete or create a topic, as an admin can.
My current code looks like this:
class PostsController < ApplicationController
before_action :require_sign_in, except: :show
before_action :authorize_user, except: [:show, :new, :create]
def show
#post = Post.find(params[:id])
end
def new
#topic = Topic.find(params[:topic_id])
#post = Post.new
end
def create
#post.body = params[:post][:body]
#topic = Topic.find(params[:topic_id])
#post = #topic.posts.build(post_params)
#post.user= current_user
if #post.save
flash[:notice] = "Post was saved"
redirect_to [#topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :new
end
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
#post.assign_attributes(post_params)
if #post.save
flash[:notice] = "Post was updated."
redirect_to [#post.topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :edit
end
end
def destroy
#post = Post.find(params[:id])
if #post.destroy
flash[:notice] = "\"#{#post.title}\" was deleted successfully."
redirect_to #post.topic
else
flash[:error] = "There was an error deleting the post."
render :show
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
def authorize_user
post = Post.find(params[:id])
unless current_user == post.user || current_user.admin?
flash[:error] = "You must be an admin to do that."
redirect_to [post.topic, post]
end
end
end
I've already added a moderator role to the enum role.
I apologise if this seems really basic...but it has got me stumped!
Thanks in advance!
I could answer with some custom solution, but it's better to use a more structured and community-reviewed approach: authorization with cancan.
As tompave noticed you can use cancan gem for this.
Personally I prefer pundit.
In old days I used to define permissions directly in code everywhere: in controllers, in views and even models. But it's really bad practice. When your app grows, you are lost: you update a view, but you should make the same change in controller and sometimes in model too. It soon becomes absolutely unmanageable and you have no idea what your users can and cannot do.
Pundit, on the other hand, offers central place -- policy -- for defining what user can do. Views and controllers can then use those policies.
For example, if you need to define Post's policy you simply create app/policies/post_policy.rb file:
class PostPolicy
attr_reader :user
attr_reader :post
def initialize(user, post)
#user = user
#post = post
end
def author?
post.user == user
end
def update?
author? || user.admin? || user.moderator?
end
def create?
author? || user.admin?
end
def destroy?
author? || user.admin?
end
# etc.
end
Now whenever you need to check user's ability to perform action, you can simply invoke:
# in controller
def update
#post = Post.find(params[:id])
authorize #post
# do whatever required
end
# in view
<% if policy(post).update? %>
<%= link_to 'Edit Post', post_edit_path(post) %>
<% end %>
As you can see Pundit is very easy to comprehend and it uses the same "convention over configuration" approach as Rails. At the same time it's very flexible and allows you to test virtually anything.
You will definitely need Pundit or any similar gem to manage permission in your ambitious app.
Have a basic blog (it's actually edgeguide's blog: http://edgeguides.rubyonrails.org/getting_started.html)
Then I integrated Devise into it. So, user can only log in and see their own information.
Now trying to change it somewhat.
I'd like the users to see all content, but only edit and destroy their own only.
Trying to use before_action filter like this:
`before_action :authorize, :only => [:edit, :destroy]`
And this is the authorize method that I wrote:
def authorize
#article = Article.find(params[:id])
if !#article.user_id = current_user.id then
flash[:notice] = "You are not the creator of this article, therefore you're not permitted to edit or destroy this article"
end
end
But it doesn't work. Everything acts as normal, and I can delete mine and everyone's else content.
How do I get it that I can destroy ONLY my own content, and not everyone's else?
Not using CanCan, nor do I want to.
Not sure if this is worth including or not, but originally when I had everyone see their own content, that was via create action:
def create
#article = Article.new(article_params)
#article.user_id = current_user.id if current_user
if #article.save
redirect_to #article
else
render 'new'
end
end
You're having several problems
first, look at that :
if !#article.user_id = current_user.id then
You're only using one = instead of == so you are doing an assignation that will evaluate to current_user.id
Also, in your condition, you're only setting a flash message but not doing anything to really prevent the user.
Here's a corrected version :
def authorize
#article = Article.find(params[:id])
unless #article.user_id == current_user.id
flash[:notice] = "You are not the creator of this article, therefore you're not permitted to edit or destroy this article"
redirect_to root_path # or anything you prefer
return false # Important to let rails know that the controller should not be executed
end
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
I have a controller with a lot of code duplication such as:
class PostController < ApplicationController
def action1
end
...
def actionN
end
end
And basically each action do something like this:
def action
#post = Post.find(params[:id])
if #post.action(current_user)
flash[:notice] = "#{custom string for this action}"
else
flash[:notice] = "Problem with your request"
end
redirect_to root_url
end
I thought about a method in ApplicationController that takes an array of symbols and generate the other methods, such as:
def self.action_for(*args)
args.each do |method, string|
define_method method.to_sym do
#post = Post.find(params[:id])
if #post.send method.to_sym
flash[:notice] = string
else
flash[:notice] = "Problem with your request"
end
redirect_to root_url
end
end
end
And call in PostController:
action_for [:action1, "Congratulations!"], [:action2, "Cool action!"] ..
I think this solution is ugly, it makes the ApplicationController dirty and allow other controllers to call my actions.
Any idea to solve the code-duplication problem?
Why don't you make a single action which will receive some extra parameter, like msg? Then you can take advantage of built-in I18n support:
def some_action
#post = Post.find(params[:id])
if #post.action(current_user)
flash[:notice] = I18n.t("messages.#{params[:msg]}", default: "Wrong message type")
else
flash[:notice] = I18n.t("messages.problem")
end
redirect_to root_url
end
Or maybe that makes sense to allow your #post.action to return some message for your notice?
I don't think there's anything too ugly in this solution.
To limit the logic to one controller, you can define self.action_for in PostController, instead of ApplicationController, and call it below its definition.
Note that you're already passing in first elements in pairs as symbols, so to_sym calls in action_for are not necessary.