Elegantly escaping errors - ruby-on-rails

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.

Related

Can I use CanCan with model(s) other than User?

I'm new to rails and building app from data on an api, I have two tables I want to use for users - students and educators.
I can authenticate in the controllers with
private
def fetch_user_data(username, password)
require 'URI'
uri = URI('the url for the api')
res = Net::HTTP.post_form(uri, 'username' => username, 'password' => password)
xml = res.body
doc = Nokogiri::Slop(xml)
#status = doc.auth.status.content.to_s
#username = doc.auth.username.content.to_s
#token = doc.auth.token.content.to_s
#person_id = doc.auth.person_pk.content.to_i
#security_roles = doc.auth.security_roles.content.to_s.downcase
end
def assign_user
if /faculty/ =~ #security_roles
#user = Educator.find_by(person_id: #person_id)
elsif /student/ =~ #security_roles
#user = Student.find_by(person_id: #person_id)
end
end
def authenticate_user(username, password)
fetch_user_data(username, password)
assign_user
session[:user_id] = #user.id
redirect_to #user
end
Now I know it's probably not pretty, but I'm learning as I go. I use the authenticate_user() in the sessions controller, and based on the redirect, the authentication seems fine. I do have one question about the :user_id key in the session -- is that a key only created for the session or is it trying to pull a value from a user table? Knowing that would help. My guess is it's just created for the session, but I have no idea.
Ok so now for my real problem. I'm trying to use cancan and I'm getting stuck at defining current user.
I figured I could have the #current_user instance point to the #user I assigned in assign_user. This doesn't seem to be working though. I tried a couple things, but I'm stuck. Maybe I can't even do that? Larger question? Do I have to have a user model to make cancan work? Can I use the two models Educators and Students and mask the user references in cancan on those?
I tried this, but it's not working -- any help?
def current_user
#current_user ||= #user
end
edit: Figured out a bit.
1. methods were in ApplicationHelper. Moved current_user() to ApplicationController . Changed syntax to conventional, but added conditionals.
def current_user
if Student.where(id:session[:user_id]).count == 0
#current_user ||= Educator.find(session[:user_id])
else
#current_user ||= Student.find(session[:user_id])
end
end
This seems to fix things, and allows me to use both tables as the user models.
There are several ways to define current_user (if you aren't using Devise), but this one here is pretty standard:
class ApplicationController < ActionController::Base
def current_user
#current_user ||= User.find(session[:user_id])
end
end
As for your question about the session, the way you have it set up, you're setting session[:user_id] equal to #user.id (which is always the same for each user).
minor aside consider using the gem CanCanCan which is as it sounds. CanCan not maintained and some one set up replacement CanCanCan. You won'thave to change any code.

Security - Checking if current user exists before executing code

In one of my controllers I have this method:
def method_name
if current_user
#model = Model.find(params[:id])
if #model.destroy
flash.alert = 'Model deleted successfully'
redirect_to models_path
end
end
end
I check if there is a current_user assigned by devise before giving the ability for the #model to be deleted. Is this safe and sufficient in terms of security?
What I really do is just checking if current_user exists. So is there a way that somebody can "trick" the system that current_user does exist and as a result be able to trigger the commands included in the method?
You will get a spectrum of answers in this. But if you want the user to be logged in then just do this at the top of your controller:
before_filter :authenticate_user!
That is provided by devise and ensures that there is a logged in user before allowing any controller actions.
If you have simple authorization then yes, most likely though you are going to want to make sure that the user has the authorization to delete the object. You can do that several ways. My favorite one right now is the Pundit gem.
You could also just check that the user owns the object in order to be able to delete it. That code would look something like this
#model = Model.find(params[:id)
if current_user.id == #model.user_id
# Rest of your destroy code
end

Signin Sessions Helpers in Rails

If you watch over any of Ryan Bates Authentication related Railscasts you'll see a recurring theme when creating sigin/signout functionality and I wanted to understand that a little bit more clearly.
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
For example usually in a session controller the create action will contain an assignment to the sessions hash such as session[:user_id] = user.id given that the variable user is set to an Active Record Object.
The above helper method is then used throughout the views to find the current signed in user.
However when signing out the destroy action contains only the line session[:user_id] = nil
My question is wouldn't #current_user also be needed to set to nil since it would be set to the previous User that was signed in?
Typically after setting session[:user_id] = nil your controller will return so #current_user still being active doesn't matter. You have to remember that #current_user only exists for that request, the next request that comes through is a new instance of that controller class.
You are right that if you did something like this:
def destroy
session[:user_id] = nil
logger.debug current_user.inspect # Current user is still set for this request
redirect_to admin_url, notice => "You've successfully logged out."
end
You would see the user information in the log file, but normally you are doing a redirect right after clearing the session[:user_id] so that controller instance is done.

Rails: where does the infamous "current_user" come from?

I've been looking around recently into Rails and notice that there are a lot of references to current_user. Does this only come from Devise? and do I have to manually define it myself even if I use Devise? Are there prerequisites to using current_user (like the existence of sessions, users, etc)?
It is defined by several gems, e.g. Devise
You'll need to store the user_id somewhere, usually in the session after logging in. It also assumes your app has and needs users, authentication, etc.
Typically, it's something like:
class ApplicationController < ActionController::Base
def current_user
return unless session[:user_id]
#current_user ||= User.find(session[:user_id])
end
end
This assumes that the User class exists, e.g. #{Rails.root}/app/models/user.rb.
Updated: avoid additional database queries when there is no current user.
Yes, current_user uses session. You can do something similar in your application controller if you want to roll your own authentication:
def current_user
return unless session[:user_id]
#current_user ||= User.find(session[:user_id])
end

Rails Security: redirect if not current_user

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...

Resources