In my application, I store the user's ID in session[]. At the beginning of every controller action, I'm calling a method defined in the ApplicationController called current_user:
def current_user
#current_user ||= session[:current_user_id] &&
User.find_by_id(session[:current_user_id])
end
At the beginning of my controllers' methods, I have the following:
#current_user = current_user
if #current_user == nil
redirect_to :home
return
end
This is obviously repetitive code and should be a method somewhere. I read the answer for this question, and tried putting my method into a parent class that my controller classes now descend from, however it seems like I can't redirect from that method now.
In my parent class, I have:
def verify_user
user = current_user
if user == nil
redirect_to "/"
return
end
return user
end
And now I've changed my controller methods to this:
#current_user = verify_user
This doesn't work, and I think I know why. For one, I can't simply call return in my verify_user method, as that obviously will just return to the controller. The redirect doesn't seem to have any affect, probably because format.html is being called after the redirect call, which was the reason for the return in the original code.
So, what am I doing wrong here, and what suggestion do you have to solve it? Is this the wrong approach? My main goal is to keep the entire "check if user is logged in otherwise redirect" to one line of code per controller method.
Take a look at the devise gem https://github.com/plataformatec/devise. It handles a lot of this basic user authentication logic for you. This specific problem can we solved by adding before_filter :authenticate_user! to the controllers or actions that need to be guarded.
Add the following logic to the ApplicationController class:
class ApplicationController < ActionController::Base
def current_user
...
end
def logged_in?
current_user.present?
end
def require_user
return true if logged_in?
render_error_message("You must be logged in to access this page",
new_user_session_url)
return false
end
def render_message message
respond_to do |format|
format.html {
if request.xhr?
render(:text => message, :status => :unprocessable_entity)
else
redirect_to(root_url, :notice => message)
end
}
format.json { render :json => message, :status => :unprocessable_entity }
format.xml { render :xml => message, :status => :unprocessable_entity }
end
end
end
Now add a before_filter to your controller:
class OrdersController < ApplicationController
before_filter :require_user
end
Related
I'm working on my first rails api server.
I've got a controller for my User model that looks as such:
class UsersController < ApplicationController
def index
if current_user.admin?
#users = User.all
render json: #users
else
render json: { message: 'You do not have the appropriate permissions to access this resource' }, status: 401
end
end
def show
if User.exists?(#id)
#id = params[:id]
if current_user.id.to_s == #id || current_user.admin?
#user = User.find(#id)
render json: #user
else
render json: { message: 'You do not have the appropriate permissions to access this resource' }, status: 401
end
else
render json: { message: 'Requested resource not found' }, status: 404
end
end
end
What I want and currently have for these two controller methods is:
/users fetch all users only if the authenticated user making the request is of role admin
/users/:id fetch a user by id only if the authenticated user making the request has a matching id or is of role admin
The current implementation breaks the DRY philosophy. The reasoning is that the logic for handling whether or not the requesting user has the permissions to access the requested resource(s) is repeated across both controller methods. Furthermore, any model's controller method for show will repeat the logic for checking whether or not the requested resource exists. I also feel like this kind of implementation makes for fat controllers, where I'd rather them be skinny.
What I want to know from the community and from those that have solved this problem before; what is the best way to go about this in order to conform to the DRY philosophy and to keep controllers skinny.
Good to know: I'm using devise and devise-token-auth for authentication.
You need to use some kind of Authorization gem like cancancan. It is exactly what you need. Also it's else not elsif. elsif is followed by condition.
You can use github.com/varvet/pundit instead, for authorization.
It matches with the controller, instead of putting the authorization in the controller, you can use this to move out the authorization to another class.
I have used this across multiple Rails/Rails-API projects and didn't encounter a problem so far.
Instead of writing the code above. You can do this instead.
Also, prioritize early returns over nested ifs for readability.
In your controller.
class UsersController < ApplicationController
def index
authorize User # This will call the policy that matches this controller since this is UsersController it will call `UserPolicy`
#users = User.all
render :json => #users
end
def show
#user = User.find_by :id => params[:id] # Instead of using exists which query the data from db then finding it again, you can use find_by which will return nil if no records found.
if #user.blank?
return render :json => {:message => 'User not found.'}, :status => 404
end
authorize #user # This will call the policy that matches this controller since this is UsersController it will call `UserPolicy`
render :json => #user
end
end
In your Policy
class UserPolicy < ApplicationPolicy
def index?
#user.admin? # The policy is called in controller then this will check if the user is admin if not it will raise Pundit::NotAuthorizedError
end
def show?
#user.admin? || #record == #user # The policy is called in controller then this will check if the user is admin or the user is the same as the record he is accessing if not it will raise Pundit::NotAuthorizedError
end
end
In your ApplicationController
class ApplicationController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, :with => :show_forbidden
private
def show_forbidden exception
return render :json => {
:message => 'You are not authorized to perform this action.'
}, :status => 403
end
end
I have a link in my application.html.erb file
<%= link_to 'Sprzedaż', sell_path, remote: true %>
In the controller I authenticate user with before_action :authenticate_user!. Below is my authenticate_user! method.
protected
def authenticate_user!
if user_signed_in?
super
else
respond_to do |format|
format.js { render nothing: true, status: :unauthorized }
format.html { redirect_to root_path, :alert => 'Aby przejść do tej strony, musisz być zalogowany.' }
end
end
end
Basically it works correctly if the user isn't authorized. If the user has enabled Javascript it shows nice notification, and if the user hasn't enabled Javascript it shows alert and redirect to root_path which is good. The problem is that when the user is signed in and click the link nothing happens. It should redirect to the sell_path.
This is my ItemsController
class ItemsController < ApplicationController
before_action :authenticate_user!
def sell
#user = current_user
#items = JSON.parse(HTTParty.get("http://steamcommunity.com/profiles/#{#user.uid}/inventory/json/730/2?l=polish").body)
end
end
This is my ApplicationController
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
before_action :steam_informations
def steam_informations
#steam = session[:steam]
end
protected
def authenticate_user!
if user_signed_in?
super
else
respond_to do |format|
format.js { render nothing: true, status: :unauthorized }
format.html { redirect_to root_path, :alert => 'Aby przejść do tej strony, musisz być zalogowany.' }
end
end
end
end
You are trying to override helpers which defined in runtime. This is not how you must to do it.
In your case I recommend you to define for example authenticate! method like this:
def authenticate!
return true if user_signed_in?
respond_to do |format|
format.js { render nothing: true, status: :unauthorized }
format.html { redirect_to root_path, :alert => 'Aby przejść do tej strony, musisz być zalogowany.' }
end
end
This method will do nothing if user signed in and redirect to root page if user not signed in. Just define this method in ApplicationController and then use before_filter :authenticate! hook to execute it.
When before_filter \ before_action method returns false - rails won't execute your action. And if return true or just return - rails will execute action after hooks like authenticate! and render your views. To make it clear I'll show you some examples.
class FooController << ApplicationController
before_filter :dead_hook, only: :index
before_filter :nice_hook, only: :show
def index
# this action will be never executed because dead_hook method returns false.
end
def show
# this action will be executed right after nice_hook method because of 'return true' command in nice_hook method
end
def dead_hook
return false
end
def nice_hook
return true
end
end
Another way to do just like you trying to do - monkey-patch devise helper. You can do it like this:
module Devise
module Controllers
module Helpers
def authenticate_user!
# implement your logic here
end
end
end
end
Here you can check out whats going on in devise helpers:
Github Devise Helpers source code
Just for clarification: there is no difference between before_filter and before_action. Feel free to use any of them. before_action newer but before_filter not deprecated.
Preface: I'm using devise for authentication.
I'm trying to catch unauthorized users from being able to see, edit, or update another user's information. My biggest concern is a user modifying the form in the DOM to another user's ID, filling out the form, and clicking update. I've read specifically on SO that something like below should work, but it doesn't. A post on SO recommended moving the validate_current_user method into the public realm, but that didn't work either.
Is there something obvious I'm doing wrong? Or is there a better approach to what I'm trying to do, either using devise or something else?
My UsersController looks like this:
class UsersController < ApplicationController
before_filter :authenticate_admin!, :only => [:new, :create, :destroy]
before_filter :redirect_guests
def index
redirect_to current_user unless current_user.try(:admin?)
if params[:approved] == "false"
#users = User.find_all_by_approved(false)
else
#users = User.all
end
end
def show
#user = User.find(params[:id])
validate_current_user
#user
end
def new
#user = User.new
end
def edit
#user = User.find(params[:id])
validate_current_user
#user
end
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
format.html { redirect_to #user, :notice => 'User was successfully created.' }
else
format.html { render :action => "new" }
end
end
end
def update
#user = User.find(params[:id])
validate_current_user
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to #user, :notice => 'User was successfully updated.' }
else
format.html { render :action => "edit" }
end
end
end
private
def redirect_guests
redirect_to new_user_session_path if current_user.nil?
end
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
return redirect_to(current_user)
end
end
end
The authenticate_admin! method looks like this:
def authenticate_admin!
return redirect_to new_user_session_path if current_user.nil?
unless current_user.try(:admin?)
flash[:error] = "Unauthorized access!"
redirect_to root_path
end
end
EDIT -- What do you mean "it doesn't work?"
To help clarify, I get this error when I try to "hack" another user's account:
Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most
once per action. Also note that neither redirect nor render terminate
execution of the action, so if you want to exit an action after
redirecting, you need to do something like "redirect_to(...) and
return".
If I put the method code inline in the individual controller actions, they do work. But, I don't want to do that because it isn't DRY.
I should also specify I've tried:
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
redirect_to(current_user) and return
end
end
If you think about it, return in the private method just exits the method and passes control back to the controller - it doesn't quit the action. If you want to quit the action you have to return again
For example, you could have something like this:
class PostsController < ApplicationController
def show
return if redirect_guest_posts(params[:guest], params[:id])
...
end
private
def redirect_guest_post(author_is_guest, post_id)
redirect_to special_guest_post_path(post_id) if author_is_guest
end
end
If params[:guest] is present and not false, the private method returns something truthy and the #show action quits. If the condition fails then it returns nil, and the action continues.
You are trying and you want to authorize users before every action. I would suggest you to use standard gems like CanCan or declarative_authorization.
Going ahead with this approach you might end up reinventing the wheel.
In case you decide on using cancan, all you have to do is add permissions in the ability.rb file(generated by rails cancan:install)
can [:read,:write,:destroy], :role => "admin"
And in the controller just add load_and_authorize_resource (cancan filter). It will check if the user has permissions for the current action. If the user doesnt have persmissions, then it will throw a 403 forbidden expection, which can be caught in the ApplicationController and handled appropriately.
Try,
before_filter :redirect_guests, :except => [:new, :create, :destroy]
should work.
This is because you are using redirect twice, in authenticate_admin! and redirect_guests for new, create and destroy actions.
"Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action."
That's the reason of the error. In show method, if you are neither the owner of this account nor the admin, you are facing two actions: redirect_to and render
My suggestion is to put all of the redirect logic into before_filter
I have a rails app . I have created a sessionscontroller and want to redirect to users page '/users' once the user signs in. But the redirect doesnt seem to be happening.
class SessionsController < ApplicationController
def create
user = User.find_or_create_by_fbid(params[:user][:fbid]) #...Success
user.update_attributes(params[:user]) #....Sucess
sign_in(user) # ....This occurs successfully
redirect_to users_path # .... Redirect doesnt occur on the browser side
end
end
The sign_in method is defined inside the Application Controller
class ApplicationController < ActionController::Base
def sign_in(user)
session[:fbid] = user.fbid
#current_user = user
end
end
Server Logs below . The redirect actually seems to be happening on the server side . But I do not see any change on the client side. The browser doesnt change page.
The UsersController
class UsersController < ApplicationController
def index
#users = User.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #users }
end
end
end
Original Ajax Post -
$.post("/sessions",{user:{name:profile.name, email:profile.email,fbid:profile.id}});
The redirect occurs successfully if I use a javascript redirect statement inside the $post() as a callback function .
$.post("/sessions",{user:{name:profile.name, email:profile.email,fbid:profile.id}},function( data ) {
window.location="/users";
}
);
You need to handle the response's redirection in an ajax query. Normally the browser handles, but it won't with ajax.
From this SO question (edited slightly for your case)
var params = {user:{name:profile.name, email:profile.email,fbid:profile.id}};
$.ajax({
type: "POST",
url: "/sessions",
data: params,
dataType: "json",
success: function(data, textStatus) {
if (data.redirect) {
// data.redirect contains the string URL to redirect to
window.location.href = data.redirect;
}
else {
// data.form contains the HTML for the replacement form
$("#login_form").replaceWith(data.form);
}
}
});
I'm using a real basic authentication based of Ryan Bates' screencast. If you can't figure it out from my code, I recommend watching Authentication from Scratch
applications_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
helper_method :current_user
private
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
sessions_controller.eb
class SessionsController < ApplicationController
def new
end
def create
user = User.authenticate(params[:email], params[:password])
if user
session[:user_id] = user.id
redirect_to root_url, :notice => "Logged in!"
else
flash.now.alert = "Invalid email or password"
render "new"
end
end
def destroy
session[:user_id] = nil
redirect_to root_url, :notice => "Logged out!"
end
end
Ajax Post -
$.post("/sessions",{user:{name:profile.name, email:profile.email,fbid:profile.id}});
I seem to have an authorization hiccup in my Ruby on Rails app. I have been using the following method in my application controller and it has been working beautifully.
def require_owner
obj = instance_variable_get("##{controller_name.singularize.camelize.underscore}") # LineItem becomes #line_item
return true if current_user_is_owner?(obj)
render_error_message("You must be the #{controller_name.singularize.camelize} owner to access this page", root_url)
return false
end
I then filter in the specific controllers by:
before_filter :require_owner, :only => [:destroy, :update, :edit]
I recently created a new controller which has a bit of a different naming convention that seems to be causing a problem. Normally my controllers read messages_controller or posts_controller. In this specific case I named the resource box_wod which generated box_wods_controller.
This is the only controller that seems to be having a problem with this filter so I bet I can tell it is in the naming of it and therefore the application_controller method is not recognizing the owner of the record.
I am not getting an error message but the application is not letting me edit, update or destroy a record because I am not the BoxWod owner. My routes are correct as are my associations and the correct information is getting passed to the box_wod table.
Is there a way to rewrite the application_controller method to recognize the additional underscore in the box_wod resource? Or is this even my problem?
UPDATE:
Here are the three methods in the BoxWodsController:
def edit
#workout_count = Workout.count
#box_wod = BoxWod.find(params[:id])
end
def update
#box_wod = BoxWod.find(params[:id])
respond_to do |format|
if #box_wod.update_attributes(params[:box_wod])
flash[:notice] = 'BoxWod was successfully updated.'
format.html { redirect_to(#box_wod) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #box_wod.errors, :status => :unprocessable_entity }
end
end
end
def destroy
#box_wod = BoxWod.find(params[:id])
#box_wod.destroy
respond_to do |format|
format.html { redirect_to(box_wods_url) }
format.js
end
end
In situations like this, I like to create a controller method that I can override when necessary. For example:
# application_controller.rb
class ApplicationController
def require_owner
obj = instance_variable_get("##{resource_instance_variable_name}")
# Do your authorization stuff
end
private
def resource_instance_variable_name
controller_name.singularize.camelize.underscore
end
end
# box_wods_controller.rb
class BoxWodsController
private
def resource_instance_variable_name
'box_wod' # Or whatever your instance variable is called
end
end
Lastly, please post your BoxWodsController code so we can better diagnose the problem.
It would seem that the #box_wod instance variable is not created until the require_owner method is invoked so current_user_is_owner? is checking a nil value, resulting in it always returning false. Perhaps you need another before_filter to populate the instance variable before require_owner is invoked.