views:

284

answers:

3

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
+1  A: 

The simple answer is to change the message to something that fits both: "You cannot mess with another author's posts."

kajaco
Yeah, but I don't really want to do that.
John Topley
+2  A: 

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"
Ian Terrell
Don't make a query before the auth check!
Pedro Daniel
@Pedro You're going to have to explain to me how you can check authorization (not authentication) based on a specific model before you have a copy of that model. :)
Ian Terrell
Your authorization method makes 0 queries.You make a find by before checking if the user is authorized or not.
Pedro Daniel
Btw I'm talking about the second example.
Pedro Daniel
You have to pull the model out of the database (in one method or another) to see if the owner owns it. That's the unavoidable reality of model-dependent authorizations.
Ian Terrell
Excellent answer - thanks. I went with your second example in the end (authorized_to). I tried the final example but it actually still raises the double render exception.The Authorization plugin feels a little heavyweight for this, but I'll certainly investigate it for future apps.
John Topley
The other example should still work -- just be sure to have "and return" after punt. My personal opinion on leveraging third party plugins/libraries is that while it may feel heavyweight for little projects, you probably end up spending as much time reinventing a wheel that does less as learning and leveraging another library that does more than you need. You never have to use everything, after all.
Ian Terrell
I did have "and return" after punt. I copied your example exactly and it didn't work.
John Topley
I suspect you're missing something somewhere. And cargo culting will only get you so far; you should work to understand the what and the why of every line of code you write. Then you'll be able to debug simple problems yourself -- or more likely, avoid them. :)
Ian Terrell
With respect Ian, I do.
John Topley
+2  A: 

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.

Mike