How to avoid UniqueViolation errors - ruby-on-rails

My facebook-like application gives users the opportunity to delete unwanted messages from following users. For this purpose I created a MicropostQuarantine association between users and microposts: if such an association between a user and a micropost exists, then that user will not see that micropost in their feed. However, microposts will be visible also in a user's profile page, included those already in quarantine. So, if I removed a micropost from my feed and visited the profile page of that micropost's user, I would still see that micropost and have access to the remove button that would raise the following error:
ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_micropost_quarantines_on_user_id_and_micropost_id"
DETAIL: Key (user_id, micropost_id)=(1, 300) already exists.
: INSERT INTO "micropost_quarantines" ("user_id", "micropost_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"):
The quarantine method in the microposts controller is simple:
def quarantine
current_user.micropost_quarantines.create!(micropost_id: #micropost.id)
end
There is a before_action :entitled_user, only: :quarantine defined as follows:
def entitled_user
#micropost = Micropost.find(params[:id])
redirect_to root_url unless (current_user == #micropost.user || current_user.following.include?(#micropost.user))
end
As you can see a user can only quarantine his own microposts and microposts from following users. In order to avoid UniqueViolation errors, I thought of adding some code to the entitled_user method, to check if the association already exists:
def entitled_user
#micropost = Micropost.find(params[:id])
#quarantine = MicropostQuarantine.find_by(micropost_id: #micropost.id, user_id: current_user.id)
redirect_to root_url unless (current_user == #micropost.user || current_user.following.include?(#micropost.user) || #quarantine.nil?)
end
However this does not work: the entitled_user method is ignored/bypassed by rails for some unknown reasons, I keep receiving the UniqueViolation: ERROR from ActiveRecord and the whole unless conditional is ignored, so that I would be able to quarantine microposts from non following users.

I think we shoudn't use unless in complicated condition, try to the following:
redirect_to root_url if (current_user != #micropost.user && current_user.following.exclude?(#micropost.user)) || #quarantine.present?
Tips:
def entitled_user
#micropost = Micropost.find(params[:id])
if (current_user.id != #micropost.user_id && !current_user.following.exists?(#micropost.user_id)) ||
current_user.micropost_quarantines.exists?(micropost_id: #micropost.id)
redirect_to root_path and return
end
end
Using :exists? (SQL side) is more efficient than :include? (Ruby side)
Using #micropost.user_id instead of #micropost.user because we don't need instance #user, so we don't need to do like this:
SELECT (*) FROM users WHERE id = #{#micropost.user_id}
Hope this helps!

Related

Does the Rails Console Bypass Mass-Assignment Protection?

This might be a stupid question, but please bear with me.
I've been playing around with a rails app that I am working on, and I was in the console (ie. rails c), and I decided to try to add a user to the database via the console.
My User model has a role attribute, which is excluded from the list of strong params in the UsersController. However, when I was using the console, I was able to edit the value of the new user's role, by doing update_attribute. This concerns me. Does this mean that I am not doing strong params correctly and have somehow not protected my User model from mass-assignment? Or does the rails console bypass mass assignment intentionally? Is there any security vulnerability here?
Here is the console input/output:
2.3.1 :004 > user.update_attribute("role", "admin")
(0.1ms) begin transaction
SQL (0.7ms) UPDATE "users" SET "updated_at" = ?, "role" = ? WHERE "users"."id" = ? [["updated_at", "2017-06-21 10:25:34.134203"], ["role", "admin"], ["id", 4]]
(92.1ms) commit transaction
=> true
and here is the relevant part of UsersController:
def create
sleep(rand(5)) # random delay; mitigates Brute-Force attacks
#user = User.new(user_params)
if #user.save #&& verify_recaptcha(model: #user)
if #user.update_attribute('role', 'user')
#user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
else
render 'new' #Reset the signup page
end
end
#...
#Defines which fields are permitted/required when making a new user.
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
thank you in advance.
user.update_attribute("role", "admin")
it has got nothing to do with strong parameters..
That just generates an sql query as you see in the console which updates the record.
strong parameters are used to restrict unpermitted params coming from the view/client and modify your record.
As in your case,
your user_params does not include role because you are assigning it yourself. in case you had not done that and in the request body I had sent role: 'admin',
User.new(params)
would make the user admin, if verify_recaptcha(model: #user) condition fails..

Get any id to any user exists in the database

I am new to rails and have a task that asks me to send an invitation for any user to be admin in my magazine here is my piece of code
def invite
inviteUser = { 'user_id' => current_user.id, 'Magazine_id' => params[:id] }
CollaborationInvitation.create(inviteUser)
#magazine = Magazine.find(params[:id])
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
I need to replace current_user.id with something that refers to any user's id which exists in my database to send him an invitation to be admin with me I tried to add #User=Users.All and then pass it as a variable but it got me an error I tried a lot of things but every time I get an error except for adding current_user.id
ps: I am using devise for authentication
You asked a couple things, and it is kind of confusing what you want to do.
Here is how you get all ids of records in a model.
Rails4: User.ids
Rails3: User.all.map(&:id)
Or (not sure if #pluck is in Rails 3 or not)
User.pluck(:id)
If you want to get a random user (you mentioned "any user") you could do.
User.find(User.pluck(:id).sample)
Though I think what you really want to do is to pass the id or some other attribute of a user as a param to the action and send that user an invitation.
Presumably you either have a post or get route for "users#invite" (the action you wrote in your question). You can add a named parameter there or you can pass a url param or if you are using a post route, you could add the param to the post body.
Then in your contoller you can do something like this (I'll use email as an attribute):
def invite
#user = User.find_by(email: params[:user_email])
#Rails 3 like this
# #user = User.find_by_email(params[:user_email])
# now do stuff with user
end
User.all will return you the collection of users. So,
Find the user object to get an id...
Try this code....
def invite
inviteUser = { 'user_id' => User.find_by_email('user#example.com').id, 'Magazine_id' => params[:id] }
CollaborationInvitation.create(inviteUser)
#magazine = Magazine.find(params[:id])
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
You can try
User.last.id
or
User.find_by_email("xyz#test.com").id
or
User.where(email: "xyz#test.com").first.id
Replace xyz#test.com with desired user email address. To get more details on rails active record query interface, please read rails guides http://guides.rubyonrails.org/active_record_querying.html

Rails assign session variable

Hi I'm hacking around Rails the last 8 months so don't truly understand a number of things. I've seen and used helper methods that set the current_user etc. and am now trying to replicate something similar.
I'm trying to set a global value in the application controller for the current company the user is in. I know I'm doing it wrong but I can't quite figure out how to solve it or even if I'm approaching it correctly. I want it so that when the user clicks on a company, the global variable is set to the id of the company they are in. Then in any other sub-model, if I want info on the company, I use the global variable to retrieve the company object with that id.
The code that's causing the problem is in my navbar in application.html.erb
<li><%= link_to "Company", company_path, :method => :get %></li>
This works when I'm using the companies controller etc. But when I try use any other controller I'm getting the error
ActionController::UrlGenerationError in Employees#index
No route matches {:action=>"show", :controller=>"companies"} missing required keys: [:id]
which as I understand it means it can't render the url because no company id parameter is being passed?
I was trying to hack a helper method I used in another project (for getting the current_user) but have realised that it uses the session to extract the user.id to return a user object. Here's my attempt at the helper method in application_controller.rb
def current_company
#company = Company.find_by_id(params[:id])
end
I'm not able to get the current company from the session so my method above is useless unless the company id is being passed as a parameter.
I've thought about passing the company id on every sub-model method but
I'm not 100% sure how to do this
It doesn't sound very efficient
So my question is am I approaching this correctly? What's the optimal way to do it? Can I create a helper method that stores a global company id variable that gets set once a user accesses a company and can then be retrieved by other models?
I probably haven't explained it too well so let me know if you need clarification or more info. Thanks for looking.
Edit 1
Made the changes suggested by Ruby Racer and now I have:
application.html.erb
<%unless current_page?(root_path)||current_page?(companies_path)||current_company.nil? %>
<li><%= link_to "Company", company_path, :method => :get %></li>
This is not displaying the link in the navbar, I presume because current_company is nil (the other two unless statements were fine before I added current_company.nil?
I'm setting the current_company in
companies_controller.rb
before_action :set_company, only: [:show, :edit, :update, :destroy, :company_home]
def company_home
current_company = #company
respond_with(#company)
end
application_controller.rb
def current_company=(company)
session[:current_company] = company.id
puts "The current_company has been assigned"
puts params.inspect
end
def current_company
#company = Company.find_by_id(session[:current_company])
puts "The current_company helper has been called"
puts #company
puts params.inspect
end
Am I doing something wrong?
Edit 2
I have no idea why this isn't working. After the above edits, it appears as though the session[:company_id] is not being assigned so the current_company helper method is returning nil. I've tried printing the session paramaters puts session.inspect and can't find any company_id information. Anyone any idea why it isn't assigning the value?
Edit 3
Can't for the life of me figure out what's going wrong. I've tried multiple things including moving the current_company = #company into the set_company method in companies_controller.rb which now looks like this:
def company_home
puts "Test the current company"
puts "#{#company.id} #{#company.name}"
puts params.inspect
end
private
def set_company
#company = Company.find_by_id(params[:id])
if #company.nil?||current_user.organisation_id != #company.organisation.id
flash[:alert] = "Stop poking around you nosey parker"
redirect_to root_path
else
current_company = #company
end
end
The company_home method is being given a company object (I can see this in the console output below) but the current_company assignment is just not happening. Here's the console output for reference
Started GET "/company_home/1" for 80.55.210.105 at 2014-12-19 10:26:49 +0000
Processing by CompaniesController#company_home as HTML
Parameters: {"authenticity_token"=>"gfdhjfgjhoFFHGHGFHJGhjkdgkhjgdjhHGLKJGJHpDQs6yNjONwSyTrdgjhgdjgjf=", "id"=>"1"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = 6 ORDER BY "users"."id" ASC LIMIT 1
Company Load (0.3ms) SELECT "companies".* FROM "companies" WHERE "companies"."id" = 1 LIMIT 1
Organisation Load (0.3ms) SELECT "organisations".* FROM "organisations" WHERE "organisations"."id" = $1 LIMIT 1 [["id", 6]]
Test the current company
1 Cine
{"_method"=>"get", "authenticity_token"=>"gfdhjfgjhoFFHGHGFHJGhjkdgkhjgdjhHGLKJGJHpDQs6yNjONwSyTrdgjhgdjgjf=", "controller"=>"companies", "action"=>"company_home", "id"=>"1"}
Rendered companies/company_home.html.erb within layouts/application (0.1ms)
Company Load (0.6ms) SELECT "companies".* FROM "companies" WHERE "companies"."id" IS NULL LIMIT 1
The current_company helper has been called
{"_method"=>"get", "authenticity_token"=>"gfdhjfgjhoFFHGHGFHJGhjkdgkhjgdjhHGLKJGJHpDQs6yNjONwSyTrdgjhgdjgjf=", "controller"=>"companies", "action"=>"company_home", "id"=>"1"}
CACHE (0.0ms) SELECT "organisations".* FROM "organisations" WHERE "organisations"."id" = $1 LIMIT 1 [["id", 6]]
Completed 200 OK in 280ms (Views: 274.0ms | ActiveRecord: 1.7ms)
As per above, under the line The current_company helper has been called, there's a blank line where puts #company should be outputting something. This means the current_company method is returning nothing.
Also, in the company_home method in the companies_controller, if I change puts "#{#company.id} #{#company.name}" to puts "#{current_company.id} #{current_company.name}" an error gets thrown.
Has anyone any idea why the def current_company=(company) isn't assigning a session parameter? Thanks
Final Edit
I've no idea why, but it appears the problem related to this:
def current_company=(company)
session[:current_company] = company.id
puts "The current_company has been assigned"
puts params.inspect
end
It looks as though this never gets called. I don't understand why as I've never used something like this before.
I'll put my fix in an answer.
Ok, you need to do two things.
First thing, you need to assign your company.id to a session variable
def current_company=(company)
session[:company_id]=company.id
end
Second, your helper method for current_company will be as follows:
def current_company
Company.find_by_id(session[:company_id])
end
This can be nil if there is no session[:company_id] or if it corresponds to no company. That's ok...
Next, it is quite unlikely to get it working without an id, if you use /companies both for your index and your show actions.
Now, for your first task. Setting the variable:
controller:companies_controller.rb
def show
# assuming you have a before_action, something like set_company, no need to redo it
current_company=#company # this will set the session variable
end
If you want your navbar to lead to your current_company, you will need to write:
<% unless current_company.nil? %>
<li><%= link_to "Company", current_company, :method => :get %></li>
<% end %>
I don't know what you want to do if there is no current company, so I just leave it out.
Instead of using a helper method to assign a value or search for a company, I've assigned a session variable in the set_company method in companies_controller.rb. This can then be accessed around the application
companies_controller.rb
private
def set_company
#company = Company.find_by_id(params[:id])
if #company.nil?||current_user.organisation_id != #company.organisation.id
redirect_to root_path
else
session[:current_company] = #company
current_company = #company
end
end

how to prevent a DELETE HTTP request from succeeding in this situation?

Rails beginner here..
I have a users resource where I implemented a callback that's supposed to prevent an admin user from deleting herself.
before_filter :admin_no_delete, only: :destroy
def admin_no_delete
admin_id = current_user.id if current_user.admin?
redirect_to root_path if params[:id] == admin_id
end
If this looks familiar to some, it's from Michael Hartl's rails tutorial, exercise #10 here but I tried to do it differently, not as he suggested.
My (lame) test for this fails
describe "deleting herself should not be permitted" do
before do
delete user_path(admin)
end
it { should_not redirect_to(users_path) }
end
But exposing a delete link for the admin user just to test and clicking on that link, it seems like the callback actually succeeds in executing (redirecting to root_path).
I was able to invoke the destroy action using jQuery to delete the record being protected by the callback (using Web Inspector's javascript console):
$.ajax({url: 'http://localhost:3000/users/104', type: 'DELETE', success: function(result){alert(result)} })
Looking for ideas on how to prevent a DELETE HTTP request from succeeding in this situation.. also any ideas on how to properly test for this kind of situation?
Thanks.
Simple: params[:id] is a string, while admin_id is a Fixnum. You can just change it as follows and it should work:
redirect_to root_path if params[:id].to_i == admin_id
The logic you're using seems a little odd to me, though. Why use a before filter if it's just for one action, and why change the redirect? I think the logic should be directly in the destroy action and look something like this:
def destroy
unless current_user.admin? && current_user.id == params[:id].to_i
User.find(params[:id]).destroy
flash[:success] = "User destroyed."
end
redirect_to users_path
end
You're comparing admin_id, an integer with params[:id]. Values in params are always strings (or arrays/hashes containing more strings) so the comparison will always fail.

How do I prevent access to records that belong to a different user

How do I prevent accessing a specific set of records based on a session variable?
i.e. I have a table of items with a user_id key, how do I filter access to the items based on user_id. I don't want someone to be able to access /items/3/edit unless that item has their user id against it (based on session var)
update:
I am using the answer suggested by #fl00r with one change, using find_by_id() rather than find() as it returns a nil and can be handled quite nice:
#item = current_user.items.find_by_id([params[:id]]) || item_not_found
where item_not_found is handled in the application controller and just raises a routing error.
Restrict access by fetching items through your current_user object (User should :has_many => :items)
# ItemsController
def edit
#item = current_user.items.find(params[:id])
...
end
where current_user is kind of User.find(session[:user_id])
UPD
Useful Railscast: http://railscasts.com/episodes/178-seven-security-tips, TIP #5
You can check access in show/edit/update method:
def edit
#item = Item.find(params[:id])
restrict_access if #item.user_id != current_user.id
....
end
and add restrict_access method, for example in application_controller
def restrict_access
redirect_to root_path, :alert => "Access denied"
end

Resources