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
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.
I'm trying to limit editing capability to an article's author in my rails app.
In my controller I'm adding the user id as the author which works fine:
def create
#article = Article.new(article_params)
#article.author = current_user.id
...
end
then checking it on the edit action:
def edit
#article = Article.find(params[:id])
redirect_to root_path, notice: 'Whoa there amigo!' unless current_user.id == #article.author
end
Editing results in redirect every time.
(I'm not using Devise, CanCan or anything else at this point as this is the only functionality I need right now.)
Update
Based on comments, I updated my create method to:
def create
#article = current_user.articles.build(article_params)
and my edit method to:
def edit
#article = current_user.articles.find(params[:id])
redirect_to root_path, notice: 'Whoa..' unless current_user.id.to_s == #article.user_id
This works but "current_user.id.to_s" seems a bit hacky for something that must be incredibly common.
In create action:
def create
#article = Article.new(article_params)
#article.author = current_user
...
end
And in edit change the condition to:
unless current_user == #article.author
This way you compare user objects and not user objects with ids.
Hi I am new to rails and I am trying to figure out how to prevent unauthorized access to the update action of a controller.
I know I could have a before_filer that kicks out people that arent logged in and I have redirect_to in the edit action, but I want a way to stop a user from editing an object that does not belong to them.
Ex: A authorized user can simply change a job object in my app, by directly sending a PUT request with any job.id as a parameter and change any field they want.
Here is my controller:
def update
#job = Job.find(params[:id])
#job.update_attributes(params[:job])
redirect_to jobs_path
end
To try and fix this problem I tried to check in the update action if it the user was authorized and if they werent, i would redirect them to the index page.
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path
end
#job.update_attributes(params[:job])
redirect_to jobs_path
end
But when I try to do this, rails gives me an error saying I can only have one redirect in an action.
Well, the straightforward fix to your immediate problem is to use flow control to make sure that only one redirect_to is ever reached on a single request, as many others have suggested.
However, that's not really how I'd solve your larger problem.
First, there are a lot of existing solutions for managing authorization, such as cancan or rolify. I'd look into those.
Second, I'd use a before_filter to block access, as you suggest. Something like:
before_filter :load_job, :only => [:show, :edit, :update, :delete]
before_filter :require_authorization, :only => [:edit, :update, :delete]
def load_job
#job = Job.find(params[:id])
end
def require_authorization
redirect_to jobs_path unless current_user.can_edit?(#job) # or whatever you want to check
end
The before filters will execute in order, so you'll already have the user & the job available when you check permissions, and can check permissions for that specific job.
def update
#job = Job.find(params[:id])
#job.update_attributes(params[:job]) unless #job.user.id != current_login
redirect_to jobs_path
end
:)
This is probably because after the first redirect the second one could still be executed.
Thus putting the update_attributes and the second redirect into the else path like this should solve the problem:
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path
else
#job.update_attributes(params[:job])
redirect_to jobs_path
end
end
You can either do redirect_to jobs_path and return or return redirect_to jobs_path.
Try the following:
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path and return
end
#job.update_attributes(params[:job])
redirect_to jobs_path and return
end
Use an Else Clause
The problem is that the redirect_to method doesn't end the current method; it just tells the controller to set some headers. In order to prevent this problem, you need to make sure that control doesn't "fall through" to the second redirect. One way to do this would be to put your alternative path into an else clause. For example:
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path
else
#job.update_attributes(params[:job])
redirect_to jobs_path
end
end
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.