Rails Security: redirect if not current_user - ruby-on-rails

I've been using authlogic and it works really well. One thing I've noticed is that as a hacker I could easily type in this address:
localhost::3000/users/a_user_that_is_not_me/edit
Since the form in the edit form is for #user which is set to current_user and requires an authenticity token, even if I tried to put in details for the other user I end up changing my own account instead of the other users.
That's nice and good, but I'd like it so that these hackers get redirected before they even see the form.
I tried this in the users_controller:
def edit
if admin?
#user = params[:user]
elsif User.find_by_username(params[:id]) != current_user
#user = current_user
#not_user = User.find_by_username(params[:id])
redirect_to user_path(#not_user)
else
#user = current_user
end
end
The redirect works if I type in an address with another user's name but I get a 404 error when trying to access the edit page for the current user.
Any ideas why this doesn't work?

If you're going to be doing this kind of thing a lot, check out an authorization plugin like authorization-san.
Authorization differs from authentication in that authentication is logging in, but authorization pertains to the authenticated (or un-authenticated) user's rights to perform actions.
With authentication-san, you could define this rule with this piece of code in your controller:
# this assumes you've got some way to set #user to the user you're looking up,
# e.g. in a before_filter
allow_access(:authenticated, :only => [:edit, :update]) { current_user == #user }

It looks like you are assigning #user to a string if the current user is an admin. This is simpler (less typo-prone):
def edit
u = User.find_by_username!(params[:id])
if admin? or current_user.username == params[:id]
#user = u
else
redirect_to user_path(u)
end
end
Also, don't you want to use find_by_username! (with bang on end) so that a 404 page is rendered when the user is not found? I'm not sure how you're getting the 404 page now...

Related

Session in Action Mailer - how to pass it?

Let's say I have a website where people can get a free ebook if they will sign up for a newsletter - after they've done it, I will create a User model and I will show them Edit Form to add some extra details about them.
I don't want to force them to add a password or any other details on the first page because it would decrease conversions and I don't require the additional information either. Also, I don't want them to have forever access to the Edit page so I solved it by assigned a session to them and recognize it through it on the Edit page. This is my controller:
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
UserWorker.perform_in(5.minutes, 'new_user', user.id)
redirect to edit form...
end
end
def edit
#user = User.find(session[:user_id])
end
def update
#user = User.find(session[:user_id])
#user.update!(user_edit_params)
redirect_to user_thank_you_path
end
end
But if they won't add extra information within 10 mins, I will send them an email via ActiveMailer with a link to the Edit form and ask them to do so.
Th question is how could I identify the user through the session and show them the form - how could I do User.find(session[:user_id] via ActionMailer)? Is it actually a correct way or would you recommend a different approach?
One way could be to set a background job to run in 10 minutes.
Inside that job, you would check if they're still "unregistered". You deliver the email if they've yet to complete the registration.
Something like this;
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
RegistrationCompletionReminderWorker.perform_in(10.minutes, user.id)
# redirect to edit form...
end
end
end
class RegistrationCompletionReminderWorker
def perform(user_id)
user = User.find(user_id)
if user.password.nil? # or whatever your logic for registration completion is
UserMailer.registration_reminder(user_id).deliver_now
end
end
end

Devise - Respond with resource and custom errors

I only want some users to be able to reset their passwords provided they have a flag set against their profile. I have the current code
class PasswordsController < Devise::PasswordsController
def create
self.resource = resource_class
u = User.find_or_initialize_with_errors([:email], resource_params, :not_found)
if u.try(:group).nil?
super
else
#respond_with(resource)
redirect_to :new_user_password, notice: "Your password is managed by your team leader, please contact them."
end
end
end
Although it's commented out above, I am trying to respond with the resource, just as devise does in it's code. If I respond with the current resource I get undefined methodusers_url'`, how can I solve this?
Also, what is the best way to add errors to this resource so that devise_error_messages! will display them correctly.

How can I make specific user(not current_user) sign out on rails-devise website?

I want to know how I can make specific user(not current_user) sign out.
I saw this http://www.rubydoc.info/github/plataformatec/devise/master/Devise/Controllers/SignInOut#sign_out-instance_method and maked this code.
def kick_admin
user = User.find params[:user_id]
user.admin = false
user.save
sign_out user #want to kick him.
end
But it does not make that user sign out but make me(current_user) signed out.
What is the right way to use the sign_out method?
I checked this answer(Sign out specific user with Devise in Rails) but it was not helpful.
One way you could do this is create a new attribute in the User table, call it force_sign_out.
def kick_admin
user = User.find params[:user_id]
user.update_attributes(admin: false, force_sign_out: true)
end
And have a before action in ApplicatonController so that if the user attempts any activity he's signed out
class ApplicationController < ActionController::Base
before_action :check_if_force_sign_out
def check_if_force_sign_out
return unless current_user.force_sign_out
current_user.update_attributes(force_sign_out: false) # reset for non-admin log in
sign_out
redirect_to root_path
end
end

Authorize related object when using pundit gem

If I have user object and user has one basicinfo. In user show action I have:
def show
#user = User.find params[:id]
authorize #user
end
And in show.html.erb I must show user's basicinfo, such as:
User name is: <%= #user.basicinfo.name %>
In this case should I also authorize basicinfo in user show action?
def show
#user = User.find params[:id]
authorize #user
authorize #user.basicinfo, :show?
end
The authorization applies to the entire action.
If you want to filter out some elements in the view you can do so on an ad hoc basis, basically applying whatever attribute you are using in the xxxPolicy class (which is not provided above)
Handling user authorization is possibly too complicated via Pundit
def initialize(user, user)
I definitely do filtering in the views when it comes to user actions

Elegantly escaping errors

I have statements such as #user = User.find(current_user.id) throughout my application.
Sometimes a user might enter with a nil variable (such as a new user for whom current_user is nil).
I'm sure the dumb way to do this would be to scatter if statements everywhere like...
if current_user.exists?
#user = User.find(current_user.id)
else
redirect_to root_url
---*or*---
#user = "new" # for use with if/case statements later on
end
What is the elegant way to deal with this confusion?
#user = User.find(current_user.id) is a little unnecessary. Mostly because current_user is a User object already, so at the very least you should do #user = current_user, but I would recommend that if it isn't already done by the authentication framework, I would add this to you application controller:
helper_method :current_user
That will make the current_user object available to your views and render the #user object unnecessary.
For handling redirects, I usually have this in my application controller:
before_filter :require_login
def require_login
current_user || redirect_to(root_url)
end
And then in my controllers that don't want to redirect:
skip_before_filter :require_login
Regarding setting the user to new, I wouldn't do it. I generally like my User objects to be user objects. I would just test for a new user by if current_user where a nil current_user is the same as setting it to 'new'.
I hope this helps
Assuming the language you're using is object oriented, I would create an object that holds the current user context. By default you could use a CurrentUserContext instance for uknown users that have very limited access.
When users login, you can load all user information and security information into a new instance of CurrentUserContext.
It's just a rough idea, but maybe it helps.
edit: This way you wouldn't need to create all kind of security exception rules... You just assume the security settings of the current context instance and ajust application behaviour according to that.
To get nils when there is no current user:
#user = current_user && User.find(current_user.id)
To get "new" when there is no current user:
#user = current_user ? User.find(current_user.id) : 'new'
Neither really solves the problem, but now at least it's on one line. For a more general, solution, perhaps you should scrap the current_user variable.

Resources