I'm using Active Admin's CanCan authorization adapter, along with Rolify, to manage authorization on an admin site. I have a model, company, that has_many :manuals, and another model, manuals, that has_many :parts.
If a user does not have access to read admin/manuals/1 and types it into the address bar, they are redirected properly and presented with the unauthorized message. However, if the user types in admin/manuals/1/parts they are not denied access. They are taken to that page, except all the parts are hidden from them. They should be getting redirected to the dashboard with an unauthorized message.
Here is my configuration. Thanks in advance for any advice you can offer.
config/routes.rb
ActiveAdmin.routes(self)
models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
can :read, ActiveAdmin::Page, :name => "Dashboard"
if user.has_role? :admin
can :manage, :all
elsif user.has_role? :moderator
can :manage, Part, :manual => { :company_id => user.company_id }
else
can :read, Part, :manual => { :company_id => user.company_id }
end
end
end
I've also overwritten the default authorization methods in controllers/application_controller.rb
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
end
def authenticate_admin_user!
authenticate_user!
unless user_signed_in?
flash[:alert] = "You are not authorized to view this page"
redirect_to root_path
end
end
def current_admin_user #use predefined method name
return nil unless user_signed_in?
current_user
end
def after_sign_in_path_for(user)
if current_user.has_role? :admin
admin_dashboard_path
elsif current_user.has_role? :moderator
admin_manuals_path
else
company_path(user.company)
end
end
Did you add the method load_and_authorize_resource to your controller?
Like this:
class SomeController < ApplicationController
load_and_authorize_resource
...
end
Check Abilities & Authorization
Related
I use Devise, CanCanCan and rails_admin. With accessibility everything is OK, but I fail to get a flash message and redirect to root_path and getting the screen below. Any ideas, hints on how I could get flash message instead of this? Other flash messages, both alerts and notifications display OK. Thanks a lot.
My ApplicationController is:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :authenticate_user!
def after_sign_in_path_for(resource)
stored_location_for(resource) || welcome_path_url
end
rescue_from CanCan::AccessDenied do |exception|
respond_to do |format|
format.json { head :forbidden, content_type: 'text/html' }
format.html { redirect_to main_app.root_url, notice: exception.message }
format.js { head :forbidden, content_type: 'text/html' }
end
end
end
rails_admin.rb
RailsAdmin.config do |config|
### Popular gems integration
## == Devise ==
config.authenticate_with do
warden.authenticate! scope: :user
end
config.current_user_method(&:current_user)
## == CancanCan ==
# config.authorize_with :cancancan
config.authorize_with do
redirect_to main_app.root_path unless current_user.superuser ==true
end
<....>
end
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
# Define abilities for the passed in user here. For example:
#
user ||= User.new # guest user (not logged in)
if user.superuser?
can :manage, :all
cannot :access, :rails_admin
cannot :manage, :dashboard
end
if user.office?
cannot :access, :rails_admin
cannot :manage, :dashboard
can :read, :all
end
end
end
EDIT:
The working answer is marked.
Also I should define status variable like this:
def status
#user_role ||= User.new(read_attribute(:user_role))
end
in my User model.
It seems RailsAdmin::MainController isn't derived from ApplicationController you defined on the app to catch the exceptions raised on its subclass.
You need to explicitly add the config to use another parent controller like below. Please find the related gem documentation here:
# in config/initializers/rails_admin.rb
config.parent_controller = 'ApplicationController'
This stack overflow answer as well might help you.
In my app I have a model called User that has_one Talent.
In CanCanCan I have this ability:
class Ability
include CanCan::Ability
def initialize(user)
if user.nil?
can :read, User
can :read, Talent, is_public?: true
else
can :read, Talent, is_public?: true
end
My page is being rendered by the ProfilesController#show. Like this:
class ProfilesController < ApplicationController
before_action :check_ability, except: [:show]
def show
#user = User.find(params[:id])
authorize! :read, #user
authorize! :read, #user.talent
if current_user
sent_connections = current_user.sent_connections
connections = sent_connections + current_user.all_connections
#is_connected = !(connections.select { |c| c.user.id == #user.id }.empty?)
end
#top_5_photos = #user.top_5_photos
end
Well. Im trying to render a profile that the method: is_public returns false. But the page is being rendered correctly, while I expected was that the user cant see the page because of the rule:
can :read, Talent, is_public?: true
What Im missing here?
If I remember it correctly,
can :read, Talent, is_public?: true
^ is_public? above is expected to be an attribute by Cancancan.
But because is_public? is a custom method, then can you try the following instead?
can :read, Talent do |talent|
talent.is_public?
end
In my ability.rb, I have the following rule:
elsif user.has_role? :demo
can :read, Profile, demo_featured: true, demo_linked: true, message: "To access this profile, please subscribe here."
But that doesn't produce the message I want.
How do I get this specific rule to produce the message I want?
Edit 1
Here is the full ability.rb if condition:
def initialize(user)
user ||= User.new # guest user (not logged in)
alias_action :create, :show, :new, :destroy, to: :csnd
if user.has_role? :admin
can :manage, :all
elsif user.has_role? :coach
# do some stuff
elsif user.has_role? :demo
can :read, Profile, demo_featured: true, demo_linked: true
elsif user.has_role? :player
# can do some stuff
else
can :read, Profile
end
end
These are some bits from my ProfilesController:
before_action :set_profile, only: [:show, :edit, :update, :destroy, :invite_user, :profiles]
def set_profile
#profile = Profile.published.includes(:grades, :positions, :achievements, :videos, :transcripts).friendly.find(params[:id])
end
This is what you are looking for:
https://github.com/CanCanCommunity/cancancan/wiki/Exception-Handling
The cancan docs give examples of customizing the message when you authorize! in the controller, and when you manually raise an error, but there doesn't seem to be any mechanism for specifying messages in ability.rb.
Instead, you could catch and modify it in your ApplicationController:
class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |exception|
if current_user.has_role? :demo
redirect_to :back, :alert => "To access this profile, please subscribe here."
end
# render 403, etc.
end
end
Look for rescue_from CanCan::AccessDenied in your main application controller or in your specific controller. It should do something like redirecting to a login page. In my case it's something like this:
rescue_from CanCan::AccessDenied do ||
redirect_to new_user_session_path
end
Since you're producing a different exception message and then displaying it it'd probably be like this, using the flash:
rescue_from CanCan::AccessDenied do |exception|
flash[:notice] = exception.message
redirect_to new_user_session_path
end
Your own logic may vary depending on how you want to handle when the user doesn't have access. It's possible you may even have it setup in a per-controller basis, but this should be the gist of it.
I want to stop users who aren't logged in from accessing the URL using CanCanCan
http://localhost:3000/users
My Ability model is
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
elsif user.roles.size > 0
can :manage, User, :id => user.id
else
can :read, :all
cannot :read, :User
end
end
end
And my Users controller is
class UsersController < ApplicationController
load_and_authorize_resource
def index
#users = User.paginate(page: params[:page],:per_page => 5)
end
def new
# #user = User.new
end
...
end
When I access the page as an guest user. I see the users index page instead of being redirected to login by this code in my application controller
rescue_from CanCan::AccessDenied do |exception|
if user_signed_in?
flash[:error] = "Access denied!"
redirect_to root_url
else
flash[:error] = "Please Sign in"
redirect_to new_user_session_path
end
end
CanCanCan works and stops access to the other actions in the controller just not for index.
In my users controller I was missing
before_filter :authenticate_user!, :except => [:new, :create]
This was allowing the guest user to access the page.
I'd like that after a user logs in, depending on it's role it redirects to some page, I've read this, this, and this and yet I can't seem to do it right, I've tried each one and each has given me issues.
All I want is that after a user logs in, it redirects him/her to x_path, what I have technically works, but after it logs in, it tries to load the redirect_user template.
ApplicationController
class ApplicationController < ActionController::Base
before_filter :authenticate_user!
protect_from_forgery
rescue_from CanCan::AccessDenied do |e|
redirect_to new_user_session_path, alert: e.message
end
def redirect_user
if current_user.role == 'Administrator'
redirect_to rails_admin_path
elsif current_user.role == 'C1' or 'D1' or 'M1' or 'M2'
redirect_to upload_files_path
end
end
end
Routes.rb
Siteconfigurationlistgenerator::Application.routes.draw do
devise_for :users
match '/' => 'application#redirect_user'
Ability
class Ability
include CanCan::Ability
def initialize(user)
#Define abilities for the passed in user here.
user ||= User.new #guest user (not logged in)
#a signed-in user can do everything
if user.role == 'Administrator'
#an admin can do everything
can :manage, :all
can :access, :rails_admin # grant access to rails_admin
can :dashboard # grant access to the dashboard
elsif user.role == '1' or 'M1' or 'D1' or 'M2'
can :manage, [MaterialList, Inventory, UploadFiles, DownloadTemplate]
cannot :access, :rails_admin
cannot :dashboard
can :access, [:upload_files, :download_template]
end
end
end
Currently, as I have it, it says something like template not found, it looks up the redirect_user template, in the views/application folder and it shouldn't, it should just redirect
Your routes.rb should have:
devise_for :users, :controllers => { :sessions => "sessions" }
You then need to create a sessions_controller.rb:
class SessionsController < Devise::SessionsController
protected
def after_sign_in_path_for(resource)
if resource.role == 'Administrator'
rails_admin_path
elsif resource.role == 'C1' or 'D1' or 'M1' or 'M2'
upload_files_path
else
super
end
end
end
You will notice that this method returns the path, it does not call redirect_to I have also added a call to super so that the default behaviour is called if none of the conditions match.