Ugly hack to get ActiveAdmin to play nicely with CanCan #72

Closed
obie opened this Issue May 20, 2011 · 63 comments
@obie

After some heartburn trying to find a more correct solution, I went ahead and disabled CanCan authorization altogether for admin within routes.rb

RightBonus::Application.routes.draw do
  ActiveAdmin.routes(self)
  devise_for :superadmin, ActiveAdmin::Devise.config

  ActiveAdmin::ResourceController.class_eval do
    skip_authorization_check
  end

  #...

CanCan is common enough that there should be a better way to get it to play nicely. Without this hack, you get a lot of

CanCan::AuthorizationNotPerformed (This action failed the check_authorization because it does not authorize_resource. Add skip_authorization_check to bypass this check.):

Ideas?

@gregbell

This is interesting. I haven't used CanCan in a production app yet, so I'm not that familier with how it interacts with controllers. Active Admin doesn't really do any special magic with controllers, so we should be able to get it working. I'm just back from RailsConf and am going out of town for the weekend, but I'll definitely take a look at this early next week.

Cheers,

Greg

@TheKidCoder

Im curious to know why

config.before_filter :skip_authorization_check

In the initializer wouldn’t do the trick. Haven’t looked into it too much though, will put more thought into it this week though.

@obie
@carlosacabrera

I'm having issues trying to use CanCan with ActiveAdmin at all. Is it possible to use roles for Model management and allow certain users to read, create, etc some Models, etc?

@gregbell

I'm looking into getting full support out of the box for CanCan, but in the meantime I've got all the views functioning except for the index (working on it still)

Active Admin uses InheritedResources, which is supported by CanCan by calling the load_and_authorize_resource method on the controller class. In an Active Admin register block, you can access the controller like so:

ActiveAdmin.register Post do
  controller #=> returns the controller class

  # So you could do
  controller.load_and_authorize_resource

  # You can also pass a block to the controller method to 
  # evaluate some code within it
  controller do
    load_and_authorize_resource
  end

CanCan makes some assumption about how we're getting the data for the index action which Active Admin does not follow. I'll take a look and see if its something we can override to support.

@carlosacabrera

@gregbell thanks! that seems to be an elegant solution but I keep getting the error below. Any idea?

ActionView::Template::Error (undefined method `total_pages' for #ActiveRecord::Relation:0x00000101b87750):
1: render renderer_for(:index)

@TheKidCoder

Greg Wrote -> "except for the index (working on it still)"

There is still some work that needs to be done for the index action. I'm sure greg will update us when there is a solution.

@carlosacabrera

Could it be related to this? ryanb/cancan#362

@carlosacabrera

My bad! I see.

@coriordan

just wondering, does anyone know what the status of CanCan support is in Active Admin? What does and doesn't work?

@craigkerstiens

Is there any update on status of this? Is the index still the primary issue on this? I noticed the cancan branch and tried it, but received a different error on the index pages, but still related to pagination. Is that branch expected to be closer than the master to being able to support cancan?

@ghost

I've been working at trying to fix this problem for a site I've been developing. Whilst ugly, the following allows the index action to render with cancan authorization enabled

#Within DashboardController
      def index
        if defined? CanCan then
            authorize! :index, self.class.name
        end
        @dashboard_sections = find_sections
        render_or_default 'index'
      end
@craigkerstiens

That seems to work on the Dashboards Controller, but doesn't help any on the underlying ResourcesController. In other words you're dashboard works, but any of the indexes for your models don't seem to.

Applying the same code to the ResourcesController doesn't seem to work either.

@ghost

For every active_admin page i've just inserted the following.

controller.authorize_resource

In the Dashboard controller it might be better to use admin_prefix + "::Dashboard" with the authorize! call.

@paulogeyer

How can I acces the can? and cannot? methods when registering a resource?

I've found that I can hide the resrouces from menu using "menu false", but I can't access cancan abilities from inside resource definition

@apauly

As dysfictional mentioned, this works like a charm:

ActiveAdmin.register Post do
  controller.authorize_resource
end

It works on index pages and all the other single resource pages.

@paulogeyer: You can access the "can?" and "cannot?" methods through the controller, e.g.:


controller.current_ability.can?

What I'm trying to do now, is to hide all resources from the menu, that can't be accessed by the current user. Actually I'm doing something like this:

ActiveAdmin.register Post do
  controller.authorize_resource

  menu :if => lambda{|tabs_renderer|
    controller.current_ability.can?(:manage, Post)
  }
end

This works for me - the only downside is that I have to re-mention the resource class in this block.
I digged a bit into the code, and all that is passed to the block is the current instance of the TabsRenderer. So in this example, I can't check the users access for the Post class, because I can't access the resource class.

@paulogeyer

thanks @apauly!

I wanted to hide the links, authorize_resource was working perfectly but still left the links for unauthorized resources. I'll try your solution for can and cannot methods

@paulogeyer

@apauly I've tried your solution, but the resource still is on menu, and when I click on it, returns an authorization error

@apauly

@paulogeyer Have you changed both appearances of "Post"? I just tried this solution in production mode (which means cache_classes = true) with several different users - it works.
I'm using ActiveAdmin on the master branch - not the cancan branch. Do you have perhaps some other specific configurations?

@paulogeyer

Changed my gemfile to use the master branch, it is working now! Thanks again!

@joselo

@apuly have you found a way to hide the menu??, I was trying the same, but seems like the menu method doesn't accept an :if clause it just ignore the block

I was trying using proc, like this

menu proc{ controller.current_ability.can?(:manage, User) }

However I got this error

undefined method `current_ability' for Admin::UsersController:Class
@apauly

Well, you need to define a method which returns the current users ability.

For example something like this:

class ApplicationController < ActionController::Base
  def current_ability
    @current_ability ||= Ability.new(current_admin_user)
  end
end
@joselo

Thanks!! Actually was my mistake, I didn't have to include that method in the app controller. I only had to change my Gemfile to use the latest version:

gem 'activeadmin', :git => 'https://github.com/gregbell/active_admin.git'

it's working now :)

@marzocchi

About the pagination error in the index action...

The following worked for me, while still allowing me to use abilities with conditions or blocks (which implies using #load_resource, which in turn results in the “undefined method `total_pages'” error in the index).

ActiveAdmin.register Foo do
  controller.skip_load_resource :only => :index
  controller.load_and_authorize_resource
end
@bradphelan

I've added the trick

  config.before_filter :skip_authorization_check

as suggested and I can get to the login screen. I log in with admin@example.com:password then it redirects to

/admin

which automatically redirects to

/users/sign_in

This seems to be some interaction between my current use of devise and active_admin. The top of my routes file is

Mysugrapp::Application.routes.draw do 
  ActiveAdmin.routes(self)

  devise_for :users

  devise_for :admin_users, ActiveAdmin::Devise.config

and my application controller is

class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :authenticate_user!

  # ensure all controllers are explicity authorized
  # check_authorization


The error trace is. https://gist.github.com/1109441. Anything obvious I might be doing wrong here?

and if I comment out the devise_for :users line then I get the error when visiting /admin

Started GET "/admin" for 127.0.0.1 at 2011-07-27 16:28:41 +0200
  Processing by Admin::DashboardController#index as HTML
Completed 500 Internal Server Error in 142ms
undefined method `authenticate_user!' for #<Admin::DashboardController:0x00000107c98c38>

why would it call authenticate_user when I have

  # == User Authentication
  #
  # Active Admin will automatically call an authentication 
  # method in a before filter of all controller actions to 
  # ensure that there is a currently logged in admin user.
  #
  # This setting changes the method which Active Admin calls
  # within the controller.
  config.authentication_method = :authenticate_admin_user!

set in the config/initializers/active_admin.rb

Regards

Brad

@bradphelan

I seem to have found my way around the above problems. Finally found out that the active_admin controllers inherit from InheritedResource::Base which itself inherits from ApplicationController where I had the before_filter set to authenticate_user!.

Is this really the right behaviour that active_admin should inherit from my application controller. Anyway I created a new class called ResourceController where I shoved in all my code that was in ApplicationController which is now empty.

@tobyhede

I am having some problems with cancan authorization.

I am attempting to get a form to display fields based on the current user role:

f controller.current_ability.can? :manage, @user
  f.inputs "Roles" do
    f.input :roles, :as => :select, :collection => Role.by_name.all
  end
end

But when I use the above, I see:

undefined method `current_ability' for Admin::UsersController:Class

I have tried adding the "current_ability" method to the ApplicationController as well as using:

controller do
  def current_ability
    @current_ability ||= Ability.new(current_admin_user)
  end
end

Does anyone have any ideas?

@tobyhede

I got it working by dropping to a partial ... might be interesting to look at having the form in the view context like the index portions of the dsl?

@denis

This works for me - the only downside is that I have to re-mention the resource class in this block.
I digged a bit into the code, and all that is passed to the block is the current instance of the TabsRenderer. So in this example, I can't check the users access for the Post class, because I can't access the resource class.

@apauly, you can access resource class as config.resource.

So you code becomes:

ActiveAdmin.register Post do
  controller.authorize_resource

  menu :if => lambda { |tabs_renderer|
    controller.current_ability.can?(:manage, config.resource)
  }
end
@leonels

Still getting error: undefined method num_pages

ActiveAdmin.register Project do
  controller
  controller.skip_load_resource :only => :index
  controller.load_and_authorize_resource

  controller do    
    def index
     @projects = Project.accessible_by(current_ability)
    end

I also tried this query in the index action:

@projects = Project.all(:conditions => ["user_id == ?", current_user.id])

I uninstalled the will_paginate gem because active_admin now uses kaminari. I'm using Devise and CanCan.

Anybody knows how to load index resource without getting an error?

@leonels

I found a way to get it working on the index action: #355

@macfanatic

@tobyhede - did you figure your issue out? Everyone keeps mentioning to add def current_ability to the ApplicationController < ActionController::Base class, but I'm just getting the error you mention above.

@vairix-ssierra

@macfanatic - you should add

def current_ability 
  @current_ability ||= Ability.new(current_user) 
end

to the ApplicationController < ActionController::Base and

config.before_filter :current_ability

to your initializers/active_admin.rb

@macfanatic

I'm using the current master of active admin & have the following items in place:

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  protect_from_forgery

  def current_user
    current_admin_user
  end

  def current_ability 
    @current_ability ||= Ability.new(current_user) 
  end

  before_filter :set_timezone
  def set_timezone
    Time.zone = current_admin_user.time_zone if current_admin_user and !current_admin_user.time_zone.blank?
  end

  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end

end

In config/initializers/active_admin.rb

ActiveAdmin.setup do |config|
  config.authentication_method = :authenticate_admin_user!
  config.current_user_method = :current_admin_user
  config.before_filter :current_ability
end

In app/admin/admin_users.rb

ActiveAdmin.register ActiveAdmin do
    if controller.current_ability.can?( :change_password, resource )
      action_item :only => [:show, :edit] do
        link_to "Change Password", change_password_app_admin_user_path( resource )
      end
    end
end

With all of this, after restarting the server, I receive the following errors:

    undefined method `current_ability' for App::AdminUsersController:Class
@vairix-ssierra

In app/admin/admin_users.rb you should have

ActiveAdmin.register ActiveAdmin do
  controller.authorize_resource

  if can?(:change_password)
    action_item :only => [:show, :edit] do
      link_to "Change Password", change_password_app_admin_user_path(resource)
    end
  end

end
@macfanatic

Your above code still produces the same error for me.

I have a hunch that maybe this is related to the order in which the gems are being loaded, as no matter what I try or where I try it (hacked together some code in active_admin itself for the action_items.rb file), I get controller.can? is not defined.

Can you post what order you have your gems loaded? New to Rails, but just a thought that I'm going to look more into.

@vairix-ssierra
gem 'rails', '3.1.0'
gem 'activeadmin'
...
gem "cancan"

Remember to reload the server after making any changes to the initializers:

In config/initializers/active_admin.rb I have:

config.before_filter :current_ability
config.authentication_method = :authenticate_admin_user!
config.current_user_method = :current_admin_user
@macfanatic
gem 'rails', '3.0.9'
gem 'rake', "0.9.2"
gem 'activeadmin', :git => 'git://github.com/gregbell/active_admin.git'
gem 'cancan', :git => 'git://github.com/ryanb/cancan.git'

In config/initalizers/active_admin.rb

  config.before_filter :current_ability
  config.authentication_method = :authenticate_admin_user!
  config.current_user_method = :current_admin_user

I tried moving the CanCan gem higher up in the Gemfile & re-running Bunlder, restarting the server, etc - all still with the undefined can? method error.

I'm not sure what else to try, other than upgrading to Rails 3.1 (don't want to fiddle with right now) or creating a new project from scratch and trying to get this all in place, but that's still a good investment of time. Appreciate all your help, any other thoughts?

Using the code you posted a few lines above, I get an error that can? isn't defined for ActiveAdmin::DSL, but every other way I've tried just executing the method on the controller context itself, and I get Admin::UsersController:Class doesn't define can?.

@vairix-ssierra

Does this work for you?

ActiveAdmin.register AdminUser do

  controller.authorize_resource

  if controller.current_ability.can?( :change_password, resource )
    action_item :only => [:show, :edit] do
      link_to "Change Password", change_password_app_admin_user_path( resource )
    end
  end
end
@macfanatic

Nope, get undefined method 'current_ability' for App::AdminUsersController:Class error.

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  protect_from_forgery

  def current_user
    current_admin_user
  end

  def current_ability 
    @current_ability ||= Ability.new(current_user) 
  end

  before_filter :set_timezone
  def set_timezone
    Time.zone = current_admin_user.time_zone if current_admin_user and !current_admin_user.time_zone.blank?
  end

  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end

end

You've seen config/initializers/active_admin.rb & my Gemfile

Trying the exact code you have above, with the exception of changing the first line to:
ActiveAdmin.register AdminUser do

@jcuervo

@doabit, I added some updates and added the link to the wiki home. Others can contribute and maybe load some stuff that worked for them (with some explanations): https://github.com/gregbell/active_admin/wiki

@macfanatic

Thanks for posting the wiki, but that doesn't cover the issue that I was discussing with CanCan here.

I want to conditionally show a action_item and, as such, need access to the current_ability or can? instance methods defined on the ApplicationController class.

The problem is that ActiveAdmin::DSL.controller returns a Class object, not the instance of the controller itself. If I define current_ability on ApplicationController as a class method, it does in fact get found, but I still can't use CanCan b/c that's all defined as instance methods.

class ApplicationController < ActionController::Base
  def self.current_ability
    @ability ||= Ability.new(nil) #can't use current_admin_user(), as that is an instance method
  end
end

This will in fact work without binding errors, but is logically broken (as it doesn't use the current user session at all)

ActiveAdmin.register Post do
  if controller.current_ability.can?( :index, Post) 
    action_item :testing, :only => :index do
      Rails.logger.info ":testing"
    end
  end
end

Is there a way to get the actual controller instance being used from the DSL, instead of an instance of a Class object, describing the class of the controller?

@macfanatic

Ok - I looked through someone else's posting of a quick hack to wrap the ActiveAdmin::Resource::ActionItems module code in conditionals, and hit me that I could re-use this for my purposes inside the ActiveAdmin.register block, I just needed to move the conditional statement inside the function call from outside, as the reference to controller is different per that context.

Example:

ActiveAdmin.register Post do
  action_item :only => [:edit, :show] do
    if controller.current_ability.can?( :change_password, resource ) 
      link_to "Change Password", change_password_app_admin_user_path( resource )
    end
  end
end
@doabit

Yes,that can work well. I use

  action_item :only => [:show] do
    if can?(:top,resource) 
      link_to "�Top Post", top_admin_post_path(resource)
    end
  end
@gutenye

@doabit

there's an error in wiki, https://github.com/gregbell/active_admin/wiki/How-to-work-with-cancan

ActiveAdmin.register AdminUser do       
  controller.authorize_resource 
end 

you need controller.load_resource before controller.authorize_resource, but I still can't find a way to use controller.load_and_authorize_resource

@holden

What about the scope_to method? Cancan seems to work fine with the menu and action_items but I can't similarly tame the scope_to method.

scope_to :current_user, :association_method => :space_bookings unless proc{ can?(:manage, :all) }

I'd like to use the scope_to on everyone except admins... and have tried every variation I can think of.

scope_to :current_user, :association_method => :space_bookings unless proc{ current_user.admin? }

etc.

Any ideas?

@DMajrekar

I've been working on getting this working in a slightly neater way and come up with the following initialiser:

require 'active_admin'
module ActiveAdmin
  class ResourceController < BaseController

    module CanCanCollection
      def active_admin_collection
        cancan_restrict(super)
      end

      def cancan_restrict(chain)
        chain.accessible_by(current_ability, :show)
      end
    end

    include CanCanCollection
  end

  class Namespace
    def register_resource_controller(config)
      class_def =<<-CLASS_DEF
  class ::#{config.controller_name} < ActiveAdmin::ResourceController
    load_and_authorize_resource :class => #{config.resource_class}
    skip_load_resource :only => :index
  end
      CLASS_DEF
      eval class_def
      config.controller.active_admin_config = config
    end
  end
end

This adds authorisation to all resource pages in active admin and ensures that only authorised resources are visible on index pages.

FYI, I tried adding:

    load_and_authorize_resource
    skip_load_resource :only => :index

within the initial monkey patch for ResourceController but this caused problems with the DashboardPage.

If this wants to be added to the core of ActiveAdmin, rather than some sort of ActiveAdminCanCan gem, I think this should be all extracted into a single module that check for the existence on CanCan and then injects the relevant code. Also, I would suggest that string evaluated in register_resource_controller is moved to a separate method call (like the dashboard currently works) which is overridden in a separate module.

Let me know what you think, and if everyone is happy going down this sort of path, I can look at adding this into ActiveAdmin with tests etc.

Also, related to limiting the display of Action Item, I've added some snippets to #355 that people might find useful.

EDIT: Update code related to bug fix below

@lacco

Snippet to disable CanCan:

require 'active_admin'
module ActiveAdmin
  class ResourceController
    skip_authorization_check
  end
end
@DMajrekar

I've noticed an issue with my Initializer when the resource's name has been changed using the :as option to ActiveAdmin.register. This can be fixed by changing the line:

load_and_authorize_resource

to:

load_and_authorize_resource :class => #{config.resource_class}

in the initlaizer. NB: I've edited the code in my initial comment

@jmonster

@GutenYe I can't get this to work -- the load_resource portion causes this error:
undefined method `num_pages' for #ActiveRecord::Relation:0x007fbb6896f908

@DMajrekar where is this supposed to go? I'm getting the following erorr when I tried placing it in a few places (/lib, initializers)

/Users/jd/.rvm/gems/ruby-1.9.3-p0/gems/activeadmin-0.3.4/lib/active_admin/namespace.rb:176:in `eval': uninitialized constant ActiveAdmin::BaseController (NameError)

@jmonster

@DMajrekar but apparently just dropping it in an initializer works fine /if/ I reference your fork ... but then nothing appears to work correctly. I've lost links to my oauth login from the login screen + every controller I hit returns not authorized, regardless of what my ability file says.

@DMajrekar

@jmonster The correct place for the file in in config/initializers. You'll need to use the HEAD of active_admin rather than the gem as there has been a change to the ResourceController inheritance chain. FYI, my Gemfile has the following

gem 'activeadmin',                  '~> 0.3.2', :git => 'git://github.com/gregbell/active_admin.git', :ref => 'HEAD'
@ches

Is there a pretty way to ask, "is this an ActiveAdmin controller?" e.g. for the blunt case of skipping CanCan altogether, Devise for instance has a predicate method so one can do this in ApplicationController:

check_authorization :unless => :devise_controller?
@DMajrekar

@ches What use case are you thinking of when skipping authorisation? If you're using my patch above, authorisation is only be included inside ActiveAdmin controllers so I'm unsure of why you'd need to place anything like this at the ApplicationController level.

@ches

@DMajrekar Yeah, speaking for the case of current released ActiveAdmin version, or master. I can get away with skipping on an application where there isn't currently a strong need for granular admin authorization -- there's only really a "god role" needed for now, and admins are a separate user model/Devise scope (ActiveAdmin's default AdminUser setup) so that satisfies this well enough. By the time we need more control, smoother CanCan support may be officially integrated.

I just find the approach of a check_authorization condition in ApplicationController cleaner and more visible than monkey-patching a skip_authorization_check in, so that would be preferable to recommend to people for now if it existed. I can easily imagine myself later trying to get CanCan working in admin controllers and scratching my head for awhile wondering why it isn't working because of a monkey patch hidden away in an initializer that I never look at.

@DMajrekar

@ches I can see what you mean. So far, I've only needed an all or nothing approach so the initialiser works well as a fire and forget type thing.

Seeing as all of the ActiveAdmin controllers inherit from ActiveAdmin::BaseController, would a proc checking the inheritance chain work:

check_authorization :unless => proc { |controller| controller.kind_of?(ActiveAdmin::BaseController) }

I guess you should be able to place this in a method as well, and then use the method name as a symbol in place of the proc.

I've not tested it, but it might do what you need.
NB: This would apply to the current master of ActiveAdmin as the BaseController is still un-released functionality

EDIT: Clarity

@travisp

@DMajrekar I don't believe that your approach works because the :unless and :if options for check_authorization only support a method name as a symbol.

The following does work however:

check_authorization :unless => admin_controller?

def admin_controller?
   self.kind_of?(ActiveAdmin::BaseController)
end
@DMajrekar

@travisp That makes sense

@gregbell

I'm closing this issue for now. It appears that there is plenty of documentation in the wiki for using CanCan with Active Admin. I've also added a new feature (unscheduled) for an authorization layer abstraction with a CanCan adapter.

@gregbell gregbell closed this Feb 16, 2012
@miguelperez

Hi, I think I am missing something.

I setup an ability like

  can :read, AdminUser, :role => 'active'

But still that admin_user can read all the AdminUsers. So I do not know what I am doing something wrong. I noticed up in this post, that the support for the index action it's still a work in progress, but I wanted to know if there is any ugly hack I could use so users are only able of seeing objects they are allowed to read.

Thanks in advance.

@macfanatic

You're missing a call to filter the listing of users down to the ones that they should have access to - this is a two step process in CanCan.

In your app/admin/admin_users.rb file:

  controller do
    def scoped_collection
      end_of_association_chain.accessible_by(current_ability)
    end
  end

In your ability class:

class Ability
  include CanCan::Ability

  def initialize(user, account)

    return if user.nil?

    can :batch_action, :all

    if user.admin?

            can :manage, :all

    elsif user.employee?

      can [:edit, :update, :change_password, :process_password_change, :profile], AdminUser, :id => user.id

    end

  end

end

The Ability model allows you to define that the "employee" role user can only edit & update their own record (with custom member_action actions I have in place for change_password, process_password_change & profile) while admins can get to all of them.

@gravis

FYI, I'm having this issue too, without using cancan at all. I can't get AA to authenticate admins, I always have to sign in a user before.
Anyway, I don't think application_controller should be used, frontend has most of the time nothing to do with backoffice, so if methods are shared, we should likely use an included module instead. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment