diff --git a/.travis.yml b/.travis.yml index 13bd18573e..aef76bf3e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,9 +68,6 @@ matrix: - rvm: 2.6.3 env: CI_ORM=active_record CI_DB_ADAPTER=postgresql CI_DB_USERNAME=postgres gemfile: gemfiles/rails_6.0.gemfile - - rvm: 2.6.3 - env: CI_ORM=active_record CI_DB_ADAPTER=sqlite3 - gemfile: gemfiles/cancan.gemfile - rvm: ruby-head env: CI_ORM=mongoid gemfile: gemfiles/rails_5.2.gemfile diff --git a/Appraisals b/Appraisals index e5abbd38f1..918126dd72 100644 --- a/Appraisals +++ b/Appraisals @@ -107,13 +107,3 @@ appraise "rails-6.0" do gem 'paper_trail', '>= 5.0' end end - -appraise "cancan" do - gem 'rails', '~> 5.1.0' - gem 'sassc-rails', '~> 2.1' - gem 'devise', '~> 4.0' - - group :test do - gem 'cancan', '>= 1.6' - end -end diff --git a/gemfiles/cancan.gemfile b/gemfiles/cancan.gemfile deleted file mode 100644 index 93b35b9e0a..0000000000 --- a/gemfiles/cancan.gemfile +++ /dev/null @@ -1,51 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal", ">= 2.0" -gem "rails", "~> 5.1.0" -gem "haml" -gem "devise", "~> 4.0" -gem "sassc-rails", "~> 2.1" - -group :active_record do - gem "paper_trail" - - platforms :ruby, :mswin, :mingw do - gem "mysql2", ">= 0.3.14" - gem "sqlite3", ">= 1.3" - end -end - -group :development, :test do - gem "pry", ">= 0.9" -end - -group :test do - gem "carrierwave", [">= 2.0.0.rc", "< 3"] - gem "coveralls" - gem "database_cleaner", [">= 1.2", "!= 1.4.0", "!= 1.5.0"] - gem "dragonfly", "~> 1.0" - gem "factory_bot", ">= 4.2" - gem "generator_spec", ">= 0.8" - gem "launchy", ">= 2.2" - gem "mini_magick", ">= 3.4" - gem "paperclip", [">= 3.4", "!= 4.3.0"] - gem "poltergeist", "~> 1.5" - gem "pundit" - gem "rack-cache", require: "rack/cache" - gem "rspec-rails", ">= 2.14" - gem "rspec-expectations", "!= 3.8.3" - gem "rubocop", "~> 0.41.2" - gem "simplecov", ">= 0.9", require: false - gem "shrine", "~> 2.0" - gem "shrine-memory" - gem "timecop", ">= 0.5" - gem "cancan", ">= 1.6" - - platforms :ruby_19 do - gem "tins", "~> 1.6.0", require: false - end -end - -gemspec path: "../" diff --git a/lib/generators/rails_admin/templates/initializer.erb b/lib/generators/rails_admin/templates/initializer.erb index 570a244c9d..0419a08891 100644 --- a/lib/generators/rails_admin/templates/initializer.erb +++ b/lib/generators/rails_admin/templates/initializer.erb @@ -8,8 +8,8 @@ RailsAdmin.config do |config| # end # config.current_user_method(&:current_user) - ## == Cancan == - # config.authorize_with :cancan + ## == CancanCan == + # config.authorize_with :cancancan ## == Pundit == # config.authorize_with :pundit diff --git a/lib/rails_admin.rb b/lib/rails_admin.rb index 369a0b7d1f..569e6cb0a3 100644 --- a/lib/rails_admin.rb +++ b/lib/rails_admin.rb @@ -2,7 +2,6 @@ require 'rails_admin/abstract_model' require 'rails_admin/config' require 'rails_admin/extension' -require 'rails_admin/extensions/cancan' require 'rails_admin/extensions/cancancan' require 'rails_admin/extensions/pundit' require 'rails_admin/extensions/paper_trail' diff --git a/lib/rails_admin/config.rb b/lib/rails_admin/config.rb index 81ee1c251c..a938ccf31c 100644 --- a/lib/rails_admin/config.rb +++ b/lib/rails_admin/config.rb @@ -141,11 +141,11 @@ def audit_with(*args, &block) # end # # To use an authorization adapter, pass the name of the adapter. For example, - # to use with CanCan[https://github.com/ryanb/cancan], pass it like this. + # to use with CanCanCan[https://github.com/CanCanCommunity/cancancan/], pass it like this. # - # @example CanCan + # @example CanCanCan # RailsAdmin.config do |config| - # config.authorize_with :cancan + # config.authorize_with :cancancan # end # # See the wiki[https://github.com/sferik/rails_admin/wiki] for more on authorization. diff --git a/lib/rails_admin/config/actions/base.rb b/lib/rails_admin/config/actions/base.rb index 423861decc..c604ea6c57 100644 --- a/lib/rails_admin/config/actions/base.rb +++ b/lib/rails_admin/config/actions/base.rb @@ -99,7 +99,7 @@ class Base key.to_sym end - # For Cancan and the like + # For CanCanCan and the like register_instance_option :authorization_key do key.to_sym end diff --git a/lib/rails_admin/extensions/cancan.rb b/lib/rails_admin/extensions/cancan.rb deleted file mode 100644 index 46994cf972..0000000000 --- a/lib/rails_admin/extensions/cancan.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'rails_admin/extensions/cancan/authorization_adapter' - -RailsAdmin.add_extension(:cancan, RailsAdmin::Extensions::CanCan, authorization: true) diff --git a/lib/rails_admin/extensions/cancan/authorization_adapter.rb b/lib/rails_admin/extensions/cancan/authorization_adapter.rb deleted file mode 100644 index b518a89919..0000000000 --- a/lib/rails_admin/extensions/cancan/authorization_adapter.rb +++ /dev/null @@ -1,57 +0,0 @@ -module RailsAdmin - module Extensions - module CanCan - # This adapter is for the CanCan[https://github.com/ryanb/cancan] authorization library. - # You can create another adapter for different authorization behavior, just be certain it - # responds to each of the public methods here. - class AuthorizationAdapter - # See the +authorize_with+ config method for where the initialization happens. - def initialize(controller, ability = ::Ability) - @controller = controller - @controller.instance_variable_set '@ability', ability - @controller.extend ControllerExtension - @controller.current_ability.authorize! :access, :rails_admin - end - - # This method is called in every controller action and should raise an exception - # when the authorization fails. The first argument is the name of the controller - # action as a symbol (:create, :bulk_delete, etc.). The second argument is the - # AbstractModel instance that applies. The third argument is the actual model - # instance if it is available. - def authorize(action, abstract_model = nil, model_object = nil) - @controller.current_ability.authorize!(action, model_object || abstract_model && abstract_model.model) if action - end - - # This method is called primarily from the view to determine whether the given user - # has access to perform the action on a given model. It should return true when authorized. - # This takes the same arguments as +authorize+. The difference is that this will - # return a boolean whereas +authorize+ will raise an exception when not authorized. - def authorized?(action, abstract_model = nil, model_object = nil) - @controller.current_ability.can?(action, model_object || abstract_model && abstract_model.model) if action - end - - # This is called when needing to scope a database query. It is called within the list - # and bulk_delete/destroy actions and should return a scope which limits the records - # to those which the user can perform the given action on. - def query(action, abstract_model) - abstract_model.model.accessible_by(@controller.current_ability, action) - end - - # This is called in the new/create actions to determine the initial attributes for new - # records. It should return a hash of attributes which match what the user - # is authorized to create. - def attributes_for(action, abstract_model) - @controller.current_ability.attributes_for(action, abstract_model && abstract_model.model) - end - - module ControllerExtension - def current_ability - # use _current_user instead of default current_user so it works with - # whatever current user method is defined with RailsAdmin - @current_ability ||= @ability.new(_current_user) - end - end - end - end - end -end diff --git a/lib/rails_admin/extensions/cancancan/authorization_adapter.rb b/lib/rails_admin/extensions/cancancan/authorization_adapter.rb index 645bf1d676..a2f11d7a86 100644 --- a/lib/rails_admin/extensions/cancancan/authorization_adapter.rb +++ b/lib/rails_admin/extensions/cancancan/authorization_adapter.rb @@ -2,7 +2,28 @@ module RailsAdmin module Extensions module CanCanCan # This adapter is for the CanCanCan[https://github.com/CanCanCommunity/cancancan] authorization library. - class AuthorizationAdapter < RailsAdmin::Extensions::CanCan::AuthorizationAdapter + class AuthorizationAdapter + module ControllerExtension + def current_ability + # use _current_user instead of default current_user so it works with + # whatever current user method is defined with RailsAdmin + @current_ability ||= @ability.new(_current_user) + end + end + + # See the +authorize_with+ config method for where the initialization happens. + def initialize(controller, ability = ::Ability) + @controller = controller + @controller.instance_variable_set '@ability', ability + @controller.extend ControllerExtension + @controller.current_ability.authorize! :access, :rails_admin + end + + # This method is called in every controller action and should raise an exception + # when the authorization fails. The first argument is the name of the controller + # action as a symbol (:create, :bulk_delete, etc.). The second argument is the + # AbstractModel instance that applies. The third argument is the actual model + # instance if it is available. def authorize(action, abstract_model = nil, model_object = nil) return unless action subject = model_object || abstract_model && abstract_model.model @@ -13,6 +34,10 @@ def authorize(action, abstract_model = nil, model_object = nil) end end + # This method is called primarily from the view to determine whether the given user + # has access to perform the action on a given model. It should return true when authorized. + # This takes the same arguments as +authorize+. The difference is that this will + # return a boolean whereas +authorize+ will raise an exception when not authorized. def authorized?(action, abstract_model = nil, model_object = nil) return unless action subject = model_object || abstract_model && abstract_model.model @@ -20,6 +45,20 @@ def authorized?(action, abstract_model = nil, model_object = nil) @controller.current_ability.can?(*resolve_with_compatibility(action, subject)) end + # This is called when needing to scope a database query. It is called within the list + # and bulk_delete/destroy actions and should return a scope which limits the records + # to those which the user can perform the given action on. + def query(action, abstract_model) + abstract_model.model.accessible_by(@controller.current_ability, action) + end + + # This is called in the new/create actions to determine the initial attributes for new + # records. It should return a hash of attributes which match what the user + # is authorized to create. + def attributes_for(action, abstract_model) + @controller.current_ability.attributes_for(action, abstract_model && abstract_model.model) + end + private def authorized_for_dashboard_in_legacy_way?(action, silent = false) diff --git a/spec/integration/authorization/cancan_spec.rb b/spec/integration/authorization/cancan_spec.rb deleted file mode 100644 index 61d6c36c7e..0000000000 --- a/spec/integration/authorization/cancan_spec.rb +++ /dev/null @@ -1,348 +0,0 @@ -require 'spec_helper' - -RSpec.describe 'RailsAdmin CanCan Authorization', type: :request do - class Ability - include CanCan::Ability - def initialize(user) - can :access, :rails_admin if user.roles.include? :admin - if user.roles.include? :test_exception - can :dashboard - can :access, :rails_admin - can :manage, :all - can :show_in_app, :all - - # fix for buggy and inconsistent behaviour in Cancan 1.6.8 => https://github.com/ryanb/cancan/issues/721 - if CI_ORM != :mongoid - cannot [:update, :destroy], Player - can [:update, :destroy], Player, retired: false - else - cannot [:update, :destroy], Player, retired: true - end - else - can :dashboard - can :manage, Player if user.roles.include? :manage_player - can :read, Player, retired: false if user.roles.include? :read_player - can :create, Player, suspended: true if user.roles.include? :create_player - can :update, Player, retired: false if user.roles.include? :update_player - can :destroy, Player, retired: false if user.roles.include? :destroy_player - can :history, Player, retired: false if user.roles.include? :history_player - can :show_in_app, Player, retired: false if user.roles.include? :show_in_app_player - end - end - end - - class AdminAbility - include CanCan::Ability - def initialize(user) - can :access, :rails_admin if user.roles.include? :admin - can :show_in_app, :all - can :manage, :all - end - end - - subject { page } - - before do - RailsAdmin.config do |c| - c.authorize_with(:cancan) - c.authenticate_with { warden.authenticate! scope: :user } - c.current_user_method(&:current_user) - end - @player_model = RailsAdmin::AbstractModel.new(Player) - @user = FactoryBot.create :user - login_as @user - end - - describe 'with no roles' do - before do - @user.update_attributes(roles: []) - end - - it 'GET /admin should raise CanCan::AccessDenied' do - expect { visit dashboard_path }.to raise_error(CanCan::AccessDenied) - end - - it 'GET /admin/player should raise CanCan::AccessDenied' do - expect { visit index_path(model_name: 'player') }.to raise_error(CanCan::AccessDenied) - end - end - - describe 'with read player role' do - before do - @user.update_attributes(roles: [:admin, :read_player]) - end - - it 'GET /admin should show Player but not League' do - visit dashboard_path - is_expected.to have_content('Player') - is_expected.not_to have_content('League') - is_expected.not_to have_content('Add new') - end - - it 'GET /admin/player should render successfully but not list retired players and not show new, edit, or delete actions' do - # ensure :name column to be shown - RailsAdmin.config Player do - list do - field :name - end - end - @players = [ - FactoryBot.create(:player, retired: false), - FactoryBot.create(:player, retired: true), - ] - - visit index_path(model_name: 'player') - - is_expected.to have_content(@players[0].name) - is_expected.not_to have_content(@players[1].name) - is_expected.not_to have_content('Add new') - is_expected.to have_css('.show_member_link') - is_expected.not_to have_css('.edit_member_link') - is_expected.not_to have_css('.delete_member_link') - is_expected.not_to have_css('.history_show_member_link') - is_expected.not_to have_css('.show_in_app_member_link') - end - - it 'GET /admin/team should raise CanCan::AccessDenied' do - expect { visit index_path(model_name: 'team') }.to raise_error(CanCan::AccessDenied) - end - - it 'GET /admin/player/new should raise CanCan::AccessDenied' do - expect { visit new_path(model_name: 'player') }.to raise_error(CanCan::AccessDenied) - end - end - - describe 'with create and read player role' do - before do - @user.update_attributes(roles: [:admin, :read_player, :create_player]) - end - - it 'GET /admin/player/new should render and create record upon submission' do - visit new_path(model_name: 'player') - - is_expected.not_to have_content('Save and edit') - is_expected.not_to have_content('Delete') - - is_expected.to have_content('Save and add another') - fill_in 'player[name]', with: 'Jackie Robinson' - fill_in 'player[number]', with: '42' - fill_in 'player[position]', with: 'Second baseman' - click_button 'Save' # first(:button, "Save").click - is_expected.not_to have_content('Edit') - - @player = RailsAdmin::AbstractModel.new('Player').first - expect(@player.name).to eq('Jackie Robinson') - expect(@player.number).to eq(42) - expect(@player.position).to eq('Second baseman') - expect(@player).to be_suspended # suspended is inherited behavior based on permission - end - - it 'GET /admin/player/1/edit should raise access denied' do - @player = FactoryBot.create :player - expect { visit edit_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) - end - end - - describe 'with update and read player role' do - before do - @user.update_attributes(roles: [:admin, :read_player, :update_player]) - end - - it 'GET /admin/player/1/edit should render and update record upon submission' do - @player = FactoryBot.create :player - visit edit_path(model_name: 'player', id: @player.id) - is_expected.to have_content('Save and edit') - is_expected.not_to have_content('Save and add another') - is_expected.not_to have_content('Add new') - is_expected.not_to have_content('Delete') - is_expected.not_to have_content('History') - is_expected.not_to have_content('Show in app') - fill_in 'player[name]', with: 'Jackie Robinson' - click_button 'Save' # click_button "Save" # first(:button, "Save").click - @player.reload - expect(@player.name).to eq('Jackie Robinson') - end - - it 'GET /admin/player/1/edit with retired player should raise access denied' do - @player = FactoryBot.create :player, retired: true - expect { visit edit_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) - end - - it 'GET /admin/player/1/delete should raise access denied' do - @player = FactoryBot.create :player - expect { visit delete_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) - end - end - - describe 'with history role' do - it 'shows links to history action' do - @user.update_attributes(roles: [:admin, :read_player, :history_player]) - @player = FactoryBot.create :player - - visit index_path(model_name: 'player') - is_expected.to have_css('.show_member_link') - is_expected.not_to have_css('.edit_member_link') - is_expected.not_to have_css('.delete_member_link') - is_expected.to have_css('.history_show_member_link') - - visit show_path(model_name: 'player', id: @player.id) - is_expected.to have_content('Show') - is_expected.not_to have_content('Edit') - is_expected.not_to have_content('Delete') - is_expected.to have_content('History') - end - end - - describe 'with show in app role' do - it 'shows links to show in app action' do - @user.update_attributes(roles: [:admin, :read_player, :show_in_app_player]) - @player = FactoryBot.create :player - - visit index_path(model_name: 'player') - is_expected.to have_css('.show_member_link') - is_expected.not_to have_css('.edit_member_link') - is_expected.not_to have_css('.delete_member_link') - is_expected.not_to have_css('.history_show_member_link') - is_expected.to have_css('.show_in_app_member_link') - - visit show_path(model_name: 'player', id: @player.id) - is_expected.to have_content('Show') - is_expected.not_to have_content('Edit') - is_expected.not_to have_content('Delete') - is_expected.not_to have_content('History') - is_expected.to have_content('Show in app') - end - end - - describe 'with all roles' do - it 'shows links to all actions' do - @user.update_attributes(roles: [:admin, :manage_player]) - @player = FactoryBot.create :player - - visit index_path(model_name: 'player') - is_expected.to have_css('.show_member_link') - is_expected.to have_css('.edit_member_link') - is_expected.to have_css('.delete_member_link') - is_expected.to have_css('.history_show_member_link') - is_expected.to have_css('.show_in_app_member_link') - - visit show_path(model_name: 'player', id: @player.id) - is_expected.to have_content('Show') - is_expected.to have_content('Edit') - is_expected.to have_content('Delete') - is_expected.to have_content('History') - is_expected.to have_content('Show in app') - end - end - - describe 'with destroy and read player role' do - before do - @user.update_attributes(roles: [:admin, :read_player, :destroy_player]) - end - - it 'GET /admin/player/1/delete should render and destroy record upon submission' do - @player = FactoryBot.create :player - player_id = @player.id - visit delete_path(model_name: 'player', id: player_id) - - click_button "Yes, I'm sure" - - expect(@player_model.get(player_id)).to be_nil - end - - it 'GET /admin/player/1/delete with retired player should raise access denied' do - @player = FactoryBot.create :player, retired: true - expect { visit delete_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) - end - - it 'GET /admin/player/bulk_delete should render records which are authorized to' do - active_player = FactoryBot.create :player, retired: false - retired_player = FactoryBot.create :player, retired: true - - post bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) - - expect(response.body).to include(active_player.name) - expect(response.body).not_to include(retired_player.name) - end - - it 'POST /admin/player/bulk_destroy should destroy records which are authorized to' do - active_player = FactoryBot.create :player, retired: false - retired_player = FactoryBot.create :player, retired: true - - delete bulk_delete_path(model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) - expect(@player_model.get(active_player.id)).to be_nil - expect(@player_model.get(retired_player.id)).not_to be_nil - end - end - - describe 'with exception role' do - it 'GET /admin/player/bulk_delete should render records which are authorized to' do - @user.update_attributes(roles: [:admin, :test_exception]) - active_player = FactoryBot.create :player, retired: false - retired_player = FactoryBot.create :player, retired: true - - post bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) - - expect(response.body).to include(active_player.name) - expect(response.body).not_to include(retired_player.name) - end - - it 'POST /admin/player/bulk_destroy should destroy records which are authorized to' do - @user.update_attributes(roles: [:admin, :test_exception]) - active_player = FactoryBot.create :player, retired: false - retired_player = FactoryBot.create :player, retired: true - - delete bulk_delete_path(model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) - expect(@player_model.get(active_player.id)).to be_nil - expect(@player_model.get(retired_player.id)).not_to be_nil - end - end - - describe 'with a custom admin ability' do - before do - RailsAdmin.config { |c| c.authorize_with :cancan, AdminAbility } - @user = FactoryBot.create :user - login_as @user - end - - describe 'with admin role only' do - before do - @user.update_attributes(roles: [:admin]) - end - - it 'GET /admin/team should render successfully' do - visit index_path(model_name: 'team') - expect(page.status_code).to eq(200) - end - - it 'GET /admin/player/new should render successfully' do - visit new_path(model_name: 'player') - expect(page.status_code).to eq(200) - end - - it 'GET /admin/player/1/edit should render successfully' do - @player = FactoryBot.create :player - visit edit_path(model_name: 'player', id: @player.id) - expect(page.status_code).to eq(200) - end - - it 'GET /admin/player/1/edit with retired player should render successfully' do - @player = FactoryBot.create :player, retired: true - visit edit_path(model_name: 'player', id: @player.id) - expect(page.status_code).to eq(200) - end - - it 'GET /admin/player/1/delete should render successfully' do - @player = FactoryBot.create :player - visit delete_path(model_name: 'player', id: @player.id) - expect(page.status_code).to eq(200) - end - - it 'GET /admin/player/1/delete with retired player should render successfully' do - @player = FactoryBot.create :player, retired: true - visit delete_path(model_name: 'player', id: @player.id) - expect(page.status_code).to eq(200) - end - end - end -end if defined?(CanCan) && !defined?(CanCanCan) diff --git a/spec/integration/authorization/cancancan_spec.rb b/spec/integration/authorization/cancancan_spec.rb index cadba0efcd..762e695768 100644 --- a/spec/integration/authorization/cancancan_spec.rb +++ b/spec/integration/authorization/cancancan_spec.rb @@ -363,4 +363,4 @@ def initialize(_) is_expected.to have_content('Dashboard') end end if CanCan::VERSION < '3' -end if defined?(CanCanCan) +end