I have defined my own method authorize_user in one of my controllers, as:
def authorize_user
if !((current_user.has_role? :admin, #operator) || (current_user.has_role? :super_admin))
raise CanCan::AccessDenied
end
end
I want to rescue from the CanCan exception (or any other exception for that matter). I have used Rolify in my app. How do I rescue and redirect to the root_url of my app with a custom message?
I have tried the following options, but none of them worked:
Try 1:
rescue CanCan::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
end
Error in this case: syntax error, unexpected keyword_do, expecting '('
Try 2:
rescue CanCan::AccessDenied
redirect_to root_url, :alert => "Unauthorized Access"
Error in this case: Render and/or redirect were called multiple times in this action
How do I solve this issue?
This is my controller code:
class CabsController < ApplicationController
before_action :set_cab, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!
after_action :authorize_user
# Some basic CRUD actions
private
def set_cab
#cab = Cab.find(params[:id])
#operator = Operator.find(params[:operator_id])
end
def cab_params
params.require(:cab).permit(:category, :number)
end
def authorize_user
if !((current_user.has_role? :admin, #operator) || (current_user.has_role? :super_admin))
raise CanCan::AccessDenied
end
end
end
I think you could try the rescue_from method.
For example, your ApplicationController, would look like this:
class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied, with: :not_authorized
#other stuff
private
def not_authorized
redirect_to root_url, alert: "Unauthorized Access"
end
end
Since the question was updated with more code, here is additional information:
Some suggestions:
Make the :authorize_user a before_action as well. That way you don't need to worry about code running in the action even when the user was not allowed to do stuff.
You might also need to add the same :only option as for the :set_cab since you use the #operator instance variable.
Last, a personal code style preference is that I would have changed the if ! to unless to increase reading flow.
Try redirect_to(...) and return.
Agreeing with Jakob W I would like to point, that authorization (and authentication) MUST be performed only before action. What is the purpose of any authorization and exception raising when DB transaction, reading/writing to filesystem etc have been already done?
And using before_action has no problem with Render and/or redirect were called multiple times in this action - there will be only one redirect - in exception handling before controller method call.
So, I recommend next code (updated Jakob W's sample):
class CabsController < ApplicationController
#...
before_action :authorize_user
private
#...
def authorize_user
if !((current_user.has_role? :admin, #operator) || (current_user.has_role? :super_admin))
raise CanCan::AccessDenied
end
end
end
class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied, with: :not_authorized
#other stuff
private
def not_authorized
redirect_to(request.referrer || root_path), alert: "Unauthorized Access"
end
end
Could I recommend another authorization gem? I think this one is flexible and easy to use - pundit (https://github.com/elabs/pundit). Page on github has some useful tips on authorization.
Related
I've got a situation where User after successfully Sign up process (I'm using Devise gem) will have access only for two controllers until he passes all validation process. To check if validation process is in progress I'm using status column. If status == '' means validation is not finished.
I'm wondering how to implement such an access to only two pages? my first thought was Pundit gem but if I have 50 controllers I would need to implement 50 policies, inside of which I'll have numerous methods correspondent to controller actions. Is there any better way to do so?
[EDIT]
This is what I have so far:
class BaseController < ApplicationController
before_action :authorized_user
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def authorized_user
policy_scope(current_user)
end
private
def user_not_authorized
flash[:alert] = 'You are not authorized to perform this action.'
redirect_to(request.referrer || root_path)
end
end
User policy:
class UserPolicy < ApplicationPolicy
class Scope < Scope
def resolve
return raise(Pundit::NotAuthorizedError) unless user.status == 'active'
scope.all
end
end
end
This Answer only works if your controllers inherit from ApplicationController.
Add a before_action to your ApplicationController which does something when the user is not validated. This triggers now on all controller actions.
class ApplicationController < ActionController::Base
before_action :require_validated
private
def require_validated
redirect_to somewhere_url unless current_user.status == 'validated'
end
end
To except specific controller you can skip_before_action
class SomeController < ApplicationController
skip_before_action :require_validated
# skip only for specific actions
skip_before_action :require_validated, only: [:new, :create]
end
Thanks to the suggestions in the comments from #tadam I ended up with below code:
#intermediate controller class
class BaseController < ApplicationController
before_action :authorized_user
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def authorized_user
authorize :global, :only_active?
end
private
def user_not_authorized
flash[:alert] = 'You are not authorized to perform this action.'
redirect_to root_path
end
end
Global policy used in BaseController
class GlobalPolicy < ApplicationPolicy
def only_active?
active?
end
end
I have an app using devise, cancan and rolify that redirects on login to a page specific to their roles. What I am trying to figure out is how I can link to that same path elsewhere in the app, not just as a redirect?
class ApplicationController < ActionController::Base
protect_from_forgery
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_path, :alert => exception.message
end
def after_sign_in_path_for(resource)
case current_user.roles.first.name
when 'admin'
users_path
when 'yin'
content_yin_path
when 'yang'
content_yang_path
when 'yin_yang_lite'
content_yin_yang_lite_path
when 'yin_yang_pro'
content_yin_yang_pro_path
else
root_path
end
end
end
Basically, I am looking for how to get to the current_user => role => path, or current_user_role_path
You can define any controller method to also act as a helper method using the helper_method macro in the controller. For your case:
ApplicationController
helper_method :after_sign_in_path_for
I'm using devise and trying the next following:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :is_worker
def is_worker
if user_signed_in?
#email = current_user.email
if #email && Worker.find_by_email(#email).nil?
redirect_to '/tasksadmins'
else
redirect_to '/workers'
end
else
redirect_to '/users/sign_in'
end
end
end
when I try to enter the site: localhost:3000/tasksadmins, I got:
Oops! It was not possible to show this website
The website at http://localhost:3000/tasksadmins seems to be unavailable. The precise error was:
Too many redirects
It could be temporarily switched off or moved to a new address. Don't forget to check that your internet connection is working correctly.
How can I fix it please?
before_filter is applied to every single request. That's why it's redirecting again and again.
You might want to only filter specific actions:
before_filter :is_worker, only: :index
Another solution would be to check wether a redirect is necessary in #is_worker:
redirect_to '/workers' unless request.fullpath == '/workers'
EDIT:
Another way would be to skip the before filter for the target actions of your redirects. Example:
class WorkersController < ApplicationController
skip_before_filter :is_worker, only: :index
# …
end
In my case:
users_controller.rb
before_action :logged_in?, only: :new
def new
#user = User.new
render layout: "session"
end
and
application_controller.rb
def logged_in?
redirect_to users_new_url unless current_user.present?
end
When I was trying to redirect to the 'users/new' page,same error occurred.
This is just because I'm trying to redirect to the 'users/new' page and "def logged_in?" is also redirecting to the same page.
Then I changed the application_controller.rb code like this:
def logged_in?
redirect_to root_url unless current_user.blank?
end
Error_Resolved.
Having problems figuring this out.
trying to do a
rescue_from NoMethodError, :with => :try_some_options
But its not working.
EDITED:
For testing I'm doing a simple redirect
def try_some_options
redirect_to root_url
end
EDITED 2:
Sample of my controller. Added (exception) as recommended below.
I know the reason I'm getting the error. Using Authlogic and authlogic_facebook_connect plugin. When user is created from the facebook plugin the "MyCar" model, which is associated with a user is not created like it normally is created if a user registers locally. Since I do call on the user model and reference the users car throughout different parts of the site, I would like to do something like what you see below and eventually put it in my application_controller.
class UsersController < ApplicationController
before_filter :login_required, :except => [:new, :create]
rescue_from NoMethodError, :with => :try_some_options
...
def show
store_target_location
#user = current_user
end
def create
#user = User.new(params[:user])
if #user.save
MyCar.create!(:user => #user)
flash[:notice] = "Successfully created profile."
redirect_to profile_path
else
render :action => 'new'
end
end
...
protected
def try_some_options(exception)
if logged_in? && current_user.my_car.blank?
MyCar.create!(:user => current_user)
redirect_to_target_or_default profile_path
end
end
...
end
EDITED 3: Hacked it for now since I know why the error is showing up, but would like to figure out how to rescue_from NoMethodError
class UsersController < ApplicationController
before_filter :login_required, :except => [:new, :create]
before_filter :add_car_if_missing
def add_car_if_missing
if logged_in? && current_user.my_car.blank?
MyCar.create!(:user => current_user)
end
end
end
I just read your post when trying to come up with a solution to the same problem. In the end I did the following:
class ExampleController < ApplicationController
rescue_from Exception, :with => :render_404
...
private
def render_404(exception = nil)
logger.info "Exception, redirecting: #{exception.message}" if exception
render(:action => :index)
end
end
This worked well for me. It is a catch all situation yet it just may help you. All the best.
Given that it is well-documented how to use before_filter for a single user classification, I'm having trouble getting action-level protection for multiple user types. Let me explain:
I've got something like this...
class ApplicationController < ActionController::Base
class << self
attr_accessor :standard_actions
end
#standard_actions = [:index, :show, :new, :edit, :create, :update, :destroy]
def require_guardian
unless current_user and current_user.is_a?(Guardian)
store_location
redirect_to home_url
return false
end
end
def require_admin
unless current_user and current_user.is_a?(Administrator)
store_location
redirect_to register_url
return false
end
end
end
And in the GuardiansController I want to only allow the standard actions for Administrator but all other actions should require Guardian. So I tried this...
class GuardiansController < ApplicationController
before_filter :require_admin, :only => ApplicationController::standard_actions
before_filter :require_guardian, :except => ApplicationController::standard_actions
...
end
Which ends up doing a recursive redirection. There must be a better way?
OK, this is another case of not looking carefully and missing something. I inadvertently had setup the route to redirect the user in a recursive way. The above solution works just fine when you set the routes properly:
def require_guardian
unless current_user and current_user.is_a?(Guardian)
store_location
# this route (home_url) sent the user to another controller which ran a before_filter sending them back here again.
# redirect_to home_url
# So I changed it to a neutral route and it works great!
redirect_to register_url
return false
end
end