From fd03eae5621a365419c217ed8b8b61c2299add0b Mon Sep 17 00:00:00 2001 From: Sergei Smagin Date: Fri, 15 Sep 2017 11:06:08 +0300 Subject: [PATCH 01/54] Add active_record_5_adapter --- lib/cancan.rb | 3 +- .../model_adapters/active_record_4_adapter.rb | 2 +- .../model_adapters/active_record_5_adapter.rb | 24 +++++ .../active_record_4_adapter_spec.rb | 10 +- .../active_record_5_adapter_spec.rb | 100 ++++++++++++++++++ .../active_record_adapter_spec.rb | 8 +- 6 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 lib/cancan/model_adapters/active_record_5_adapter.rb create mode 100644 spec/cancan/model_adapters/active_record_5_adapter_spec.rb diff --git a/lib/cancan.rb b/lib/cancan.rb index 92a8ee88..dc06e4ad 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -10,6 +10,7 @@ require 'cancan/model_adapters/default_adapter' if defined? ActiveRecord - require 'cancan/model_adapters/active_record_adapter' + require 'cancan/model_adapters/active_record_adapter' require 'cancan/model_adapters/active_record_4_adapter' + require 'cancan/model_adapters/active_record_5_adapter' end diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index 5a8f2ef9..0869d349 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -3,7 +3,7 @@ module ModelAdapters class ActiveRecord4Adapter < AbstractAdapter include ActiveRecordAdapter def self.for_class?(model_class) - model_class <= ActiveRecord::Base + ActiveRecord::VERSION::MAJOR == 4 && model_class <= ActiveRecord::Base end # TODO: this should be private diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb new file mode 100644 index 00000000..6525e5c1 --- /dev/null +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -0,0 +1,24 @@ +module CanCan + module ModelAdapters + class ActiveRecord5Adapter < ActiveRecord4Adapter + AbstractAdapter.inherited(self) + + def self.for_class?(model_class) + ActiveRecord::VERSION::MAJOR == 5 && model_class <= ActiveRecord::Base + end + + # rails 5 is capable of using strings in enum + # but often people use symbols in rules + def self.matches_condition?(subject, name, value) + return super if Array.wrap(value).all? { |x| x.is_a? Integer } + + attribute = subject.send(name) + if value.is_a?(Enumerable) + value.map(&:to_s).include? attribute + else + attribute == value.to_s + end + end + end + end +end diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index 9016fdc1..0508b607 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -39,7 +39,7 @@ class Child < ActiveRecord::Base .to eq [child2, child1] end - if ActiveRecord::VERSION::MINOR >= 1 + if ActiveRecord::VERSION::MINOR >= 1 || ActiveRecord::VERSION::MAJOR >= 5 it 'allows filters on enums' do ActiveRecord::Schema.define do create_table(:shapes) do |t| @@ -48,7 +48,9 @@ class Child < ActiveRecord::Base end class Shape < ActiveRecord::Base - enum color: %i[red green blue] + unless defined_enums.keys.include? 'color' + enum color: %i[red green blue] + end end red = Shape.create!(color: :red) @@ -86,8 +88,8 @@ class Shape < ActiveRecord::Base end class Disc < ActiveRecord::Base - enum color: %i[red green blue] - enum shape: { triangle: 3, rectangle: 4 } + enum color: %i[red green blue] unless defined_enums.keys.include? 'color' + enum shape: { triangle: 3, rectangle: 4 } unless defined_enums.keys.include? 'shape' end red_triangle = Disc.create!(color: Disc.colors[:red], shape: Disc.shapes[:triangle]) diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb new file mode 100644 index 00000000..de94a23b --- /dev/null +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +if ActiveRecord::VERSION::MAJOR == 5 && defined?(CanCan::ModelAdapters::ActiveRecord5Adapter) + describe CanCan::ModelAdapters::ActiveRecord5Adapter do + context 'with sqlite3' do + before :each do + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:parents) do |t| + t.timestamps null: false + end + + create_table(:children) do |t| + t.timestamps null: false + t.integer :parent_id + end + end + + class Parent < ActiveRecord::Base + has_many :children, -> { order(id: :desc) } + end + + class Child < ActiveRecord::Base + belongs_to :parent + end + + (@ability = double).extend(CanCan::Ability) + end + + it 'allows filters on enums' do + ActiveRecord::Schema.define do + create_table(:shapes) do |t| + t.integer :color, default: 0, null: false + end + end + + class Shape < ActiveRecord::Base + unless defined_enums.keys.include? 'color' + enum color: %i[red green blue] + end + end + + red = Shape.create!(color: :red) + green = Shape.create!(color: :green) + blue = Shape.create!(color: :blue) + + # A condition with a single value. + @ability.can :read, Shape, color: :green + + expect(@ability.cannot?(:read, red)).to be true + expect(@ability.can?(:read, green)).to be true + expect(@ability.cannot?(:read, blue)).to be true + + accessible = Shape.accessible_by(@ability) + expect(accessible).to contain_exactly(green) + + # A condition with multiple values. + @ability.can :update, Shape, color: %i[red blue] + + expect(@ability.can?(:update, red)).to be true + expect(@ability.cannot?(:update, green)).to be true + expect(@ability.can?(:update, blue)).to be true + + accessible = Shape.accessible_by(@ability, :update) + expect(accessible).to contain_exactly(red, blue) + end + + it 'allows dual filter on enums' do + ActiveRecord::Schema.define do + create_table(:discs) do |t| + t.integer :color, default: 0, null: false + t.integer :shape, default: 3, null: false + end + end + + class Disc < ActiveRecord::Base + enum color: %i[red green blue] unless defined_enums.keys.include? 'color' + enum shape: { triangle: 3, rectangle: 4 } unless defined_enums.keys.include? 'shape' + end + + red_triangle = Disc.create!(color: :red, shape: :triangle) + green_triangle = Disc.create!(color: :green, shape: :triangle) + green_rectangle = Disc.create!(color: :green, shape: :rectangle) + blue_rectangle = Disc.create!(color: :blue, shape: :rectangle) + + # A condition with a dual filter. + @ability.can :read, Disc, color: :green, shape: :rectangle + + expect(@ability.cannot?(:read, red_triangle)).to be true + expect(@ability.cannot?(:read, green_triangle)).to be true + expect(@ability.can?(:read, green_rectangle)).to be true + expect(@ability.cannot?(:read, blue_rectangle)).to be true + + accessible = Disc.accessible_by(@ability) + expect(accessible).to contain_exactly(green_rectangle) + end + end + end +end diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 17417b57..af4ccd66 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -81,7 +81,13 @@ class User < ActiveRecord::Base it 'is for only active record classes' do if ActiveRecord.respond_to?(:version) && - ActiveRecord.version > Gem::Version.new('4') + ActiveRecord.version > Gem::Version.new('5') + expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to_not be_for_class(Object) + expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to be_for_class(Article) + expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) + .to eq(CanCan::ModelAdapters::ActiveRecord5Adapter) + elsif ActiveRecord.respond_to?(:version) && + ActiveRecord.version > Gem::Version.new('4') expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to_not be_for_class(Object) expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to be_for_class(Article) expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) From fa370edb753ca64255705712d6c97145fe2a77fd Mon Sep 17 00:00:00 2001 From: camelmasa Date: Thu, 23 Nov 2017 13:12:15 +0900 Subject: [PATCH 02/54] Test custom find_by logic --- spec/cancan/controller_resource_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/cancan/controller_resource_spec.rb b/spec/cancan/controller_resource_spec.rb index 30000c3e..aa897059 100644 --- a/spec/cancan/controller_resource_spec.rb +++ b/spec/cancan/controller_resource_spec.rb @@ -491,7 +491,7 @@ class Model < ::Model; end it 'loads resource using custom find_by attribute' do model = Model.new - allow(Model).to receive(:name).with('foo') { model } + allow(Model).to receive(:find_by).with(name: 'foo') { model } params.merge!(action: 'show', id: 'foo') resource = CanCan::ControllerResource.new(controller, find_by: :name) From 0e4fb03173298dd1299c006322151772c166a183 Mon Sep 17 00:00:00 2001 From: Sergei Smagin Date: Sun, 12 Aug 2018 19:06:05 +0200 Subject: [PATCH 03/54] Make work with string enums consistent; add specs for string enums --- Appraisals | 8 ++-- gemfiles/activerecord_5.2.1.gemfile | 19 +++++++++ .../model_adapters/active_record_5_adapter.rb | 12 +++--- .../active_record_5_adapter_spec.rb | 42 +++++++++++++++++++ 4 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 gemfiles/activerecord_5.2.1.gemfile diff --git a/Appraisals b/Appraisals index 383d04c2..b332afe0 100644 --- a/Appraisals +++ b/Appraisals @@ -47,10 +47,10 @@ appraise 'activerecord_5.1.0' do end end -appraise 'activerecord_5.2.0' do - gem 'activerecord', '~> 5.2.0', require: 'active_record' - gem 'activesupport', '~> 5.2.0', require: 'active_support/all' - gem 'actionpack', '~> 5.2.0', require: 'action_pack' +appraise 'activerecord_5.2.1' do + gem 'activerecord', '~> 5.2.1', require: 'active_record' + gem 'activesupport', '~> 5.2.1', require: 'active_support/all' + gem 'actionpack', '~> 5.2.1', require: 'action_pack' gemfile.platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' diff --git a/gemfiles/activerecord_5.2.1.gemfile b/gemfiles/activerecord_5.2.1.gemfile new file mode 100644 index 00000000..432586ea --- /dev/null +++ b/gemfiles/activerecord_5.2.1.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activerecord", "~> 5.2.1", require: "active_record" +gem "activesupport", "~> 5.2.1", require: "active_support/all" +gem "actionpack", "~> 5.2.1", require: "action_pack" + +platforms :jruby do + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" +end + +platforms :ruby, :mswin, :mingw do + gem "sqlite3" + gem "pg", "~> 0.21" +end + +gemspec path: "../" diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index e9cade17..e6624b42 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -13,10 +13,11 @@ def self.matches_condition?(subject, name, value) return super if Array.wrap(value).all? { |x| x.is_a? Integer } attribute = subject.send(name) + raw_attribute = subject.class.send(name.to_s.pluralize)[attribute] if value.is_a?(Enumerable) - value.map(&:to_s).include? attribute + !(value.map(&:to_s) & [attribute, raw_attribute]).empty? else - attribute == value.to_s + attribute == value.to_s || raw_attribute == value.to_s end end @@ -39,9 +40,10 @@ def sanitize_sql_activerecord5(conditions) conditions.stringify_keys! - predicate_builder.build_from_hash(conditions).map do |b| - visit_nodes(b) - end.join(' AND ') + predicate_builder + .build_from_hash(conditions) + .map { |b| visit_nodes(b) } + .join(' AND ') end def visit_nodes(b) diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index de94a23b..383db8cb 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -66,6 +66,48 @@ class Shape < ActiveRecord::Base expect(accessible).to contain_exactly(red, blue) end + it 'allows strings as enum values' do + ActiveRecord::Schema.define do + create_table(:things) do |t| + t.string :size, default: 'big', null: false + end + end + + class Thing < ActiveRecord::Base + enum size: { big: 'big', medium: 'average', small: 'small' } + end + + big = Thing.create!(size: :big) + medium = Thing.create!(size: :medium) + small = Thing.create!(size: :small) + + # A condition with a single value. + @ability.can :read, Thing, size: :medium + + expect(@ability.cannot?(:read, big)).to be true + expect(@ability.can?(:read, medium)).to be true + expect(@ability.cannot?(:read, small)).to be true + + accessible = Thing.accessible_by(@ability) + expect(accessible).to contain_exactly(medium) + + @ability.cannot :read, Thing, size: 'average' # should undo previous rule + expect(@ability.can?(:read, medium)).to be false + + accessible = Thing.accessible_by(@ability) + expect(accessible).to be_empty + + # A condition with multiple values. + @ability.can :update, Thing, size: %i[big small] + + expect(@ability.can?(:update, big)).to be true + expect(@ability.cannot?(:update, medium)).to be true + expect(@ability.can?(:update, small)).to be true + + accessible = Thing.accessible_by(@ability, :update) + expect(accessible).to contain_exactly(big, small) + end + it 'allows dual filter on enums' do ActiveRecord::Schema.define do create_table(:discs) do |t| From 0fba7797b10bec747d08c880d3211256a24ae43e Mon Sep 17 00:00:00 2001 From: Sergei Smagin Date: Mon, 13 Aug 2018 09:06:19 +0200 Subject: [PATCH 04/54] Fix travis --- lib/cancan/model_adapters/active_record_5_adapter.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index e6624b42..c7856460 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -14,11 +14,7 @@ def self.matches_condition?(subject, name, value) attribute = subject.send(name) raw_attribute = subject.class.send(name.to_s.pluralize)[attribute] - if value.is_a?(Enumerable) - !(value.map(&:to_s) & [attribute, raw_attribute]).empty? - else - attribute == value.to_s || raw_attribute == value.to_s - end + !(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty? end private From c2d46a0e6e496be0958fe5ca4251adfe7ea7baa3 Mon Sep 17 00:00:00 2001 From: Sergei Smagin Date: Mon, 13 Aug 2018 09:17:19 +0200 Subject: [PATCH 05/54] Remove extra blank line --- lib/cancan/controller_additions.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index 8132c181..78e813aa 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -384,7 +384,6 @@ def cannot?(*args) end end - ActiveSupport.on_load(:action_controller) do include CanCan::ControllerAdditions end From 4d1f0d49bbe013a013efd95bbc50cfbd4ff2f697 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 16 Sep 2018 17:11:08 +0200 Subject: [PATCH 06/54] Restore 3.0.0 changes --- CHANGELOG.md | 6 ++++++ lib/cancan/exceptions.rb | 3 +++ lib/cancan/model_adapters/active_record_5_adapter.rb | 6 +----- lib/cancan/rule.rb | 11 ++++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5070c3df..e224b6c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +* [#489](https://github.com/CanCanCommunity/cancancan/pull/489/files): Drop support for actions without a subject ([@andrew-aladev][]) +* Allow to add attribute-level rules ([@phaedryx][]) +* [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed eager loading of associations for ActiveRecord 5 ([@kaspernj][]) + ## 2.3.0 (Sep 16th, 2018) * [#528](https://github.com/CanCanCommunity/cancancan/issues/528): Compress irrelevant rules before generating a query to optimize performances. ([@coorasse][]) diff --git a/lib/cancan/exceptions.rb b/lib/cancan/exceptions.rb index 78cdaecf..025c7ba0 100644 --- a/lib/cancan/exceptions.rb +++ b/lib/cancan/exceptions.rb @@ -11,6 +11,9 @@ class ImplementationRemoved < Error; end # Raised when using check_authorization without calling authorized! class AuthorizationNotPerformed < Error; end + # Raised when a rule is created with both a block and a hash of conditions + class BlockAndConditionsError < Error; end + # Raised when using a wrong association name class WrongAssociationName < Error; end diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index 94558921..ac2f23b2 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -22,13 +22,9 @@ def self.matches_condition?(subject, name, value) private - # As of rails 4, `includes()` no longer causes active record to - # look inside the where clause to decide to outer join tables - # you're using in the where. Instead, `references()` is required - # in addition to `includes()` to force the outer join. def build_relation(*where_conditions) relation = @model_class.where(*where_conditions) - relation = relation.includes(joins).references(joins) if joins.present? + relation = relation.left_joins(joins).distinct if joins.present? relation end diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 4ab345c2..ba151156 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -13,10 +13,9 @@ class Rule # :nodoc: # and subject respectively (such as :read, @project). The third argument is a hash # of conditions and the last one is the block passed to the "can" call. def initialize(base_behavior, action, subject, conditions, block) - both_block_and_hash_error = 'You are not able to supply a block with a hash of conditions in '\ - "#{action} #{subject} ability. Use either one." - raise Error, both_block_and_hash_error if conditions.is_a?(Hash) && block + condition_and_block_check(conditions, block, action, subject) @match_all = action.nil? && subject.nil? + raise Error, "Subject is required for #{action}" if action && subject.nil? @base_behavior = base_behavior @actions = Array(action) @subjects = Array(subject) @@ -92,5 +91,11 @@ def matches_subject_class?(subject) (subject.is_a?(Module) && subject.ancestors.include?(sub))) end end + + def condition_and_block_check(conditions, block, action, subject) + return unless conditions.is_a?(Hash) && block + raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\ + "Check \":#{action} #{subject}\" ability." + end end end From a2e922a4174b52e41ab20e197b6e63e451220213 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 16 Sep 2018 17:34:55 +0200 Subject: [PATCH 07/54] Add attribute-level rules --- CHANGELOG.md | 9 +- lib/cancan.rb | 1 + lib/cancan/ability.rb | 27 ++-- lib/cancan/ability/rules.rb | 15 ++- .../ability/strong_parameter_support.rb | 39 ++++++ lib/cancan/conditions_matcher.rb | 22 +-- lib/cancan/exceptions.rb | 3 + lib/cancan/parameter_validators.rb | 7 + lib/cancan/rule.rb | 29 +++- spec/cancan/ability_spec.rb | 126 ++++++++++++++++-- .../active_record_adapter_spec.rb | 9 ++ spec/cancan/rule_spec.rb | 15 ++- 12 files changed, 255 insertions(+), 47 deletions(-) create mode 100644 lib/cancan/ability/strong_parameter_support.rb create mode 100644 lib/cancan/parameter_validators.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e224b6c7..e90c46d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ ## Unreleased -* [#489](https://github.com/CanCanCommunity/cancancan/pull/489/files): Drop support for actions without a subject ([@andrew-aladev][]) -* Allow to add attribute-level rules ([@phaedryx][]) -* [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed eager loading of associations for ActiveRecord 5 ([@kaspernj][]) +* [#489](https://github.com/CanCanCommunity/cancancan/pull/489): Drop support for actions without a subject. ([@andrew-aladev][]) +* [#474](https://github.com/CanCanCommunity/cancancan/pull/474): Allow to add attribute-level rules. ([@phaedryx][]) +* [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed eager loading of associations for ActiveRecord 5. ([@kaspernj][]) ## 2.3.0 (Sep 16th, 2018) @@ -621,3 +621,6 @@ [@oliverklee]: https://github.com/oliverklee [@gingray]: https://github.com/gingray [@timraymond]: https://github.com/timraymond +[@andrew-aladev]: https://github.com/andrew-aladev +[@phaedryx]: https://github.com/phaedryx +[@kaspernj]: https://github.com/kaspernj diff --git a/lib/cancan.rb b/lib/cancan.rb index a2ff1818..86d419c4 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -1,4 +1,5 @@ require 'cancan/version' +require 'cancan/parameter_validators' require 'cancan/ability' require 'cancan/rule' require 'cancan/controller_resource' diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 20ab6297..2ec7dbc2 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -1,5 +1,7 @@ require_relative 'ability/rules.rb' require_relative 'ability/actions.rb' +require_relative 'ability/strong_parameter_support' + module CanCan # This module is designed to be included into an Ability class. This will # provide the "can" methods for defining and checking abilities. @@ -19,6 +21,7 @@ module CanCan module Ability include CanCan::Ability::Rules include CanCan::Ability::Actions + include StrongParameterSupport # Check if the user has permission to perform a given action on an object. # @@ -64,10 +67,10 @@ module Ability # end # # Also see the RSpec Matchers to aid in testing. - def can?(action, subject, *extra_args) + def can?(action, subject, attribute = nil, *extra_args) match = extract_subjects(subject).lazy.map do |a_subject| relevant_rules_for_match(action, a_subject).detect do |rule| - rule.matches_conditions?(action, a_subject, extra_args) + rule.matches_conditions?(action, a_subject, attribute, *extra_args) && rule.matches_attributes?(attribute) end end.reject(&:nil?).first match ? match.base_behavior : false @@ -134,8 +137,8 @@ def cannot?(*args) # # check the database and return true/false # end # - def can(action = nil, subject = nil, conditions = nil, &block) - add_rule(Rule.new(true, action, subject, conditions, block)) + def can(action = nil, subject = nil, *attributes_and_conditions, &block) + add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block)) end # Defines an ability which cannot be done. Accepts the same arguments as "can". @@ -150,8 +153,8 @@ def can(action = nil, subject = nil, conditions = nil, &block) # product.invisible? # end # - def cannot(action = nil, subject = nil, conditions = nil, &block) - add_rule(Rule.new(false, action, subject, conditions, block)) + def cannot(action = nil, subject = nil, *attributes_and_conditions, &block) + add_rule(Rule.new(false, action, subject, *attributes_and_conditions, &block)) end # User shouldn't specify targets with names of real actions or it will cause Seg fault @@ -239,10 +242,13 @@ def merge(ability) # # Where can_hash and cannot_hash are formatted thusly: # { - # action: array_of_objects + # action: { subject: [attributes] } # } def permissions - permissions_list = { can: {}, cannot: {} } + permissions_list = { + can: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } }, + cannot: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } } + } rules.each { |rule| extract_rule_in_permissions(permissions_list, rule) } permissions_list end @@ -250,8 +256,9 @@ def permissions def extract_rule_in_permissions(permissions_list, rule) expand_actions(rule.actions).each do |action| container = rule.base_behavior ? :can : :cannot - permissions_list[container][action] ||= [] - permissions_list[container][action] += rule.subjects.map(&:to_s) + rule.subjects.each do |subject| + permissions_list[container][action][subject.to_s] += rule.attributes + end end end diff --git a/lib/cancan/ability/rules.rb b/lib/cancan/ability/rules.rb index 00ab4250..7528665b 100644 --- a/lib/cancan/ability/rules.rb +++ b/lib/cancan/ability/rules.rb @@ -55,17 +55,20 @@ def relevant_rules_for_match(action, subject) next unless rule.only_raw_sql? raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition."\ - " The checking code cannot be determined for #{action.inspect} #{subject.inspect}" + " The checking code cannot be determined for #{action.inspect} #{subject.inspect}" end end def relevant_rules_for_query(action, subject) - relevant_rules(action, subject).each do |rule| - if rule.only_block? - raise Error, "The accessible_by call cannot be used with a block 'can' definition."\ - " The SQL cannot be determined for #{action.inspect} #{subject.inspect}" - end + rules = relevant_rules(action, subject).reject do |rule| + # reject 'cannot' rules with attributes when doing queries + rule.base_behavior == false && rule.attributes.present? + end + if rules.any?(&:only_block?) + raise Error, "The accessible_by call cannot be used with a block 'can' definition."\ + "The SQL cannot be determined for #{action.inspect} #{subject.inspect}" end + rules end # Optimizes the order of the rules, so that rules with the :all subject are evaluated first. diff --git a/lib/cancan/ability/strong_parameter_support.rb b/lib/cancan/ability/strong_parameter_support.rb new file mode 100644 index 00000000..60b14962 --- /dev/null +++ b/lib/cancan/ability/strong_parameter_support.rb @@ -0,0 +1,39 @@ +module CanCan + module Ability + module StrongParameterSupport + # Returns an array of attributes suitable for use with strong parameters + # + # Note: reversing the relevant rules is important. Normal order means that 'cannot' + # rules will come before 'can' rules. However, you can't remove attributes before + # they are added. The 'reverse' is so that attributes will be added before the + # 'cannot' rules remove them. + def permitted_attributes(action, subject) + relevant_rules(action, subject) + .reverse + .select { |rule| rule.matches_conditions? action, subject } + .each_with_object(Set.new) do |rule, set| + attributes = get_attributes(rule, subject) + # add attributes for 'can', remove them for 'cannot' + rule.base_behavior ? set.merge(attributes) : set.subtract(attributes) + end.to_a + end + + private + + def subject_class?(subject) + klass = (subject.is_a?(Hash) ? subject.values.first : subject).class + klass == Class || klass == Module + end + + def get_attributes(rule, subject) + klass = subject_class?(subject) ? subject : subject.class + # empty attributes is an 'all' + if rule.attributes.empty? && klass < ActiveRecord::Base + klass.column_names.map(&:to_sym) - Array(klass.primary_key) + else + rule.attributes + end + end + end + end +end diff --git a/lib/cancan/conditions_matcher.rb b/lib/cancan/conditions_matcher.rb index 9c5fee15..3e7d2e73 100644 --- a/lib/cancan/conditions_matcher.rb +++ b/lib/cancan/conditions_matcher.rb @@ -1,10 +1,11 @@ module CanCan module ConditionsMatcher # Matches the block or conditions hash - def matches_conditions?(action, subject, extra_args) + def matches_conditions?(action, subject, attribute = nil, *extra_args) return call_block_with_all(action, subject, extra_args) if @match_all - return @block.call(subject, *extra_args) if @block && !subject_class?(subject) - matches_non_block_conditions(subject) + return matches_block_conditions(subject, attribute, *extra_args) if @block + return matches_non_block_conditions(subject) unless conditions_empty? + true end private @@ -14,13 +15,16 @@ def subject_class?(subject) klass == Class || klass == Module end + def matches_block_conditions(subject, *extra_args) + return @base_behavior if subject_class?(subject) + @block.call(subject, *extra_args) + end + def matches_non_block_conditions(subject) - if @conditions.is_a?(Hash) - return nested_subject_matches_conditions?(subject) if subject.class == Hash - return matches_conditions_hash?(subject) unless subject_class?(subject) - end + return nested_subject_matches_conditions?(subject) if subject.class == Hash + return matches_conditions_hash?(subject) unless subject_class?(subject) # Don't stop at "cannot" definitions when there are conditions. - conditions_empty? ? true : @base_behavior + @base_behavior end def nested_subject_matches_conditions?(subject_hash) @@ -74,7 +78,7 @@ def hash_condition_match?(attribute, value) end end - def call_block_with_all(action, subject, extra_args) + def call_block_with_all(action, subject, *extra_args) if subject.class == Class @block.call(action, subject, nil, *extra_args) else diff --git a/lib/cancan/exceptions.rb b/lib/cancan/exceptions.rb index 025c7ba0..eaf22007 100644 --- a/lib/cancan/exceptions.rb +++ b/lib/cancan/exceptions.rb @@ -14,6 +14,9 @@ class AuthorizationNotPerformed < Error; end # Raised when a rule is created with both a block and a hash of conditions class BlockAndConditionsError < Error; end + # Raised when an unexpected argument is passed as an attribute + class AttributeArgumentError < Error; end + # Raised when using a wrong association name class WrongAssociationName < Error; end diff --git a/lib/cancan/parameter_validators.rb b/lib/cancan/parameter_validators.rb new file mode 100644 index 00000000..546a4f1d --- /dev/null +++ b/lib/cancan/parameter_validators.rb @@ -0,0 +1,7 @@ +module CanCan + module ParameterValidators + def valid_attribute_param?(attribute) + attribute.nil? || attribute.is_a?(Symbol) || (attribute.is_a?(Array) && attribute.all? { |a| a.is_a?(Symbol) }) + end + end +end diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index ba151156..139662bb 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -5,21 +5,26 @@ module CanCan # helpful methods to determine permission checking and conditions hash generation. class Rule # :nodoc: include ConditionsMatcher - attr_reader :base_behavior, :subjects, :actions, :conditions + include ParameterValidators + attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes attr_writer :expanded_actions # The first argument when initializing is the base_behavior which is a true/false # value. True for "can" and false for "cannot". The next two arguments are the action # and subject respectively (such as :read, @project). The third argument is a hash # of conditions and the last one is the block passed to the "can" call. - def initialize(base_behavior, action, subject, conditions, block) - condition_and_block_check(conditions, block, action, subject) + def initialize(base_behavior, action, subject, *extra_args, &block) + # for backwards compatibility, attributes are an optional parameter. Check if + # attributes were passed or are actually conditions + attributes, extra_args = parse_attributes_from_extra_args(extra_args) + condition_and_block_check(extra_args, block, action, subject) @match_all = action.nil? && subject.nil? raise Error, "Subject is required for #{action}" if action && subject.nil? @base_behavior = base_behavior @actions = Array(action) @subjects = Array(subject) - @conditions = conditions || {} + @attributes = Array(attributes) + @conditions = extra_args || {} @block = block end @@ -35,7 +40,7 @@ def catch_all? [nil, false, [], {}, '', ' '].include? @conditions end - # Matches both the subject and action, not necessarily the conditions + # Matches both the action, subject, and attribute, not necessarily the conditions def relevant?(action, subject) subject = subject.values.first if subject.class == Hash @match_all || (matches_action?(action) && matches_subject?(subject)) @@ -74,6 +79,12 @@ def attributes_from_conditions attributes end + def matches_attributes?(attribute) + return true if @attributes.empty? + return @base_behavior if attribute.nil? + @attributes.include?(attribute.to_sym) + end + private def matches_action?(action) @@ -92,10 +103,16 @@ def matches_subject_class?(subject) end end + def parse_attributes_from_extra_args(args) + attributes = args.shift if valid_attribute_param?(args.first) + extra_args = args.shift + [attributes, extra_args] + end + def condition_and_block_check(conditions, block, action, subject) return unless conditions.is_a?(Hash) && block raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\ - "Check \":#{action} #{subject}\" ability." + "Check \":#{action} #{subject}\" ability." end end end diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 86ec23d3..4fff0c2d 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -182,16 +182,24 @@ it 'lists all permissions' do @ability.can :manage, :all @ability.can :learn, Range + @ability.can :interpret, Symbol, %i[size to_s] @ability.cannot :read, String @ability.cannot :read, Hash @ability.cannot :preview, Array - expected_list = { can: { manage: ['all'], - learn: ['Range'] }, - cannot: { read: %w[String Hash], - index: %w[String Hash], - show: %w[String Hash], - preview: ['Array'] } } + expected_list = { + can: { + manage: { 'all' => [] }, + learn: { 'Range' => [] }, + interpret: { 'Symbol' => %i[size to_s] } + }, + cannot: { + read: { 'String' => [], 'Hash' => [] }, + index: { 'String' => [], 'Hash' => [] }, + show: { 'String' => [], 'Hash' => [] }, + preview: { 'Array' => [] } + } + } expect(@ability.permissions).to eq(expected_list) end @@ -356,6 +364,13 @@ expect(@ability.can?(:read, Range)).to be(true) end + it 'does not stop at cannot with block when comparing class' do + @ability.can :read, Integer + @ability.cannot(:read, Integer) { |int| int > 5 } + expect(@ability.can?(:read, 123)).to be(false) + expect(@ability.can?(:read, Integer)).to be(true) + end + it 'stops at cannot definition when no hash is present' do @ability.can :read, :all @ability.cannot :read, Range @@ -416,7 +431,7 @@ class Container < Hash expect(@ability.can?(:read, Container.new)).to be(true) end - it "has initial attributes based on hash conditions of 'new' action" do + it "has initial values based on hash conditions of 'new' action" do @ability.can :manage, Range, foo: 'foo', hash: { skip: 'hashes' } @ability.can :create, Range, bar: 123, array: %w[skip arrays] @ability.can :new, Range, baz: 'baz', range: 1..3 @@ -504,9 +519,100 @@ class Container < Hash @ability.can :read, Array, published: true do false end - end.to raise_error(CanCan::Error, - 'You are not able to supply a block with a hash of conditions in read Array ability. '\ - 'Use either one.') + end.to raise_error(CanCan::BlockAndConditionsError) + end + + it 'allows attribute-level rules' do + @ability.can :read, Array, :to_s + expect(@ability.can?(:read, Array, :to_s)).to be(true) + expect(@ability.can?(:read, Array, :size)).to be(false) + expect(@ability.can?(:read, Array)).to be(true) + end + + it 'allows an array of attributes in rules' do + @ability.can :read, [Array, String], %i[to_s size] + expect(@ability.can?(:read, String, :size)).to be(true) + expect(@ability.can?(:read, Array, :to_s)).to be(true) + end + + it 'allows cannot of rules with attributes' do + @ability.can :read, Array + @ability.cannot :read, Array, :to_s + expect(@ability.can?(:read, Array, :to_s)).to be(false) + expect(@ability.can?(:read, Array)).to be(true) + expect(@ability.can?(:read, Array, :size)).to be(true) + end + + it 'has precedence with attribute-level rules' do + @ability.cannot :read, Array + @ability.can :read, Array, :to_s + expect(@ability.can?(:read, Array, :to_s)).to be(true) + expect(@ability.can?(:read, Array, :size)).to be(false) + expect(@ability.can?(:read, Array)).to be(true) + end + + it 'allows permission on all attributes when none are given' do + @ability.can :update, Object + expect(@ability.can?(:update, Object, :password)).to be(true) + end + + it 'allows strings when checking attributes' do + @ability.can :update, Object, :name + expect(@ability.can?(:update, Object, 'name')).to be(true) + end + + it 'passes attribute to block; nil if no attribute given' do + @ability.can :update, Range do |_range, attribute| + attribute == :name + end + expect(@ability.can?(:update, 1..3, :name)).to be(true) + expect(@ability.can?(:update, 2..4)).to be(false) + end + + it 'combines attribute checks with conditions hash' do + @ability.can :update, Range, begin: 1 + @ability.can :update, Range, :name, begin: 2 + expect(@ability.can?(:update, 1..3, :notname)).to be(true) + expect(@ability.can?(:update, 2..4, :notname)).to be(false) + expect(@ability.can?(:update, 2..4, :name)).to be(true) + expect(@ability.can?(:update, 3..5, :name)).to be(false) + expect(@ability.can?(:update, Range)).to be(true) + expect(@ability.can?(:update, Range, :name)).to be(true) + end + + it 'returns an array of permitted attributes for a given action and subject' do + user_class = Class.new(ActiveRecord::Base) + allow(user_class).to receive(:column_names).and_return(%w[first_name last_name]) + allow(user_class).to receive(:primary_key).and_return('id') + @ability.can :read, user_class + @ability.can :read, Array, :special + @ability.can :action, :subject, :attribute + expect(@ability.permitted_attributes(:read, user_class)).to eq(%i[first_name last_name]) + expect(@ability.permitted_attributes(:read, Array)).to eq([:special]) + expect(@ability.permitted_attributes(:action, :subject)).to eq([:attribute]) + end + + it 'returns permitted attributes when used with blocks' do + user_class = Struct.new(:first_name, :last_name) + @ability.can :read, user_class, %i[first_name last_name] + @ability.cannot(:read, user_class, :first_name) { |u| u.last_name == 'Smith' } + expect(@ability.permitted_attributes(:read, user_class.new('John', 'Jones'))).to eq(%i[first_name last_name]) + expect(@ability.permitted_attributes(:read, user_class.new('John', 'Smith'))).to eq(%i[last_name]) + end + + it 'returns permitted attributes when using conditions' do + @ability.can :read, Range, %i[nil? to_s class] + @ability.cannot :read, Range, %i[nil? to_s], begin: 2 + @ability.can :read, Range, :to_s, end: 4 + expect(@ability.permitted_attributes(:read, 1..3)).to eq(%i[nil? to_s class]) + expect(@ability.permitted_attributes(:read, 2..5)).to eq([:class]) + expect(@ability.permitted_attributes(:read, 2..4)).to eq(%i[class to_s]) + end + + it 'respects inheritance when checking permitted attributes' do + @ability.can :read, Integer, %i[nil? to_s class] + @ability.cannot :read, Numeric, %i[nil? class] + expect(@ability.permitted_attributes(:read, Integer)).to eq([:to_s]) end describe 'unauthorized message' do diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 84b7b66b..832be450 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -360,6 +360,15 @@ class User < ActiveRecord::Base expect { @ability.can? :read, Article }.not_to raise_error end + it 'should ignore cannot rules with attributes when querying' do + user = User.create! + article = Article.create!(user: user) + ability = Ability.new(user) + ability.can :read, Article + ability.cannot :read, Article, :secret + expect(Article.accessible_by(ability)).to eq([article]) + end + context 'with namespaced models' do before :each do ActiveRecord::Schema.define do diff --git a/spec/cancan/rule_spec.rb b/spec/cancan/rule_spec.rb index 388ae24e..7d0966df 100644 --- a/spec/cancan/rule_spec.rb +++ b/spec/cancan/rule_spec.rb @@ -2,10 +2,10 @@ require 'ostruct' # for OpenStruct below # Most of Rule functionality is tested in Ability specs -describe CanCan::Rule do +RSpec.describe CanCan::Rule do before(:each) do @conditions = {} - @rule = CanCan::Rule.new(true, :read, Integer, @conditions, nil) + @rule = CanCan::Rule.new(true, :read, Integer, @conditions) end it 'returns no association joins if none exist' do @@ -34,7 +34,7 @@ end it 'returns no association joins if conditions is nil' do - rule = CanCan::Rule.new(true, :read, Integer, nil, nil) + rule = CanCan::Rule.new(true, :read, Integer, nil) expect(rule.associations_hash).to eq({}) end @@ -49,4 +49,13 @@ @conditions = {} expect(@rule).to_not be_unmergeable end + + it 'allows nil in attribute spot for edge cases' do + rule1 = CanCan::Rule.new(true, :action, :subject, nil, :var) + expect(rule1.attributes).to eq [] + expect(rule1.conditions).to eq :var + rule2 = CanCan::Rule.new(true, :action, :subject, nil, %i[foo bar]) + expect(rule2.attributes).to eq [] + expect(rule2.conditions).to eq %i[foo bar] + end end From e916892606794377214a8561288b4ba5c1ea47cd Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Wed, 26 Sep 2018 12:37:37 +0200 Subject: [PATCH 08/54] Add both checks in test --- spec/cancan/controller_resource_spec.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/spec/cancan/controller_resource_spec.rb b/spec/cancan/controller_resource_spec.rb index aa897059..c6b2df10 100644 --- a/spec/cancan/controller_resource_spec.rb +++ b/spec/cancan/controller_resource_spec.rb @@ -489,7 +489,17 @@ class Model < ::Model; end expect(resource.send(:id_param)).to be_nil end - it 'loads resource using custom find_by attribute' do + it 'loads resource using ActiveRecord find_by method' do + model = Model.new + allow(Model).to receive(:name).with('foo') { model } + + params.merge!(action: 'show', id: 'foo') + resource = CanCan::ControllerResource.new(controller, find_by: :name) + resource.load_resource + expect(controller.instance_variable_get(:@model)).to eq(model) + end + + it 'loads resource using custom find_by method' do model = Model.new allow(Model).to receive(:find_by).with(name: 'foo') { model } From dcb15cf931b8733a51ef5baec3f0175919365307 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Wed, 3 Oct 2018 14:26:08 +0200 Subject: [PATCH 09/54] Improve tests --- .../active_record_5_adapter_spec.rb | 197 ++++++++++-------- 1 file changed, 107 insertions(+), 90 deletions(-) diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index 383db8cb..77629685 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -6,136 +6,153 @@ before :each do ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do - create_table(:parents) do |t| - t.timestamps null: false + create_table(:shapes) do |t| + t.integer :color, default: 0, null: false end - create_table(:children) do |t| - t.timestamps null: false - t.integer :parent_id + create_table(:things) do |t| + t.string :size, default: 'big', null: false end - end - class Parent < ActiveRecord::Base - has_many :children, -> { order(id: :desc) } + create_table(:discs) do |t| + t.integer :color, default: 0, null: false + t.integer :shape, default: 3, null: false + end end - class Child < ActiveRecord::Base - belongs_to :parent + unless defined?(Thing) + class Thing < ActiveRecord::Base + enum size: { big: 'big', medium: 'average', small: 'small' } + end end - (@ability = double).extend(CanCan::Ability) - end - - it 'allows filters on enums' do - ActiveRecord::Schema.define do - create_table(:shapes) do |t| - t.integer :color, default: 0, null: false + unless defined?(Shape) + class Shape < ActiveRecord::Base + enum color: %i[red green blue] end end - class Shape < ActiveRecord::Base - unless defined_enums.keys.include? 'color' + unless defined?(Disc) + class Disc < ActiveRecord::Base enum color: %i[red green blue] + enum shape: { triangle: 3, rectangle: 4 } end end + end - red = Shape.create!(color: :red) - green = Shape.create!(color: :green) - blue = Shape.create!(color: :blue) - - # A condition with a single value. - @ability.can :read, Shape, color: :green - - expect(@ability.cannot?(:read, red)).to be true - expect(@ability.can?(:read, green)).to be true - expect(@ability.cannot?(:read, blue)).to be true - - accessible = Shape.accessible_by(@ability) - expect(accessible).to contain_exactly(green) + subject(:ability) { Ability.new(nil) } - # A condition with multiple values. - @ability.can :update, Shape, color: %i[red blue] + context 'when enums use integers as values' do + let(:red) { Shape.create!(color: :red) } + let(:green) { Shape.create!(color: :green) } + let(:blue) { Shape.create!(color: :blue) } - expect(@ability.can?(:update, red)).to be true - expect(@ability.cannot?(:update, green)).to be true - expect(@ability.can?(:update, blue)).to be true + context 'when the condition contains a single value' do + before do + ability.can :read, Shape, color: :green + end - accessible = Shape.accessible_by(@ability, :update) - expect(accessible).to contain_exactly(red, blue) - end + it 'can check ability on single models' do + is_expected.not_to be_able_to(:read, red) + is_expected.to be_able_to(:read, green) + is_expected.not_to be_able_to(:read, blue) + end - it 'allows strings as enum values' do - ActiveRecord::Schema.define do - create_table(:things) do |t| - t.string :size, default: 'big', null: false + it 'can use accessible_by helper' do + accessible = Shape.accessible_by(ability) + expect(accessible).to contain_exactly(green) end end - class Thing < ActiveRecord::Base - enum size: { big: 'big', medium: 'average', small: 'small' } - end + context 'when the condition contains multiple values' do + before do + ability.can :update, Shape, color: %i[red blue] + end - big = Thing.create!(size: :big) - medium = Thing.create!(size: :medium) - small = Thing.create!(size: :small) + it 'can check ability on single models' do + is_expected.to be_able_to(:update, red) + is_expected.not_to be_able_to(:update, green) + is_expected.to be_able_to(:update, blue) + end - # A condition with a single value. - @ability.can :read, Thing, size: :medium + it 'can use accessible_by helper' do + accessible = Shape.accessible_by(ability, :update) + expect(accessible).to contain_exactly(red, blue) + end + end + end - expect(@ability.cannot?(:read, big)).to be true - expect(@ability.can?(:read, medium)).to be true - expect(@ability.cannot?(:read, small)).to be true + context 'when enums use strings as values' do + let(:big) { Thing.create!(size: :big) } + let(:medium) { Thing.create!(size: :medium) } + let(:small) { Thing.create!(size: :small) } - accessible = Thing.accessible_by(@ability) - expect(accessible).to contain_exactly(medium) + context 'when the condition contains a single value' do + before do + ability.can :read, Thing, size: :medium + end - @ability.cannot :read, Thing, size: 'average' # should undo previous rule - expect(@ability.can?(:read, medium)).to be false + it 'can check ability on single models' do + is_expected.not_to be_able_to(:read, big) + is_expected.to be_able_to(:read, medium) + is_expected.not_to be_able_to(:read, small) + end - accessible = Thing.accessible_by(@ability) - expect(accessible).to be_empty + it 'can use accessible_by helper' do + expect(Thing.accessible_by(ability)).to contain_exactly(medium) + end - # A condition with multiple values. - @ability.can :update, Thing, size: %i[big small] + context 'when a rule is overriden' do + before do + ability.cannot :read, Thing, size: 'average' + end - expect(@ability.can?(:update, big)).to be true - expect(@ability.cannot?(:update, medium)).to be true - expect(@ability.can?(:update, small)).to be true + it 'is recognised correctly' do + is_expected.not_to be_able_to(:read, medium) + expect(Thing.accessible_by(ability)).to be_empty + end + end + end - accessible = Thing.accessible_by(@ability, :update) - expect(accessible).to contain_exactly(big, small) - end + context 'when the condition contains multiple values' do + before do + ability.can :update, Thing, size: %i[big small] + end - it 'allows dual filter on enums' do - ActiveRecord::Schema.define do - create_table(:discs) do |t| - t.integer :color, default: 0, null: false - t.integer :shape, default: 3, null: false + it 'can check ability on single models' do + is_expected.to be_able_to(:update, big) + is_expected.not_to be_able_to(:update, medium) + is_expected.to be_able_to(:update, small) end - end - class Disc < ActiveRecord::Base - enum color: %i[red green blue] unless defined_enums.keys.include? 'color' - enum shape: { triangle: 3, rectangle: 4 } unless defined_enums.keys.include? 'shape' + it 'can use accessible_by helper' do + expect(Thing.accessible_by(ability, :update)).to contain_exactly(big, small) + end end + end - red_triangle = Disc.create!(color: :red, shape: :triangle) - green_triangle = Disc.create!(color: :green, shape: :triangle) - green_rectangle = Disc.create!(color: :green, shape: :rectangle) - blue_rectangle = Disc.create!(color: :blue, shape: :rectangle) + context 'when multiple enums are present' do + let(:red_triangle) { Disc.create!(color: :red, shape: :triangle) } + let(:green_triangle) { Disc.create!(color: :green, shape: :triangle) } + let(:green_rectangle) { Disc.create!(color: :green, shape: :rectangle) } + let(:blue_rectangle) { Disc.create!(color: :blue, shape: :rectangle) } - # A condition with a dual filter. - @ability.can :read, Disc, color: :green, shape: :rectangle + before do + ability.can :read, Disc, color: :green, shape: :rectangle + end - expect(@ability.cannot?(:read, red_triangle)).to be true - expect(@ability.cannot?(:read, green_triangle)).to be true - expect(@ability.can?(:read, green_rectangle)).to be true - expect(@ability.cannot?(:read, blue_rectangle)).to be true + it 'can check ability on single models' do + is_expected.not_to be_able_to(:read, red_triangle) + is_expected.not_to be_able_to(:read, green_triangle) + is_expected.to be_able_to(:read, green_rectangle) + is_expected.not_to be_able_to(:read, blue_rectangle) + end - accessible = Disc.accessible_by(@ability) - expect(accessible).to contain_exactly(green_rectangle) + it 'can use accessible_by helper' do + expect(Disc.accessible_by(ability)).to contain_exactly(green_rectangle) + end end end end From 7dc4961b3c97065870090ee0080f5446c238f75f Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Wed, 3 Oct 2018 14:30:18 +0200 Subject: [PATCH 10/54] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5070c3df..8db0f9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +* [#444](https://github.com/CanCanCommunity/cancancan/issues/444): Allow to use symbols when defining conditions over enums. ([@s-mage][]) + ## 2.3.0 (Sep 16th, 2018) * [#528](https://github.com/CanCanCommunity/cancancan/issues/528): Compress irrelevant rules before generating a query to optimize performances. ([@coorasse][]) @@ -615,3 +619,4 @@ [@oliverklee]: https://github.com/oliverklee [@gingray]: https://github.com/gingray [@timraymond]: https://github.com/timraymond +[@s-mage]: https://github.com/s-mage From 4dcd607b6f62f2b20d6a6c308a2702bbc896e0c3 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Wed, 3 Oct 2018 15:05:02 +0200 Subject: [PATCH 11/54] Update rubocop --- .rubocop.yml | 3 +-- cancancan.gemspec | 8 +++----- gemfiles/activerecord_4.2.gemfile | 20 +++++++++---------- gemfiles/activerecord_5.0.2.gemfile | 18 ++++++++--------- gemfiles/activerecord_5.0.gemfile | 19 +++++++++--------- gemfiles/activerecord_5.1.0.gemfile | 18 ++++++++--------- gemfiles/activerecord_5.2.0.gemfile | 18 ++++++++--------- gemfiles/activerecord_5.2.1.gemfile | 18 ++++++++--------- lib/cancan/ability.rb | 5 +---- lib/cancan/ability/rules.rb | 3 +++ lib/cancan/conditions_matcher.rb | 4 +++- lib/cancan/controller_additions.rb | 1 + lib/cancan/controller_resource.rb | 4 ++++ lib/cancan/controller_resource_loader.rb | 2 ++ lib/cancan/matchers.rb | 1 + .../model_adapters/active_record_5_adapter.rb | 6 +++--- .../model_adapters/active_record_adapter.rb | 1 + .../active_record_adapter/joins.rb | 2 +- .../model_adapters/conditions_extractor.rb | 6 +++--- lib/cancan/rule.rb | 1 + lib/cancan/rules_compressor.rb | 1 + .../cancan/ability/ability_generator.rb | 2 +- .../active_record_4_adapter_spec.rb | 8 +++----- spec/spec_helper.rb | 6 ++---- 24 files changed, 90 insertions(+), 85 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 73e6dff1..cd03d3dc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -27,7 +27,7 @@ Metrics/BlockLength: # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ # NameWhitelist: is_a? -Style/PredicateName: +Naming/PredicateName: Exclude: - 'spec/**/*' - 'lib/cancan/ability.rb' @@ -36,7 +36,6 @@ Style/PredicateName: Lint/AmbiguousBlockAssociation: Enabled: false - AllCops: TargetRubyVersion: 2.2.0 Exclude: diff --git a/cancancan.gemspec b/cancancan.gemspec index a7034fa1..8c9abd2f 100644 --- a/cancancan.gemspec +++ b/cancancan.gemspec @@ -1,6 +1,4 @@ -# coding: utf-8 - -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'cancan/version' @@ -20,9 +18,9 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.2.0' + s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0' s.add_development_dependency 'bundler', '~> 1.3' - s.add_development_dependency 'rubocop', '~> 0.48.1' s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1' s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0' - s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0' + s.add_development_dependency 'rubocop', '~> 0.59.2' end diff --git a/gemfiles/activerecord_4.2.gemfile b/gemfiles/activerecord_4.2.gemfile index 9d7a4b7f..feca9d34 100644 --- a/gemfiles/activerecord_4.2.gemfile +++ b/gemfiles/activerecord_4.2.gemfile @@ -1,20 +1,20 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "activerecord", "~> 4.2.0", require: "active_record" -gem "activesupport", "~> 4.2.0", require: "active_support/all" -gem "actionpack", "~> 4.2.0", require: "action_pack" -gem "nokogiri", "~> 1.6.8", require: "nokogiri" +gem 'actionpack', '~> 4.2.0', require: 'action_pack' +gem 'activerecord', '~> 4.2.0', require: 'active_record' +gem 'activesupport', '~> 4.2.0', require: 'active_support/all' +gem 'nokogiri', '~> 1.6.8', require: 'nokogiri' platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.24" - gem "jdbc-sqlite3" + gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.24' + gem 'jdbc-sqlite3' end platforms :ruby, :mswin, :mingw do - gem "sqlite3" - gem "pg", "~> 0.21" + gem 'pg', '~> 0.21' + gem 'sqlite3' end -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/activerecord_5.0.2.gemfile b/gemfiles/activerecord_5.0.2.gemfile index 40f67ff6..40413f0f 100644 --- a/gemfiles/activerecord_5.0.2.gemfile +++ b/gemfiles/activerecord_5.0.2.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "activerecord", "~> 5.0.2", require: "active_record" -gem "activesupport", "~> 5.0.2", require: "active_support/all" -gem "actionpack", "~> 5.0.2", require: "action_pack" +gem 'actionpack', '~> 5.0.2', require: 'action_pack' +gem 'activerecord', '~> 5.0.2', require: 'active_record' +gem 'activesupport', '~> 5.0.2', require: 'active_support/all' platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter" - gem "jdbc-sqlite3" + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' end platforms :ruby, :mswin, :mingw do - gem "sqlite3" - gem "pg", "~> 0.21" + gem 'pg', '~> 0.21' + gem 'sqlite3' end -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/activerecord_5.0.gemfile b/gemfiles/activerecord_5.0.gemfile index 899b5c1f..e50f52b5 100644 --- a/gemfiles/activerecord_5.0.gemfile +++ b/gemfiles/activerecord_5.0.gemfile @@ -1,20 +1,19 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "activerecord", "~> 5.0.0.rc1", :require => "active_record" -gem "activesupport", "~> 5.0.0.rc1", :require => "active_support/all" -gem "actionpack", "~> 5.0.0.rc1", :require => "action_pack" -gem "rubocop", "0.48.1" +gem 'actionpack', '~> 5.0.0.rc1', require: 'action_pack' +gem 'activerecord', '~> 5.0.0.rc1', require: 'active_record' +gem 'activesupport', '~> 5.0.0.rc1', require: 'active_support/all' platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter" - gem "jdbc-sqlite3" + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' end platforms :ruby, :mswin, :mingw do - gem "sqlite3" - gem "pg", '~> 0.21' + gem 'pg', '~> 0.21' + gem 'sqlite3' end -gemspec :path => "../" +gemspec path: '../' diff --git a/gemfiles/activerecord_5.1.0.gemfile b/gemfiles/activerecord_5.1.0.gemfile index 69f242d3..a9b1d41c 100644 --- a/gemfiles/activerecord_5.1.0.gemfile +++ b/gemfiles/activerecord_5.1.0.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "activerecord", "~> 5.1.0", require: "active_record" -gem "activesupport", "~> 5.1.0", require: "active_support/all" -gem "actionpack", "~> 5.1.0", require: "action_pack" +gem 'actionpack', '~> 5.1.0', require: 'action_pack' +gem 'activerecord', '~> 5.1.0', require: 'active_record' +gem 'activesupport', '~> 5.1.0', require: 'active_support/all' platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter" - gem "jdbc-sqlite3" + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' end platforms :ruby, :mswin, :mingw do - gem "sqlite3" - gem "pg", "~> 0.21" + gem 'pg', '~> 0.21' + gem 'sqlite3' end -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/activerecord_5.2.0.gemfile b/gemfiles/activerecord_5.2.0.gemfile index b3ad6223..70192ab1 100644 --- a/gemfiles/activerecord_5.2.0.gemfile +++ b/gemfiles/activerecord_5.2.0.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "activerecord", "~> 5.2.0", require: "active_record" -gem "activesupport", "~> 5.2.0", require: "active_support/all" -gem "actionpack", "~> 5.2.0", require: "action_pack" +gem 'actionpack', '~> 5.2.0', require: 'action_pack' +gem 'activerecord', '~> 5.2.0', require: 'active_record' +gem 'activesupport', '~> 5.2.0', require: 'active_support/all' platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter" - gem "jdbc-sqlite3" + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' end platforms :ruby, :mswin, :mingw do - gem "sqlite3" - gem "pg", "~> 0.21" + gem 'pg', '~> 0.21' + gem 'sqlite3' end -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/activerecord_5.2.1.gemfile b/gemfiles/activerecord_5.2.1.gemfile index 432586ea..e08f8896 100644 --- a/gemfiles/activerecord_5.2.1.gemfile +++ b/gemfiles/activerecord_5.2.1.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "activerecord", "~> 5.2.1", require: "active_record" -gem "activesupport", "~> 5.2.1", require: "active_support/all" -gem "actionpack", "~> 5.2.1", require: "action_pack" +gem 'actionpack', '~> 5.2.1', require: 'action_pack' +gem 'activerecord', '~> 5.2.1', require: 'active_record' +gem 'activesupport', '~> 5.2.1', require: 'active_support/all' platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter" - gem "jdbc-sqlite3" + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' end platforms :ruby, :mswin, :mingw do - gem "sqlite3" - gem "pg", "~> 0.21" + gem 'pg', '~> 0.21' + gem 'sqlite3' end -gemspec path: "../" +gemspec path: '../' diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 20ab6297..dc066b3b 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -167,10 +167,7 @@ def model_adapter(model_class, action) # See ControllerAdditions#authorize! for documentation. def authorize!(action, subject, *args) - message = nil - if args.last.is_a?(Hash) && args.last.key?(:message) - message = args.pop[:message] - end + message = args.last.is_a?(Hash) && args.last.key?(:message) ? args.pop[:message] : nil if cannot?(action, subject, *args) message ||= unauthorized_message(action, subject) raise AccessDenied.new(message, action, subject, args) diff --git a/lib/cancan/ability/rules.rb b/lib/cancan/ability/rules.rb index 00ab4250..42e56e9d 100644 --- a/lib/cancan/ability/rules.rb +++ b/lib/cancan/ability/rules.rb @@ -31,6 +31,7 @@ def add_rule_to_index(rule, position) # This does not take into consideration any hash conditions or block statements def relevant_rules(action, subject) return [] unless @rules + relevant = possible_relevant_rules(subject).select do |rule| rule.expanded_actions = expand_actions(rule.actions) rule.relevant? action, subject @@ -53,6 +54,7 @@ def possible_relevant_rules(subject) def relevant_rules_for_match(action, subject) relevant_rules(action, subject).each do |rule| next unless rule.only_raw_sql? + raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition."\ " The checking code cannot be determined for #{action.inspect} #{subject.inspect}" @@ -75,6 +77,7 @@ def optimize_order!(rules) (first_can_in_group = -1) && next unless rule.base_behavior (first_can_in_group = i) && next if first_can_in_group == -1 next unless rule.subjects == [:all] + rules[i] = rules[first_can_in_group] rules[first_can_in_group] = rule first_can_in_group += 1 diff --git a/lib/cancan/conditions_matcher.rb b/lib/cancan/conditions_matcher.rb index 9c5fee15..431fd761 100644 --- a/lib/cancan/conditions_matcher.rb +++ b/lib/cancan/conditions_matcher.rb @@ -4,6 +4,7 @@ module ConditionsMatcher def matches_conditions?(action, subject, extra_args) return call_block_with_all(action, subject, extra_args) if @match_all return @block.call(subject, *extra_args) if @block && !subject_class?(subject) + matches_non_block_conditions(subject) end @@ -11,7 +12,7 @@ def matches_conditions?(action, subject, extra_args) def subject_class?(subject) klass = (subject.is_a?(Hash) ? subject.values.first : subject).class - klass == Class || klass == Module + [Class, Module].include? klass end def matches_non_block_conditions(subject) @@ -34,6 +35,7 @@ def nested_subject_matches_conditions?(subject_hash) # matches_conditions_hash?(subject, conditions) def matches_conditions_hash?(subject, conditions = @conditions) return true if conditions.empty? + adapter = model_adapter(subject) if adapter.override_conditions_hash_matching?(subject, conditions) diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index 547d345f..55de662f 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -260,6 +260,7 @@ def check_authorization(options = {}) next if controller.instance_variable_defined?(:@_authorized) next if options[:if] && !controller.send(options[:if]) next if options[:unless] && controller.send(options[:unless]) + raise AuthorizationNotPerformed, 'This action failed the check_authorization because it does not authorize_resource. '\ 'Add skip_authorization_check to bypass this check.' diff --git a/lib/cancan/controller_resource.rb b/lib/cancan/controller_resource.rb index 824dc965..4c77364a 100644 --- a/lib/cancan/controller_resource.rb +++ b/lib/cancan/controller_resource.rb @@ -34,6 +34,7 @@ def load_and_authorize_resource def authorize_resource return if skip?(:authorize) + @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent) end @@ -43,6 +44,7 @@ def parent? def skip?(behavior) return false unless (options = @controller.class.cancan_skipper[behavior][@name]) + options == {} || options[:except] && !action_exists_in?(options[:except]) || action_exists_in?(options[:only]) @@ -90,6 +92,7 @@ def resource_instance=(instance) def resource_instance return unless load_instance? && @controller.instance_variable_defined?("@#{instance_name}") + @controller.instance_variable_get("@#{instance_name}") end @@ -99,6 +102,7 @@ def collection_instance=(instance) def collection_instance return unless @controller.instance_variable_defined?("@#{instance_name.to_s.pluralize}") + @controller.instance_variable_get("@#{instance_name.to_s.pluralize}") end diff --git a/lib/cancan/controller_resource_loader.rb b/lib/cancan/controller_resource_loader.rb index 54acf905..2186ddbe 100644 --- a/lib/cancan/controller_resource_loader.rb +++ b/lib/cancan/controller_resource_loader.rb @@ -11,6 +11,7 @@ module ControllerResourceLoader def load_resource return if skip?(:load) + if load_instance? self.resource_instance ||= load_resource_instance elsif load_collection? @@ -26,6 +27,7 @@ def new_actions def resource_params_by_key(key) return unless @options[key] && @params.key?(extract_key(@options[key])) + @params[extract_key(@options[key])] end diff --git a/lib/cancan/matchers.rb b/lib/cancan/matchers.rb index 13110b57..00d86059 100644 --- a/lib/cancan/matchers.rb +++ b/lib/cancan/matchers.rb @@ -12,6 +12,7 @@ actions = args.first if actions.is_a? Array break false if actions.empty? + actions.all? { |action| ability.can?(action, *args[1..-1]) } else ability.can?(*args) diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index c7856460..eb0af8ce 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -42,14 +42,14 @@ def sanitize_sql_activerecord5(conditions) .join(' AND ') end - def visit_nodes(b) + def visit_nodes(node) # Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query if ActiveRecord::VERSION::MINOR >= 2 connection = @model_class.send(:connection) collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new) - connection.visitor.accept(b, collector).value + connection.visitor.accept(node, collector).value else - @model_class.send(:connection).visitor.compile(b) + @model_class.send(:connection).visitor.compile(node) end end end diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 3603ceb2..057730df 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -58,6 +58,7 @@ def override_scope conditions = @rules.map(&:conditions).compact return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) } return conditions.first if conditions.size == 1 + raise_override_scope_error end diff --git a/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb b/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb index 9667d2f3..6f320f01 100644 --- a/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +++ b/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb @@ -6,7 +6,7 @@ module Joins # See ModelAdditions#accessible_by def joins joins_hash = {} - @rules.reverse.each do |rule| + @rules.reverse_each do |rule| merge_joins(joins_hash, rule.associations_hash) end clean_joins(joins_hash) unless joins_hash.empty? diff --git a/lib/cancan/model_adapters/conditions_extractor.rb b/lib/cancan/model_adapters/conditions_extractor.rb index 057a8d7b..bfe8e872 100644 --- a/lib/cancan/model_adapters/conditions_extractor.rb +++ b/lib/cancan/model_adapters/conditions_extractor.rb @@ -12,6 +12,7 @@ def initialize(model_class) def tableize_conditions(conditions, model_class = @root_model_class, path_to_key = 0) return conditions unless conditions.is_a? Hash + conditions.each_with_object({}) do |(key, value), result_hash| if value.is_a? Hash result_hash.merge!(calculate_result_hash(key, model_class, path_to_key, result_hash, value)) @@ -26,9 +27,8 @@ def tableize_conditions(conditions, model_class = @root_model_class, path_to_key def calculate_result_hash(key, model_class, path_to_key, result_hash, value) reflection = model_class.reflect_on_association(key) - unless reflection - raise WrongAssociationName, "association #{key} not defined in model #{model_class.name}" - end + raise WrongAssociationName, "association #{key} not defined in model #{model_class.name}" unless reflection + nested_resulted = calculate_nested(model_class, result_hash, key, value.dup, path_to_key) association_class = reflection.klass.name.constantize tableize_conditions(nested_resulted, association_class, "#{path_to_key}_#{key}") diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 4ab345c2..bd37417f 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -16,6 +16,7 @@ def initialize(base_behavior, action, subject, conditions, block) both_block_and_hash_error = 'You are not able to supply a block with a hash of conditions in '\ "#{action} #{subject} ability. Use either one." raise Error, both_block_and_hash_error if conditions.is_a?(Hash) && block + @match_all = action.nil? && subject.nil? @base_behavior = base_behavior @actions = Array(action) diff --git a/lib/cancan/rules_compressor.rb b/lib/cancan/rules_compressor.rb index 15655c69..15a954ac 100644 --- a/lib/cancan/rules_compressor.rb +++ b/lib/cancan/rules_compressor.rb @@ -11,6 +11,7 @@ def initialize(rules) def compress(array) idx = array.rindex(&:catch_all?) return array unless idx + value = array[idx] array[idx..-1] .drop_while { |n| n.base_behavior == value.base_behavior } diff --git a/lib/generators/cancan/ability/ability_generator.rb b/lib/generators/cancan/ability/ability_generator.rb index 21568507..595b0092 100644 --- a/lib/generators/cancan/ability/ability_generator.rb +++ b/lib/generators/cancan/ability/ability_generator.rb @@ -1,7 +1,7 @@ module Cancan module Generators class AbilityGenerator < Rails::Generators::Base - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def generate_ability copy_file 'ability.rb', 'app/models/ability.rb' diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index 0508b607..beb91853 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -48,9 +48,7 @@ class Child < ActiveRecord::Base end class Shape < ActiveRecord::Base - unless defined_enums.keys.include? 'color' - enum color: %i[red green blue] - end + enum color: %i[red green blue] unless defined_enums.key? 'color' end red = Shape.create!(color: :red) @@ -88,8 +86,8 @@ class Shape < ActiveRecord::Base end class Disc < ActiveRecord::Base - enum color: %i[red green blue] unless defined_enums.keys.include? 'color' - enum shape: { triangle: 3, rectangle: 4 } unless defined_enums.keys.include? 'shape' + enum color: %i[red green blue] unless defined_enums.key? 'color' + enum shape: { triangle: 3, rectangle: 4 } unless defined_enums.key? 'shape' end red_triangle = Disc.create!(color: Disc.colors[:red], shape: Disc.shapes[:triangle]) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 26b059de..29feeca7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,12 +7,10 @@ require 'cancan/matchers' # I8n setting to fix deprecation. -if defined?(I18n) && I18n.respond_to?('enforce_available_locales=') - I18n.enforce_available_locales = false -end +I18n.enforce_available_locales = false if defined?(I18n) && I18n.respond_to?('enforce_available_locales=') # Add support to load paths -$LOAD_PATH.unshift File.expand_path('../support', __FILE__) +$LOAD_PATH.unshift File.expand_path('support', __dir__) Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } RSpec.configure do |config| From c7957d79d9f332954df02a1831a4bd0cd1507393 Mon Sep 17 00:00:00 2001 From: yui-knk Date: Tue, 23 Oct 2018 07:50:02 +0900 Subject: [PATCH 12/54] Update spec/README.rdoc to followup current situation * gemfile for activerecord_3.2 was removed. * Current supported Ruby version is 2.3+. https://github.com/CanCanCommunity/cancancan/pull/529 --- spec/README.rdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/README.rdoc b/spec/README.rdoc index 88638a93..df03a789 100644 --- a/spec/README.rdoc +++ b/spec/README.rdoc @@ -16,11 +16,11 @@ You can then run all test sets: Or individual ones: - appraisal activerecord_3.2 rake + appraisal activerecord_5.2.0 rake A list of the tests is in the +Appraisal+ file. -The specs support Ruby 1.8.7+ +The specs support Ruby 2.3+ == Model Adapters From 13ce98b29284c3610ea35b3768ece827b955b2d8 Mon Sep 17 00:00:00 2001 From: yui-knk Date: Tue, 23 Oct 2018 07:54:12 +0900 Subject: [PATCH 13/54] Fix a typo of test description --- spec/cancan/ability_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 86ec23d3..f62fab7f 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -491,7 +491,7 @@ class Container < Hash expect(@ability).to have_raw_sql(:read, :foo) end - it 'determines model adapterO class by asking AbstractAdapter' do + it 'determines model adapter class by asking AbstractAdapter' do adapter_class = double model_class = double allow(CanCan::ModelAdapters::AbstractAdapter).to receive(:adapter_class).with(model_class) { adapter_class } From 35401c5552a454f0ac4304c87ee1616f53cd026c Mon Sep 17 00:00:00 2001 From: Lukas Bischof Date: Wed, 28 Nov 2018 15:51:30 +0100 Subject: [PATCH 14/54] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d43aa2d..fbd951fa 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop Thanks to [Renuo AG](https://www.renuo.ch) for currently maintaining and supporting the project. Also many thanks to the [CanCanCan contributors](https://github.com/CanCanCommunity/cancancan/contributors). -See the [CHANGELOG](https://github.com/CanCanCommunity/cancancan/blob/master/CHANGELOG.rdoc) for the full list. +See the [CHANGELOG](https://github.com/CanCanCommunity/cancancan/blob/master/CHANGELOG.md) for the full list. CanCanCan was inspired by [declarative_authorization](https://github.com/stffn/declarative_authorization/) and [aegis](https://github.com/makandra/aegis). From 2a9375c64f343ffebbd511b3ff0be14df2f7fc78 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Tue, 8 Jan 2019 13:40:28 +0100 Subject: [PATCH 15/54] Update to bundler 2.0 --- cancancan.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cancancan.gemspec b/cancancan.gemspec index a7034fa1..4b01c132 100644 --- a/cancancan.gemspec +++ b/cancancan.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.2.0' - s.add_development_dependency 'bundler', '~> 1.3' + s.add_development_dependency 'bundler', '~> 2.0' s.add_development_dependency 'rubocop', '~> 0.48.1' s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1' s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0' From aab5bdc6c625ac37e00d3bc0c0a7903783c455f4 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Tue, 8 Jan 2019 13:52:38 +0100 Subject: [PATCH 16/54] Install latest bundler --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f85fdf19..280bc740 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,5 +38,6 @@ notifications: on_failure: change before_install: - gem update --system + - gem install bundler script: - bundle exec rubocop && bundle exec rake From 24ceac4c1b92f710cd1af2f5c1462d6a2f74ec61 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Tue, 8 Jan 2019 15:02:05 +0100 Subject: [PATCH 17/54] Update Travis configuration --- .travis.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 280bc740..6fa4a6fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,9 @@ rvm: - 2.4.2 - 2.5.0 - 2.5.1 - - jruby-9.0.5.0 + - 2.6.0 - jruby-9.1.9.0 + - jruby-9.2.5.0 gemfile: - gemfiles/activerecord_4.2.gemfile - gemfiles/activerecord_5.0.2.gemfile @@ -18,16 +19,10 @@ services: matrix: fast_finish: true exclude: - - rvm: jruby-9.0.5.0 - gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.0.2.gemfile - - rvm: jruby-9.0.5.0 - gemfile: gemfiles/activerecord_5.1.0.gemfile - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.1.0.gemfile - - rvm: jruby-9.0.5.0 - gemfile: gemfiles/activerecord_5.2.0.gemfile - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.2.0.gemfile notifications: From 946a32dd78758e697dfed65274569e0b86fe10fd Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Tue, 8 Jan 2019 15:32:02 +0100 Subject: [PATCH 18/54] exclude non working configuration for jruby --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6fa4a6fb..2b4c9ae2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,8 @@ matrix: gemfile: gemfiles/activerecord_5.1.0.gemfile - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.2.0.gemfile + - rvm: jruby-9.2.5.0 + gemfile: gemfiles/activerecord_5.0.2.gemfile notifications: email: recipients: From b3752fb0940e8dd27f4b9c2c022fe1315d969a7a Mon Sep 17 00:00:00 2001 From: Robert Paul Date: Sat, 26 Jan 2019 10:32:04 +0000 Subject: [PATCH 19/54] Fix typos referring to authorized! method (#553) --- lib/cancan/controller_additions.rb | 2 +- lib/cancan/exceptions.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index 547d345f..64ce4997 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -225,7 +225,7 @@ def skip_authorize_resource(*args) cancan_skipper[:authorize][name] = options end - # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call. + # Add this to a controller to ensure it performs authorization through +authorize+! or +authorize_resource+ call. # If neither of these authorization methods are called, # a CanCan::AuthorizationNotPerformed exception will be raised. # This is normally added to the ApplicationController to ensure all controller actions do authorization. diff --git a/lib/cancan/exceptions.rb b/lib/cancan/exceptions.rb index 78cdaecf..e11ae546 100644 --- a/lib/cancan/exceptions.rb +++ b/lib/cancan/exceptions.rb @@ -8,7 +8,7 @@ class NotImplemented < Error; end # Raised when removed code is called, an alternative solution is provided in message. class ImplementationRemoved < Error; end - # Raised when using check_authorization without calling authorized! + # Raised when using check_authorization without calling authorize! class AuthorizationNotPerformed < Error; end # Raised when using a wrong association name @@ -33,7 +33,7 @@ class WrongAssociationName < Error; end # exception.default_message = "Default error message" # exception.message # => "Default error message" # - # See ControllerAdditions#authorized! for more information on rescuing from this exception + # See ControllerAdditions#authorize! for more information on rescuing from this exception # and customizing the message using I18n. class AccessDenied < Error attr_reader :action, :subject, :conditions From 4eecb081021ec137a55405cc0258f11eabb6cdce Mon Sep 17 00:00:00 2001 From: Joel Ambass Date: Sat, 26 Jan 2019 11:48:23 +0100 Subject: [PATCH 20/54] Merge alias actions when merging abilities (#538) Closes https://github.com/CanCanCommunity/cancancan/issues/468 Closes https://github.com/CanCanCommunity/cancancan/issues/280 This changes the behaviour of `Ability#merge` to also include defined `alias_actions`. If a collision between the defined aliases occurs the actions on +self+ will be overwritten with those on the passed object. The inline documentation has been updated in order to reflect those changes. --- lib/cancan/ability.rb | 28 +++++++++++++++++++++++++++- spec/cancan/ability_spec.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 20ab6297..33d04b0b 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -202,12 +202,13 @@ def has_raw_sql?(action, subject) relevant_rules(action, subject).any?(&:only_raw_sql?) end - # Copies all rules of the given +CanCan::Ability+ and adds them to +self+. + # Copies all rules and aliased actions of the given +CanCan::Ability+ and adds them to +self+. # class ReadAbility # include CanCan::Ability # # def initialize # can :read, User + # alias_action :show, :index, to: :see # end # end # @@ -216,6 +217,7 @@ def has_raw_sql?(action, subject) # # def initialize # can :edit, User + # alias_action :create, :update, to: :modify # end # end # @@ -223,11 +225,35 @@ def has_raw_sql?(action, subject) # read_ability.can? :edit, User.new #=> false # read_ability.merge(WritingAbility.new) # read_ability.can? :edit, User.new #=> true + # read_ability.aliased_actions #=> [:see => [:show, :index], :modify => [:create, :update]] # + # If there are collisions when merging the +aliased_actions+, the actions on +self+ will be + # overwritten. + # + # class ReadAbility + # include CanCan::Ability + # + # def initialize + # alias_action :show, :index, to: :see + # end + # end + # + # class ShowAbility + # include CanCan::Ability + # + # def initialize + # alias_action :show, to: :see + # end + # end + # + # read_ability = ReadAbility.new + # read_ability.merge(ShowAbility) + # read_ability.aliased_actions #=> [:see => [:show]] def merge(ability) ability.rules.each do |rule| add_rule(rule.dup) end + @aliased_actions = aliased_actions.merge(ability.aliased_actions) self end diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index f62fab7f..fbccc108 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -564,6 +564,35 @@ class Container < Hash expect(@ability.send(:rules).size).to eq(2) end + it 'adds the aliased actions from the given ability' do + @ability.alias_action :show, to: :see + (another_ability = double).extend(CanCan::Ability) + another_ability.alias_action :create, :update, to: :manage + + @ability.merge(another_ability) + expect(@ability.aliased_actions).to eq( + read: %i[index show], + create: %i[new], + update: %i[edit], + manage: %i[create update], + see: %i[show] + ) + end + + it 'overwrittes the aliased actions with the value from the given ability' do + @ability.alias_action :show, :index, to: :see + (another_ability = double).extend(CanCan::Ability) + another_ability.alias_action :show, to: :see + + @ability.merge(another_ability) + expect(@ability.aliased_actions).to eq( + read: %i[index show], + create: %i[new], + update: %i[edit], + see: %i[show] + ) + end + it 'can add an empty ability' do (another_ability = double).extend(CanCan::Ability) From f58f2093394dcb4f84f712ad76e833df86886c5c Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 26 Jan 2019 11:50:22 +0100 Subject: [PATCH 21/54] add not_nil test, remove todo (#557) --- spec/cancan/ability_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index fbccc108..4aad3d00 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -16,9 +16,9 @@ end it 'passes true to `can?` when non false/nil is returned in block' do - @ability.can :read, :all - @ability.can :read, Symbol do |_sym| - 'foo' # TODO: test that sym is nil when no instance is passed + @ability.can :read, Symbol do |sym| + expect(sym).not_to be_nil + 'foo' end expect(@ability.can?(:read, :some_symbol)).to be(true) end From a32953948457461a109de94af24c8e21c3f61094 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 26 Jan 2019 11:52:31 +0100 Subject: [PATCH 22/54] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5070c3df..76909f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +* [#538](https://github.com/CanCanCommunity/cancancan/issues/538): Merge alias actions when merging abilities. ([@Jcambass][]) + ## 2.3.0 (Sep 16th, 2018) * [#528](https://github.com/CanCanCommunity/cancancan/issues/528): Compress irrelevant rules before generating a query to optimize performances. ([@coorasse][]) @@ -615,3 +619,4 @@ [@oliverklee]: https://github.com/oliverklee [@gingray]: https://github.com/gingray [@timraymond]: https://github.com/timraymond +[@Jcambass]: https://github.com/Jcambass From 2670c02b808c4bd9a0d4067b634230628a4f1a23 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 26 Jan 2019 12:10:40 +0100 Subject: [PATCH 23/54] Switch to bundler 2.0 --- cancancan.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cancancan.gemspec b/cancancan.gemspec index 8c9abd2f..05716bc5 100644 --- a/cancancan.gemspec +++ b/cancancan.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.2.0' s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0' - s.add_development_dependency 'bundler', '~> 1.3' + s.add_development_dependency 'bundler', '~> 2.0' s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1' s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0' s.add_development_dependency 'rubocop', '~> 0.59.2' From 5db6ddd6ebce473d86cfa8cbae7e3f78331dd276 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 26 Jan 2019 12:58:52 +0100 Subject: [PATCH 24/54] Add configuration for Rails 6.0 Add support for Rails 6.0 Update CHANGELOG Add missing dot Add exclusions to Travis matrix exclude jdbc and rails 6.0 Correct travis configuration --- .rubocop.yml | 4 +- .rubocop_todo.yml | 0 .travis.yml | 19 ++++++++- Appraisals | 16 ++++++++ CHANGELOG.md | 1 + cancancan.gemspec | 2 +- gemfiles/activerecord_4.2.gemfile | 20 ++++----- gemfiles/activerecord_5.0.2.gemfile | 18 ++++---- gemfiles/activerecord_5.1.0.gemfile | 18 ++++---- gemfiles/activerecord_5.2.0.gemfile | 19 --------- gemfiles/activerecord_5.2.1.gemfile | 18 ++++---- gemfiles/activerecord_6.0.0.gemfile | 19 +++++++++ lib/cancan/ability.rb | 2 +- .../ability/strong_parameter_support.rb | 2 +- lib/cancan/conditions_matcher.rb | 3 ++ .../model_adapters/active_record_4_adapter.rb | 9 ++-- .../model_adapters/active_record_5_adapter.rb | 4 +- .../model_adapters/active_record_adapter.rb | 41 +++++++++++++++++-- .../active_record_adapter/joins.rb | 39 ------------------ lib/cancan/rule.rb | 3 ++ .../active_record_4_adapter_spec.rb | 4 +- .../active_record_5_adapter_spec.rb | 3 +- .../active_record_adapter_spec.rb | 37 +++++++++++++---- 23 files changed, 178 insertions(+), 123 deletions(-) delete mode 100644 .rubocop_todo.yml delete mode 100644 gemfiles/activerecord_5.2.0.gemfile create mode 100644 gemfiles/activerecord_6.0.0.gemfile delete mode 100644 lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb diff --git a/.rubocop.yml b/.rubocop.yml index cd03d3dc..c7c1d81f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -39,7 +39,5 @@ Lint/AmbiguousBlockAssociation: AllCops: TargetRubyVersion: 2.2.0 Exclude: - - 'gemfiles/vendor/bundle/**/*' + - 'gemfiles/**/*' - 'Appraisals' - -inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/.travis.yml b/.travis.yml index 2b4c9ae2..47774f18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,26 +7,43 @@ rvm: - 2.5.0 - 2.5.1 - 2.6.0 + - ruby-head - jruby-9.1.9.0 - jruby-9.2.5.0 + - jruby-head + gemfile: - gemfiles/activerecord_4.2.gemfile - gemfiles/activerecord_5.0.2.gemfile - gemfiles/activerecord_5.1.0.gemfile - - gemfiles/activerecord_5.2.0.gemfile + - gemfiles/activerecord_5.2.1.gemfile + - gemfiles/activerecord_6.0.0.gemfile services: - mongodb + matrix: fast_finish: true exclude: + - rvm: 2.3.5 + gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: 2.4.2 + gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.1.0.gemfile - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.2.0.gemfile + - rvm: jruby-9.1.9.0 + gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: jruby-9.2.5.0 gemfile: gemfiles/activerecord_5.0.2.gemfile + - rvm: jruby-9.2.5.0 + gemfile: gemfiles/activerecord_6.0.0.gemfile + allow_failures: + - rvm: ruby-head + - rvm: jruby-head + notifications: email: recipients: diff --git a/Appraisals b/Appraisals index b332afe0..ea9a0dc8 100644 --- a/Appraisals +++ b/Appraisals @@ -62,3 +62,19 @@ appraise 'activerecord_5.2.1' do gem 'pg', '~> 0.21' end end + +appraise 'activerecord_6.0.0' do + gem 'actionpack', '~> 6.0.0.beta1', require: 'action_pack' + gem 'activerecord', '~> 6.0.0.beta1', require: 'active_record' + gem 'activesupport', '~> 6.0.0.beta1', require: 'active_support/all' + + platforms :jruby do + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' + end + + platforms :ruby, :mswin, :mingw do + gem 'pg', '~> 1.1.4' + gem 'sqlite3' + end +end diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca580fd..3d42a716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Unreleased +* [#560](https://github.com/CanCanCommunity/cancancan/pull/560): Add support for Rails 6.0. ([@coorasse][]) * [#489](https://github.com/CanCanCommunity/cancancan/pull/489): Drop support for actions without a subject. ([@andrew-aladev][]) * [#474](https://github.com/CanCanCommunity/cancancan/pull/474): Allow to add attribute-level rules. ([@phaedryx][]) * [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed eager loading of associations for ActiveRecord 5. ([@kaspernj][]) diff --git a/cancancan.gemspec b/cancancan.gemspec index 05716bc5..f90de120 100644 --- a/cancancan.gemspec +++ b/cancancan.gemspec @@ -22,5 +22,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'bundler', '~> 2.0' s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1' s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0' - s.add_development_dependency 'rubocop', '~> 0.59.2' + s.add_development_dependency 'rubocop', '~> 0.63.1' end diff --git a/gemfiles/activerecord_4.2.gemfile b/gemfiles/activerecord_4.2.gemfile index feca9d34..9d7a4b7f 100644 --- a/gemfiles/activerecord_4.2.gemfile +++ b/gemfiles/activerecord_4.2.gemfile @@ -1,20 +1,20 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 4.2.0', require: 'action_pack' -gem 'activerecord', '~> 4.2.0', require: 'active_record' -gem 'activesupport', '~> 4.2.0', require: 'active_support/all' -gem 'nokogiri', '~> 1.6.8', require: 'nokogiri' +gem "activerecord", "~> 4.2.0", require: "active_record" +gem "activesupport", "~> 4.2.0", require: "active_support/all" +gem "actionpack", "~> 4.2.0", require: "action_pack" +gem "nokogiri", "~> 1.6.8", require: "nokogiri" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.24' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.24" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_5.0.2.gemfile b/gemfiles/activerecord_5.0.2.gemfile index 40413f0f..40f67ff6 100644 --- a/gemfiles/activerecord_5.0.2.gemfile +++ b/gemfiles/activerecord_5.0.2.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.0.2', require: 'action_pack' -gem 'activerecord', '~> 5.0.2', require: 'active_record' -gem 'activesupport', '~> 5.0.2', require: 'active_support/all' +gem "activerecord", "~> 5.0.2", require: "active_record" +gem "activesupport", "~> 5.0.2", require: "active_support/all" +gem "actionpack", "~> 5.0.2", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_5.1.0.gemfile b/gemfiles/activerecord_5.1.0.gemfile index a9b1d41c..69f242d3 100644 --- a/gemfiles/activerecord_5.1.0.gemfile +++ b/gemfiles/activerecord_5.1.0.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.1.0', require: 'action_pack' -gem 'activerecord', '~> 5.1.0', require: 'active_record' -gem 'activesupport', '~> 5.1.0', require: 'active_support/all' +gem "activerecord", "~> 5.1.0", require: "active_record" +gem "activesupport", "~> 5.1.0", require: "active_support/all" +gem "actionpack", "~> 5.1.0", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_5.2.0.gemfile b/gemfiles/activerecord_5.2.0.gemfile deleted file mode 100644 index 70192ab1..00000000 --- a/gemfiles/activerecord_5.2.0.gemfile +++ /dev/null @@ -1,19 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 5.2.0', require: 'action_pack' -gem 'activerecord', '~> 5.2.0', require: 'active_record' -gem 'activesupport', '~> 5.2.0', require: 'active_support/all' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' diff --git a/gemfiles/activerecord_5.2.1.gemfile b/gemfiles/activerecord_5.2.1.gemfile index e08f8896..432586ea 100644 --- a/gemfiles/activerecord_5.2.1.gemfile +++ b/gemfiles/activerecord_5.2.1.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.2.1', require: 'action_pack' -gem 'activerecord', '~> 5.2.1', require: 'active_record' -gem 'activesupport', '~> 5.2.1', require: 'active_support/all' +gem "activerecord", "~> 5.2.1", require: "active_record" +gem "activesupport", "~> 5.2.1", require: "active_support/all" +gem "actionpack", "~> 5.2.1", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_6.0.0.gemfile b/gemfiles/activerecord_6.0.0.gemfile new file mode 100644 index 00000000..1e50acaf --- /dev/null +++ b/gemfiles/activerecord_6.0.0.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "actionpack", "~> 6.0.0.beta1", require: "action_pack" +gem "activerecord", "~> 6.0.0.beta1", require: "active_record" +gem "activesupport", "~> 6.0.0.beta1", require: "active_support/all" + +platforms :jruby do + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" +end + +platforms :ruby, :mswin, :mingw do + gem "pg", "~> 1.1.4" + gem "sqlite3" +end + +gemspec path: "../" diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 88653a87..73a77729 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -269,7 +269,7 @@ def merge(ability) # } def permissions permissions_list = { - can: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } }, + can: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } }, cannot: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } } } rules.each { |rule| extract_rule_in_permissions(permissions_list, rule) } diff --git a/lib/cancan/ability/strong_parameter_support.rb b/lib/cancan/ability/strong_parameter_support.rb index 60b14962..02134151 100644 --- a/lib/cancan/ability/strong_parameter_support.rb +++ b/lib/cancan/ability/strong_parameter_support.rb @@ -22,7 +22,7 @@ def permitted_attributes(action, subject) def subject_class?(subject) klass = (subject.is_a?(Hash) ? subject.values.first : subject).class - klass == Class || klass == Module + [Class, Module].include? klass end def get_attributes(rule, subject) diff --git a/lib/cancan/conditions_matcher.rb b/lib/cancan/conditions_matcher.rb index 2f45d57f..498e0c4b 100644 --- a/lib/cancan/conditions_matcher.rb +++ b/lib/cancan/conditions_matcher.rb @@ -5,6 +5,7 @@ def matches_conditions?(action, subject, attribute = nil, *extra_args) return call_block_with_all(action, subject, extra_args) if @match_all return matches_block_conditions(subject, attribute, *extra_args) if @block return matches_non_block_conditions(subject) unless conditions_empty? + true end @@ -17,12 +18,14 @@ def subject_class?(subject) def matches_block_conditions(subject, *extra_args) return @base_behavior if subject_class?(subject) + @block.call(subject, *extra_args) end def matches_non_block_conditions(subject) return nested_subject_matches_conditions?(subject) if subject.class == Hash return matches_conditions_hash?(subject) unless subject_class?(subject) + # Don't stop at "cannot" definitions when there are conditions. @base_behavior end diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index d89c3a59..0598b270 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -1,9 +1,10 @@ module CanCan module ModelAdapters - class ActiveRecord4Adapter < AbstractAdapter - include ActiveRecordAdapter + class ActiveRecord4Adapter < ActiveRecordAdapter + AbstractAdapter.inherited(self) + def self.for_class?(model_class) - ActiveRecord::VERSION::MAJOR == 4 && model_class <= ActiveRecord::Base + version_lower?('5.0.0') && model_class <= ActiveRecord::Base end # TODO: this should be private @@ -39,7 +40,7 @@ def build_relation(*where_conditions) # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions` def sanitize_sql(conditions) - if ActiveRecord::VERSION::MINOR >= 2 && conditions.is_a?(Hash) + if self.class.version_greater_or_equal?('4.2.0') && conditions.is_a?(Hash) sanitize_sql_activerecord4(conditions) else @model_class.send(:sanitize_sql, conditions) diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index 3d04f12b..efc5a33c 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -4,7 +4,7 @@ class ActiveRecord5Adapter < ActiveRecord4Adapter AbstractAdapter.inherited(self) def self.for_class?(model_class) - ActiveRecord::VERSION::MAJOR == 5 && model_class <= ActiveRecord::Base + version_greater_or_equal?('5.0.0') && model_class <= ActiveRecord::Base end # rails 5 is capable of using strings in enum @@ -51,7 +51,7 @@ def sanitize_sql_activerecord5(conditions) def visit_nodes(node) # Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query - if ActiveRecord::VERSION::MINOR >= 2 + if self.class.version_greater_or_equal?('5.2.0') connection = @model_class.send(:connection) collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new) connection.visitor.accept(node, collector).value diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 057730df..e6ba27b3 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -1,10 +1,15 @@ -require_relative 'can_can/model_adapters/active_record_adapter/joins.rb' require_relative 'conditions_extractor.rb' require 'cancan/rules_compressor' module CanCan module ModelAdapters - module ActiveRecordAdapter - include CanCan::ModelAdapters::ActiveRecordAdapter::Joins + class ActiveRecordAdapter < AbstractAdapter + def self.version_greater_or_equal?(version) + Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version) + end + + def self.version_lower?(version) + Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version) + end # Returns conditions intended to be used inside a database query. Normally you will not call this # method directly, but instead go through ModelAdditions#accessible_by. @@ -48,8 +53,38 @@ def database_records end end + # Returns the associations used in conditions for the :joins option of a search. + # See ModelAdditions#accessible_by + def joins + joins_hash = {} + @rules.reverse_each do |rule| + merge_joins(joins_hash, rule.associations_hash) + end + clean_joins(joins_hash) unless joins_hash.empty? + end + private + # Removes empty hashes and moves everything into arrays. + def clean_joins(joins_hash) + joins = [] + joins_hash.each do |name, nested| + joins << (nested.empty? ? name : { name => clean_joins(nested) }) + end + joins + end + + # Takes two hashes and does a deep merge. + def merge_joins(base, add) + add.each do |name, nested| + if base[name].is_a?(Hash) + merge_joins(base[name], nested) unless nested.empty? + else + base[name] = nested + end + end + end + def mergeable_conditions? @rules.find(&:unmergeable?).blank? end diff --git a/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb b/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb deleted file mode 100644 index 6f320f01..00000000 --- a/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +++ /dev/null @@ -1,39 +0,0 @@ -module CanCan - module ModelAdapters - module ActiveRecordAdapter - module Joins - # Returns the associations used in conditions for the :joins option of a search. - # See ModelAdditions#accessible_by - def joins - joins_hash = {} - @rules.reverse_each do |rule| - merge_joins(joins_hash, rule.associations_hash) - end - clean_joins(joins_hash) unless joins_hash.empty? - end - - private - - # Removes empty hashes and moves everything into arrays. - def clean_joins(joins_hash) - joins = [] - joins_hash.each do |name, nested| - joins << (nested.empty? ? name : { name => clean_joins(nested) }) - end - joins - end - - # Takes two hashes and does a deep merge. - def merge_joins(base, add) - add.each do |name, nested| - if base[name].is_a?(Hash) - merge_joins(base[name], nested) unless nested.empty? - else - base[name] = nested - end - end - end - end - end - end -end diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 139662bb..6ba747fe 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -20,6 +20,7 @@ def initialize(base_behavior, action, subject, *extra_args, &block) condition_and_block_check(extra_args, block, action, subject) @match_all = action.nil? && subject.nil? raise Error, "Subject is required for #{action}" if action && subject.nil? + @base_behavior = base_behavior @actions = Array(action) @subjects = Array(subject) @@ -82,6 +83,7 @@ def attributes_from_conditions def matches_attributes?(attribute) return true if @attributes.empty? return @base_behavior if attribute.nil? + @attributes.include?(attribute.to_sym) end @@ -111,6 +113,7 @@ def parse_attributes_from_extra_args(args) def condition_and_block_check(conditions, block, action, subject) return unless conditions.is_a?(Hash) && block + raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\ "Check \":#{action} #{subject}\" ability." end diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index beb91853..64f32b8d 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -if defined? CanCan::ModelAdapters::ActiveRecord4Adapter +if CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') describe CanCan::ModelAdapters::ActiveRecord4Adapter do context 'with sqlite3' do before :each do @@ -39,7 +39,7 @@ class Child < ActiveRecord::Base .to eq [child2, child1] end - if ActiveRecord::VERSION::MINOR >= 1 || ActiveRecord::VERSION::MAJOR >= 5 + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('4.1.0') it 'allows filters on enums' do ActiveRecord::Schema.define do create_table(:shapes) do |t| diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index 77629685..cd6a1382 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' -if ActiveRecord::VERSION::MAJOR == 5 && defined?(CanCan::ModelAdapters::ActiveRecord5Adapter) +if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') && + defined?(CanCan::ModelAdapters::ActiveRecord5Adapter) describe CanCan::ModelAdapters::ActiveRecord5Adapter do context 'with sqlite3' do before :each do diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 832be450..88e5fa02 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -3,6 +3,23 @@ if defined? CanCan::ModelAdapters::ActiveRecordAdapter describe CanCan::ModelAdapters::ActiveRecordAdapter do + let(:true_v) do + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') + 1 + else + "'t'" + end + end + let(:false_v) do + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') + 0 + else + "'f'" + end + end + + let(:false_condition) { "#{true_v}=#{false_v}" } + before :each do ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') ActiveRecord::Migration.verbose = false @@ -241,12 +258,12 @@ class User < ActiveRecord::Base end it 'has false conditions if no abilities match' do - expect(@ability.model_adapter(Article, :read).conditions).to eq("'t'='f'") + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) end it 'returns false conditions for cannot clause' do @ability.cannot :read, Article - expect(@ability.model_adapter(Article, :read).conditions).to eq("'t'='f'") + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) end it 'returns SQL for single `can` definition in front of default `cannot` condition' do @@ -255,7 +272,7 @@ class User < ActiveRecord::Base expect(@ability.model_adapter(Article, :read)).to generate_sql(%( SELECT "articles".* FROM "articles" -WHERE "articles"."published" = 'f' AND "articles"."secret" = 't')) +WHERE "articles"."published" = #{false_v} AND "articles"."secret" = #{true_v})) end it 'returns true condition for single `can` definition in front of default `can` condition' do @@ -268,14 +285,16 @@ class User < ActiveRecord::Base it 'returns `false condition` for single `cannot` definition in front of default `cannot` condition' do @ability.cannot :read, Article @ability.cannot :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read).conditions).to eq("'t'='f'") + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) end it 'returns `not (sql)` for single `cannot` definition in front of default `can` condition' do @ability.can :read, Article @ability.cannot :read, Article, published: false, secret: true expect(@ability.model_adapter(Article, :read).conditions) - .to orderlessly_match(%["not (#{@article_table}"."published" = 'f' AND "#{@article_table}"."secret" = 't')]) + .to orderlessly_match( + %["not (#{@article_table}"."published" = #{false_v} AND "#{@article_table}"."secret" = #{true_v})] + ) end it 'returns appropriate sql conditions in complex case' do @@ -284,8 +303,8 @@ class User < ActiveRecord::Base @ability.can :update, Article, published: true @ability.cannot :update, Article, secret: true expect(@ability.model_adapter(Article, :update).conditions) - .to eq(%[not ("#{@article_table}"."secret" = 't') ] + - %[AND (("#{@article_table}"."published" = 't') ] + + .to eq(%[not ("#{@article_table}"."secret" = #{true_v}) ] + + %[AND (("#{@article_table}"."published" = #{true_v}) ] + %[OR ("#{@article_table}"."id" = 1))]) expect(@ability.model_adapter(Article, :manage).conditions).to eq(id: 1) expect(@ability.model_adapter(Article, :read).conditions).to eq({}) @@ -305,10 +324,10 @@ class User < ActiveRecord::Base it 'does not forget conditions when calling with SQL string' do @ability.can :read, Article, published: true - @ability.can :read, Article, ['secret=?', false] + @ability.can :read, Article, ['secret = ?', false] adapter = @ability.model_adapter(Article, :read) 2.times do - expect(adapter.conditions).to eq(%[(secret='f') OR ("#{@article_table}"."published" = 't')]) + expect(adapter.conditions).to eq(%[(secret = #{false_v}) OR ("#{@article_table}"."published" = #{true_v})]) end end From f8a3d69b3ead51e9e55ddb3c6eb6b3ec271b414c Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 26 Jan 2019 17:10:01 +0100 Subject: [PATCH 25/54] simplify hash operations --- .../model_adapters/active_record_adapter.rb | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index e6ba27b3..4342251b 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -58,29 +58,25 @@ def database_records def joins joins_hash = {} @rules.reverse_each do |rule| - merge_joins(joins_hash, rule.associations_hash) + deep_merge(joins_hash, rule.associations_hash) end - clean_joins(joins_hash) unless joins_hash.empty? + deep_clean(joins_hash) unless joins_hash.empty? end private # Removes empty hashes and moves everything into arrays. - def clean_joins(joins_hash) - joins = [] - joins_hash.each do |name, nested| - joins << (nested.empty? ? name : { name => clean_joins(nested) }) - end - joins + def deep_clean(joins_hash) + joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } } end # Takes two hashes and does a deep merge. - def merge_joins(base, add) - add.each do |name, nested| - if base[name].is_a?(Hash) - merge_joins(base[name], nested) unless nested.empty? + def deep_merge(base_hash, added_hash) + added_hash.each do |key, value| + if base_hash[key].is_a?(Hash) + deep_merge(base_hash[key], value) unless value.empty? else - base[name] = nested + base_hash[key] = value end end end From 70827f937b0236f4e3086bccfdeb76f6bff70f7d Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Fri, 1 Feb 2019 19:16:09 +0100 Subject: [PATCH 26/54] remove ruby 2.5.0 from the matrix --- .travis.yml | 1 - gemfiles/activerecord_5.0.gemfile | 19 ------------------- 2 files changed, 20 deletions(-) delete mode 100644 gemfiles/activerecord_5.0.gemfile diff --git a/.travis.yml b/.travis.yml index 47774f18..5d708601 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ sudo: false rvm: - 2.3.5 - 2.4.2 - - 2.5.0 - 2.5.1 - 2.6.0 - ruby-head diff --git a/gemfiles/activerecord_5.0.gemfile b/gemfiles/activerecord_5.0.gemfile deleted file mode 100644 index e50f52b5..00000000 --- a/gemfiles/activerecord_5.0.gemfile +++ /dev/null @@ -1,19 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 5.0.0.rc1', require: 'action_pack' -gem 'activerecord', '~> 5.0.0.rc1', require: 'active_record' -gem 'activesupport', '~> 5.0.0.rc1', require: 'active_support/all' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' From 5607ddea9111ccd9a99dabd13b5010cd8a12c9b6 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Fri, 1 Feb 2019 21:06:38 +0100 Subject: [PATCH 27/54] Update Travis and Appraisal configurations --- .rubocop.yml | 2 +- .travis.yml | 3 +-- Appraisals | 2 +- gemfiles/activerecord_4.2.0.gemfile | 20 ++++++++++++++++++++ gemfiles/activerecord_4.2.gemfile | 20 -------------------- gemfiles/activerecord_5.0.2.gemfile | 18 +++++++++--------- gemfiles/activerecord_5.0.gemfile | 19 ------------------- gemfiles/activerecord_5.1.0.gemfile | 18 +++++++++--------- gemfiles/activerecord_5.2.0.gemfile | 19 ------------------- gemfiles/activerecord_5.2.1.gemfile | 18 +++++++++--------- 10 files changed, 50 insertions(+), 89 deletions(-) create mode 100644 gemfiles/activerecord_4.2.0.gemfile delete mode 100644 gemfiles/activerecord_4.2.gemfile delete mode 100644 gemfiles/activerecord_5.0.gemfile delete mode 100644 gemfiles/activerecord_5.2.0.gemfile diff --git a/.rubocop.yml b/.rubocop.yml index cd03d3dc..34085479 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -39,7 +39,7 @@ Lint/AmbiguousBlockAssociation: AllCops: TargetRubyVersion: 2.2.0 Exclude: - - 'gemfiles/vendor/bundle/**/*' + - 'gemfiles/**/*' - 'Appraisals' inherit_from: .rubocop_todo.yml diff --git a/.travis.yml b/.travis.yml index 2b4c9ae2..c53e2249 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,12 @@ sudo: false rvm: - 2.3.5 - 2.4.2 - - 2.5.0 - 2.5.1 - 2.6.0 - jruby-9.1.9.0 - jruby-9.2.5.0 gemfile: - - gemfiles/activerecord_4.2.gemfile + - gemfiles/activerecord_4.2.0.gemfile - gemfiles/activerecord_5.0.2.gemfile - gemfiles/activerecord_5.1.0.gemfile - gemfiles/activerecord_5.2.0.gemfile diff --git a/Appraisals b/Appraisals index b332afe0..f664eefe 100644 --- a/Appraisals +++ b/Appraisals @@ -1,4 +1,4 @@ -appraise 'activerecord_4.2' do +appraise 'activerecord_4.2.0' do gem 'activerecord', '~> 4.2.0', require: 'active_record' gem 'activesupport', '~> 4.2.0', require: 'active_support/all' gem 'actionpack', '~> 4.2.0', require: 'action_pack' diff --git a/gemfiles/activerecord_4.2.0.gemfile b/gemfiles/activerecord_4.2.0.gemfile new file mode 100644 index 00000000..9d7a4b7f --- /dev/null +++ b/gemfiles/activerecord_4.2.0.gemfile @@ -0,0 +1,20 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activerecord", "~> 4.2.0", require: "active_record" +gem "activesupport", "~> 4.2.0", require: "active_support/all" +gem "actionpack", "~> 4.2.0", require: "action_pack" +gem "nokogiri", "~> 1.6.8", require: "nokogiri" + +platforms :jruby do + gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.24" + gem "jdbc-sqlite3" +end + +platforms :ruby, :mswin, :mingw do + gem "sqlite3" + gem "pg", "~> 0.21" +end + +gemspec path: "../" diff --git a/gemfiles/activerecord_4.2.gemfile b/gemfiles/activerecord_4.2.gemfile deleted file mode 100644 index feca9d34..00000000 --- a/gemfiles/activerecord_4.2.gemfile +++ /dev/null @@ -1,20 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 4.2.0', require: 'action_pack' -gem 'activerecord', '~> 4.2.0', require: 'active_record' -gem 'activesupport', '~> 4.2.0', require: 'active_support/all' -gem 'nokogiri', '~> 1.6.8', require: 'nokogiri' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.24' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' diff --git a/gemfiles/activerecord_5.0.2.gemfile b/gemfiles/activerecord_5.0.2.gemfile index 40413f0f..40f67ff6 100644 --- a/gemfiles/activerecord_5.0.2.gemfile +++ b/gemfiles/activerecord_5.0.2.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.0.2', require: 'action_pack' -gem 'activerecord', '~> 5.0.2', require: 'active_record' -gem 'activesupport', '~> 5.0.2', require: 'active_support/all' +gem "activerecord", "~> 5.0.2", require: "active_record" +gem "activesupport", "~> 5.0.2", require: "active_support/all" +gem "actionpack", "~> 5.0.2", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_5.0.gemfile b/gemfiles/activerecord_5.0.gemfile deleted file mode 100644 index e50f52b5..00000000 --- a/gemfiles/activerecord_5.0.gemfile +++ /dev/null @@ -1,19 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 5.0.0.rc1', require: 'action_pack' -gem 'activerecord', '~> 5.0.0.rc1', require: 'active_record' -gem 'activesupport', '~> 5.0.0.rc1', require: 'active_support/all' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' diff --git a/gemfiles/activerecord_5.1.0.gemfile b/gemfiles/activerecord_5.1.0.gemfile index a9b1d41c..69f242d3 100644 --- a/gemfiles/activerecord_5.1.0.gemfile +++ b/gemfiles/activerecord_5.1.0.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.1.0', require: 'action_pack' -gem 'activerecord', '~> 5.1.0', require: 'active_record' -gem 'activesupport', '~> 5.1.0', require: 'active_support/all' +gem "activerecord", "~> 5.1.0", require: "active_record" +gem "activesupport", "~> 5.1.0", require: "active_support/all" +gem "actionpack", "~> 5.1.0", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_5.2.0.gemfile b/gemfiles/activerecord_5.2.0.gemfile deleted file mode 100644 index 70192ab1..00000000 --- a/gemfiles/activerecord_5.2.0.gemfile +++ /dev/null @@ -1,19 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 5.2.0', require: 'action_pack' -gem 'activerecord', '~> 5.2.0', require: 'active_record' -gem 'activesupport', '~> 5.2.0', require: 'active_support/all' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' diff --git a/gemfiles/activerecord_5.2.1.gemfile b/gemfiles/activerecord_5.2.1.gemfile index e08f8896..432586ea 100644 --- a/gemfiles/activerecord_5.2.1.gemfile +++ b/gemfiles/activerecord_5.2.1.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.2.1', require: 'action_pack' -gem 'activerecord', '~> 5.2.1', require: 'active_record' -gem 'activesupport', '~> 5.2.1', require: 'active_support/all' +gem "activerecord", "~> 5.2.1", require: "active_record" +gem "activesupport", "~> 5.2.1", require: "active_support/all" +gem "actionpack", "~> 5.2.1", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" From aab937eba1aa536a4eb7d2743380a8d04244a3e4 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Fri, 1 Feb 2019 21:12:54 +0100 Subject: [PATCH 28/54] remove not necessary checks on specs --- .../active_record_4_adapter_spec.rb | 70 +- .../active_record_5_adapter_spec.rb | 3 +- .../active_record_adapter_spec.rb | 789 +++++++++--------- .../conditions_extractor_spec.rb | 238 +++--- 4 files changed, 545 insertions(+), 555 deletions(-) diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index 64f32b8d..85860345 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -109,51 +109,49 @@ class Disc < ActiveRecord::Base end end - if Gem::Specification.find_all_by_name('pg').any? - context 'with postgresql' do - before :each do - ActiveRecord::Base.establish_connection(adapter: 'postgresql', - database: 'postgres', - schema_search_path: 'public') - ActiveRecord::Base.connection.drop_database('cancan_postgresql_spec') - ActiveRecord::Base.connection.create_database('cancan_postgresql_spec', - 'encoding' => 'utf-8', - 'adapter' => 'postgresql') - ActiveRecord::Base.establish_connection(adapter: 'postgresql', - database: 'cancan_postgresql_spec') - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define do - create_table(:parents) do |t| - t.timestamps null: false - end - - create_table(:children) do |t| - t.timestamps null: false - t.integer :parent_id - end + context 'with postgresql' do + before :each do + ActiveRecord::Base.establish_connection(adapter: 'postgresql', + database: 'postgres', + schema_search_path: 'public') + ActiveRecord::Base.connection.drop_database('cancan_postgresql_spec') + ActiveRecord::Base.connection.create_database('cancan_postgresql_spec', + 'encoding' => 'utf-8', + 'adapter' => 'postgresql') + ActiveRecord::Base.establish_connection(adapter: 'postgresql', + database: 'cancan_postgresql_spec') + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:parents) do |t| + t.timestamps null: false end - class Parent < ActiveRecord::Base - has_many :children, -> { order(id: :desc) } + create_table(:children) do |t| + t.timestamps null: false + t.integer :parent_id end + end - class Child < ActiveRecord::Base - belongs_to :parent - end + class Parent < ActiveRecord::Base + has_many :children, -> { order(id: :desc) } + end - (@ability = double).extend(CanCan::Ability) + class Child < ActiveRecord::Base + belongs_to :parent end - it 'allows overlapping conditions in SQL and merge with hash conditions' do - @ability.can :read, Parent, children: { parent_id: 1 } - @ability.can :read, Parent, children: { parent_id: 1 } + (@ability = double).extend(CanCan::Ability) + end + + it 'allows overlapping conditions in SQL and merge with hash conditions' do + @ability.can :read, Parent, children: { parent_id: 1 } + @ability.can :read, Parent, children: { parent_id: 1 } - parent = Parent.create! - Child.create!(parent: parent, created_at: 1.hours.ago) - Child.create!(parent: parent, created_at: 2.hours.ago) + parent = Parent.create! + Child.create!(parent: parent, created_at: 1.hours.ago) + Child.create!(parent: parent, created_at: 2.hours.ago) - expect(Parent.accessible_by(@ability)).to eq([parent]) - end + expect(Parent.accessible_by(@ability)).to eq([parent]) end end end diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index cd6a1382..75c7c997 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' -if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') && - defined?(CanCan::ModelAdapters::ActiveRecord5Adapter) +if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') describe CanCan::ModelAdapters::ActiveRecord5Adapter do context 'with sqlite3' do before :each do diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 88e5fa02..eb5071fb 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -1,497 +1,492 @@ require 'spec_helper' -if defined? CanCan::ModelAdapters::ActiveRecordAdapter - - describe CanCan::ModelAdapters::ActiveRecordAdapter do - let(:true_v) do - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') - 1 - else - "'t'" - end +describe CanCan::ModelAdapters::ActiveRecordAdapter do + let(:true_v) do + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') + 1 + else + "'t'" end - let(:false_v) do - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') - 0 - else - "'f'" - end + end + let(:false_v) do + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') + 0 + else + "'f'" end + end - let(:false_condition) { "#{true_v}=#{false_v}" } - - before :each do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define do - create_table(:categories) do |t| - t.string :name - t.boolean :visible - t.timestamps null: false - end - - create_table(:projects) do |t| - t.string :name - t.timestamps null: false - end - - create_table(:articles) do |t| - t.string :name - t.timestamps null: false - t.boolean :published - t.boolean :secret - t.integer :priority - t.integer :category_id - t.integer :user_id - end - - create_table(:comments) do |t| - t.boolean :spam - t.integer :article_id - t.timestamps null: false - end - - create_table(:legacy_mentions) do |t| - t.integer :user_id - t.integer :article_id - t.timestamps null: false - end - - create_table(:users) do |t| - t.string :name - t.timestamps null: false - end - end + let(:false_condition) { "#{true_v}=#{false_v}" } - class Project < ActiveRecord::Base + before :each do + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:categories) do |t| + t.string :name + t.boolean :visible + t.timestamps null: false end - class Category < ActiveRecord::Base - has_many :articles + create_table(:projects) do |t| + t.string :name + t.timestamps null: false end - class Article < ActiveRecord::Base - belongs_to :category - has_many :comments - has_many :mentions - has_many :mentioned_users, through: :mentions, source: :user - belongs_to :user + create_table(:articles) do |t| + t.string :name + t.timestamps null: false + t.boolean :published + t.boolean :secret + t.integer :priority + t.integer :category_id + t.integer :user_id end - class Mention < ActiveRecord::Base - self.table_name = 'legacy_mentions' - belongs_to :user - belongs_to :article + create_table(:comments) do |t| + t.boolean :spam + t.integer :article_id + t.timestamps null: false end - class Comment < ActiveRecord::Base - belongs_to :article + create_table(:legacy_mentions) do |t| + t.integer :user_id + t.integer :article_id + t.timestamps null: false end - class User < ActiveRecord::Base - has_many :articles - has_many :mentions - has_many :mentioned_articles, through: :mentions, source: :article + create_table(:users) do |t| + t.string :name + t.timestamps null: false end - - (@ability = double).extend(CanCan::Ability) - @article_table = Article.table_name - @comment_table = Comment.table_name end - it 'is for only active record classes' do - if ActiveRecord.respond_to?(:version) && - ActiveRecord.version > Gem::Version.new('5') - expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to_not be_for_class(Object) - expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to be_for_class(Article) - expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) - .to eq(CanCan::ModelAdapters::ActiveRecord5Adapter) - elsif ActiveRecord.respond_to?(:version) && - ActiveRecord.version > Gem::Version.new('4') - expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to_not be_for_class(Object) - expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to be_for_class(Article) - expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) - .to eq(CanCan::ModelAdapters::ActiveRecord4Adapter) - end + class Project < ActiveRecord::Base end - it 'finds record' do - article = Article.create! - adapter = CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article) - expect(adapter.find(Article, article.id)).to eq(article) + class Category < ActiveRecord::Base + has_many :articles end - it 'does not fetch any records when no abilities are defined' do - Article.create! - expect(Article.accessible_by(@ability)).to be_empty + class Article < ActiveRecord::Base + belongs_to :category + has_many :comments + has_many :mentions + has_many :mentioned_users, through: :mentions, source: :user + belongs_to :user end - it 'fetches all articles when one can read all' do - @ability.can :read, Article - article = Article.create! - expect(Article.accessible_by(@ability)).to eq([article]) + class Mention < ActiveRecord::Base + self.table_name = 'legacy_mentions' + belongs_to :user + belongs_to :article end - it 'fetches only the articles that are published' do - @ability.can :read, Article, published: true - article1 = Article.create!(published: true) - Article.create!(published: false) - expect(Article.accessible_by(@ability)).to eq([article1]) + class Comment < ActiveRecord::Base + belongs_to :article end - it 'fetches any articles which are published or secret' do - @ability.can :read, Article, published: true - @ability.can :read, Article, secret: true - article1 = Article.create!(published: true, secret: false) - article2 = Article.create!(published: true, secret: true) - article3 = Article.create!(published: false, secret: true) - Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to eq([article1, article2, article3]) + class User < ActiveRecord::Base + has_many :articles + has_many :mentions + has_many :mentioned_articles, through: :mentions, source: :article end - it 'fetches any articles which we are cited in' do - user = User.create! - cited = Article.create! - Article.create! - cited.mentioned_users << user - @ability.can :read, Article, mentioned_users: { id: user.id } - @ability.can :read, Article, mentions: { user_id: user.id } - expect(Article.accessible_by(@ability)).to eq([cited]) - end + (@ability = double).extend(CanCan::Ability) + @article_table = Article.table_name + @comment_table = Comment.table_name + end - it 'fetches only the articles that are published and not secret' do - @ability.can :read, Article, published: true - @ability.cannot :read, Article, secret: true - article1 = Article.create!(published: true, secret: false) - Article.create!(published: true, secret: true) - Article.create!(published: false, secret: true) - Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to eq([article1]) + it 'is for only active record classes' do + if ActiveRecord.version > Gem::Version.new('5') + expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to_not be_for_class(Object) + expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to be_for_class(Article) + expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) + .to eq(CanCan::ModelAdapters::ActiveRecord5Adapter) + elsif ActiveRecord.version > Gem::Version.new('4') + expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to_not be_for_class(Object) + expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to be_for_class(Article) + expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) + .to eq(CanCan::ModelAdapters::ActiveRecord4Adapter) end + end - it 'only reads comments for articles which are published' do - @ability.can :read, Comment, article: { published: true } - comment1 = Comment.create!(article: Article.create!(published: true)) - Comment.create!(article: Article.create!(published: false)) - expect(Comment.accessible_by(@ability)).to eq([comment1]) - end + it 'finds record' do + article = Article.create! + adapter = CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article) + expect(adapter.find(Article, article.id)).to eq(article) + end - it 'should only read articles which are published or in visible categories' do - @ability.can :read, Article, category: { visible: true } - @ability.can :read, Article, published: true - article1 = Article.create!(published: true) - Article.create!(published: false) - article3 = Article.create!(published: false, category: Category.create!(visible: true)) - expect(Article.accessible_by(@ability)).to eq([article1, article3]) - end + it 'does not fetch any records when no abilities are defined' do + Article.create! + expect(Article.accessible_by(@ability)).to be_empty + end - it 'should only read categories once even if they have multiple articles' do - @ability.can :read, Category, articles: { published: true } - @ability.can :read, Article, published: true - category = Category.create! - Article.create!(published: true, category: category) - Article.create!(published: true, category: category) - expect(Category.accessible_by(@ability)).to eq([category]) - end + it 'fetches all articles when one can read all' do + @ability.can :read, Article + article = Article.create! + expect(Article.accessible_by(@ability)).to eq([article]) + end - it 'only reads comments for visible categories through articles' do - @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) - Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect(Comment.accessible_by(@ability)).to eq([comment1]) - end + it 'fetches only the articles that are published' do + @ability.can :read, Article, published: true + article1 = Article.create!(published: true) + Article.create!(published: false) + expect(Article.accessible_by(@ability)).to eq([article1]) + end - it 'allows conditions in SQL and merge with hash conditions' do - @ability.can :read, Article, published: true - @ability.can :read, Article, ['secret=?', true] - article1 = Article.create!(published: true, secret: false) - article2 = Article.create!(published: true, secret: true) - article3 = Article.create!(published: false, secret: true) - Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to eq([article1, article2, article3]) - end + it 'fetches any articles which are published or secret' do + @ability.can :read, Article, published: true + @ability.can :read, Article, secret: true + article1 = Article.create!(published: true, secret: false) + article2 = Article.create!(published: true, secret: true) + article3 = Article.create!(published: false, secret: true) + Article.create!(published: false, secret: false) + expect(Article.accessible_by(@ability)).to eq([article1, article2, article3]) + end - it 'allows a scope for conditions' do - @ability.can :read, Article, Article.where(secret: true) - article1 = Article.create!(secret: true) - Article.create!(secret: false) - expect(Article.accessible_by(@ability)).to eq([article1]) - end + it 'fetches any articles which we are cited in' do + user = User.create! + cited = Article.create! + Article.create! + cited.mentioned_users << user + @ability.can :read, Article, mentioned_users: { id: user.id } + @ability.can :read, Article, mentions: { user_id: user.id } + expect(Article.accessible_by(@ability)).to eq([cited]) + end - it 'fetches only associated records when using with a scope for conditions' do - @ability.can :read, Article, Article.where(secret: true) - category1 = Category.create!(visible: false) - category2 = Category.create!(visible: true) - article1 = Article.create!(secret: true, category: category1) - Article.create!(secret: true, category: category2) - expect(category1.articles.accessible_by(@ability)).to eq([article1]) - end + it 'fetches only the articles that are published and not secret' do + @ability.can :read, Article, published: true + @ability.cannot :read, Article, secret: true + article1 = Article.create!(published: true, secret: false) + Article.create!(published: true, secret: true) + Article.create!(published: false, secret: true) + Article.create!(published: false, secret: false) + expect(Article.accessible_by(@ability)).to eq([article1]) + end + + it 'only reads comments for articles which are published' do + @ability.can :read, Comment, article: { published: true } + comment1 = Comment.create!(article: Article.create!(published: true)) + Comment.create!(article: Article.create!(published: false)) + expect(Comment.accessible_by(@ability)).to eq([comment1]) + end + + it 'should only read articles which are published or in visible categories' do + @ability.can :read, Article, category: { visible: true } + @ability.can :read, Article, published: true + article1 = Article.create!(published: true) + Article.create!(published: false) + article3 = Article.create!(published: false, category: Category.create!(visible: true)) + expect(Article.accessible_by(@ability)).to eq([article1, article3]) + end + + it 'should only read categories once even if they have multiple articles' do + @ability.can :read, Category, articles: { published: true } + @ability.can :read, Article, published: true + category = Category.create! + Article.create!(published: true, category: category) + Article.create!(published: true, category: category) + expect(Category.accessible_by(@ability)).to eq([category]) + end + + it 'only reads comments for visible categories through articles' do + @ability.can :read, Comment, article: { category: { visible: true } } + comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: false))) + expect(Comment.accessible_by(@ability)).to eq([comment1]) + end + + it 'allows conditions in SQL and merge with hash conditions' do + @ability.can :read, Article, published: true + @ability.can :read, Article, ['secret=?', true] + article1 = Article.create!(published: true, secret: false) + article2 = Article.create!(published: true, secret: true) + article3 = Article.create!(published: false, secret: true) + Article.create!(published: false, secret: false) + expect(Article.accessible_by(@ability)).to eq([article1, article2, article3]) + end + + it 'allows a scope for conditions' do + @ability.can :read, Article, Article.where(secret: true) + article1 = Article.create!(secret: true) + Article.create!(secret: false) + expect(Article.accessible_by(@ability)).to eq([article1]) + end + + it 'fetches only associated records when using with a scope for conditions' do + @ability.can :read, Article, Article.where(secret: true) + category1 = Category.create!(visible: false) + category2 = Category.create!(visible: true) + article1 = Article.create!(secret: true, category: category1) + Article.create!(secret: true, category: category2) + expect(category1.articles.accessible_by(@ability)).to eq([article1]) + end - it 'raises an exception when trying to merge scope with other conditions' do - @ability.can :read, Article, published: true - @ability.can :read, Article, Article.where(secret: true) - expect(-> { Article.accessible_by(@ability) }) - .to raise_error(CanCan::Error, - 'Unable to merge an Active Record scope with other conditions. '\ + it 'raises an exception when trying to merge scope with other conditions' do + @ability.can :read, Article, published: true + @ability.can :read, Article, Article.where(secret: true) + expect(-> { Article.accessible_by(@ability) }) + .to raise_error(CanCan::Error, + 'Unable to merge an Active Record scope with other conditions. '\ 'Instead use a hash or SQL for read Article ability.') - end + end - it 'does not allow to fetch records when ability with just block present' do - @ability.can :read, Article do - false - end - expect(-> { Article.accessible_by(@ability) }).to raise_error(CanCan::Error) + it 'does not allow to fetch records when ability with just block present' do + @ability.can :read, Article do + false end + expect(-> { Article.accessible_by(@ability) }).to raise_error(CanCan::Error) + end - it 'should support more than one deeply nested conditions' do - @ability.can :read, Comment, article: { - category: { - name: 'foo', visible: true - } + it 'should support more than one deeply nested conditions' do + @ability.can :read, Comment, article: { + category: { + name: 'foo', visible: true } - expect { Comment.accessible_by(@ability) }.to_not raise_error - end + } + expect { Comment.accessible_by(@ability) }.to_not raise_error + end - it 'does not allow to check ability on object against SQL conditions without block' do - @ability.can :read, Article, ['secret=?', true] - expect(-> { @ability.can? :read, Article.new }).to raise_error(CanCan::Error) - end + it 'does not allow to check ability on object against SQL conditions without block' do + @ability.can :read, Article, ['secret=?', true] + expect(-> { @ability.can? :read, Article.new }).to raise_error(CanCan::Error) + end - it 'has false conditions if no abilities match' do - expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) - end + it 'has false conditions if no abilities match' do + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) + end - it 'returns false conditions for cannot clause' do - @ability.cannot :read, Article - expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) - end + it 'returns false conditions for cannot clause' do + @ability.cannot :read, Article + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) + end - it 'returns SQL for single `can` definition in front of default `cannot` condition' do - @ability.cannot :read, Article - @ability.can :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read)).to generate_sql(%( + it 'returns SQL for single `can` definition in front of default `cannot` condition' do + @ability.cannot :read, Article + @ability.can :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read)).to generate_sql(%( SELECT "articles".* FROM "articles" WHERE "articles"."published" = #{false_v} AND "articles"."secret" = #{true_v})) - end - - it 'returns true condition for single `can` definition in front of default `can` condition' do - @ability.can :read, Article - @ability.can :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read).conditions).to eq({}) - expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) - end - - it 'returns `false condition` for single `cannot` definition in front of default `cannot` condition' do - @ability.cannot :read, Article - @ability.cannot :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) - end + end - it 'returns `not (sql)` for single `cannot` definition in front of default `can` condition' do - @ability.can :read, Article - @ability.cannot :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read).conditions) - .to orderlessly_match( - %["not (#{@article_table}"."published" = #{false_v} AND "#{@article_table}"."secret" = #{true_v})] - ) - end + it 'returns true condition for single `can` definition in front of default `can` condition' do + @ability.can :read, Article + @ability.can :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read).conditions).to eq({}) + expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) + end - it 'returns appropriate sql conditions in complex case' do - @ability.can :read, Article - @ability.can :manage, Article, id: 1 - @ability.can :update, Article, published: true - @ability.cannot :update, Article, secret: true - expect(@ability.model_adapter(Article, :update).conditions) - .to eq(%[not ("#{@article_table}"."secret" = #{true_v}) ] + - %[AND (("#{@article_table}"."published" = #{true_v}) ] + - %[OR ("#{@article_table}"."id" = 1))]) - expect(@ability.model_adapter(Article, :manage).conditions).to eq(id: 1) - expect(@ability.model_adapter(Article, :read).conditions).to eq({}) - expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) - end + it 'returns `false condition` for single `cannot` definition in front of default `cannot` condition' do + @ability.cannot :read, Article + @ability.cannot :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) + end - it 'returns appropriate sql conditions in complex case with nested joins' do - @ability.can :read, Comment, article: { category: { visible: true } } - expect(@ability.model_adapter(Comment, :read).conditions).to eq(Category.table_name.to_sym => { visible: true }) - end + it 'returns `not (sql)` for single `cannot` definition in front of default `can` condition' do + @ability.can :read, Article + @ability.cannot :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read).conditions) + .to orderlessly_match( + %["not (#{@article_table}"."published" = #{false_v} AND "#{@article_table}"."secret" = #{true_v})] + ) + end - it 'returns appropriate sql conditions in complex case with nested joins of different depth' do - @ability.can :read, Comment, article: { published: true, category: { visible: true } } - expect(@ability.model_adapter(Comment, :read).conditions) - .to eq(Article.table_name.to_sym => { published: true }, Category.table_name.to_sym => { visible: true }) - end + it 'returns appropriate sql conditions in complex case' do + @ability.can :read, Article + @ability.can :manage, Article, id: 1 + @ability.can :update, Article, published: true + @ability.cannot :update, Article, secret: true + expect(@ability.model_adapter(Article, :update).conditions) + .to eq(%[not ("#{@article_table}"."secret" = #{true_v}) ] + + %[AND (("#{@article_table}"."published" = #{true_v}) ] + + %[OR ("#{@article_table}"."id" = 1))]) + expect(@ability.model_adapter(Article, :manage).conditions).to eq(id: 1) + expect(@ability.model_adapter(Article, :read).conditions).to eq({}) + expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) + end - it 'does not forget conditions when calling with SQL string' do - @ability.can :read, Article, published: true - @ability.can :read, Article, ['secret = ?', false] - adapter = @ability.model_adapter(Article, :read) - 2.times do - expect(adapter.conditions).to eq(%[(secret = #{false_v}) OR ("#{@article_table}"."published" = #{true_v})]) - end - end + it 'returns appropriate sql conditions in complex case with nested joins' do + @ability.can :read, Comment, article: { category: { visible: true } } + expect(@ability.model_adapter(Comment, :read).conditions).to eq(Category.table_name.to_sym => { visible: true }) + end - it 'has nil joins if no rules' do - expect(@ability.model_adapter(Article, :read).joins).to be_nil - end + it 'returns appropriate sql conditions in complex case with nested joins of different depth' do + @ability.can :read, Comment, article: { published: true, category: { visible: true } } + expect(@ability.model_adapter(Comment, :read).conditions) + .to eq(Article.table_name.to_sym => { published: true }, Category.table_name.to_sym => { visible: true }) + end - it 'has nil joins if no nested hashes specified in conditions' do - @ability.can :read, Article, published: false - @ability.can :read, Article, secret: true - expect(@ability.model_adapter(Article, :read).joins).to be_nil + it 'does not forget conditions when calling with SQL string' do + @ability.can :read, Article, published: true + @ability.can :read, Article, ['secret = ?', false] + adapter = @ability.model_adapter(Article, :read) + 2.times do + expect(adapter.conditions).to eq(%[(secret = #{false_v}) OR ("#{@article_table}"."published" = #{true_v})]) end + end - it 'merges separate joins into a single array' do - @ability.can :read, Article, project: { blocked: false } - @ability.can :read, Article, company: { admin: true } - expect(@ability.model_adapter(Article, :read).joins.inspect).to orderlessly_match(%i[company project].inspect) - end + it 'has nil joins if no rules' do + expect(@ability.model_adapter(Article, :read).joins).to be_nil + end - it 'merges same joins into a single array' do - @ability.can :read, Article, project: { blocked: false } - @ability.can :read, Article, project: { admin: true } - expect(@ability.model_adapter(Article, :read).joins).to eq([:project]) - end + it 'has nil joins if no nested hashes specified in conditions' do + @ability.can :read, Article, published: false + @ability.can :read, Article, secret: true + expect(@ability.model_adapter(Article, :read).joins).to be_nil + end - it 'merges nested and non-nested joins' do - @ability.can :read, Article, project: { blocked: false } - @ability.can :read, Article, project: { comments: { spam: true } } - expect(@ability.model_adapter(Article, :read).joins).to eq([{ project: [:comments] }]) - end + it 'merges separate joins into a single array' do + @ability.can :read, Article, project: { blocked: false } + @ability.can :read, Article, company: { admin: true } + expect(@ability.model_adapter(Article, :read).joins.inspect).to orderlessly_match(%i[company project].inspect) + end - it 'merges :all conditions with other conditions' do - user = User.create! - article = Article.create!(user: user) - ability = Ability.new(user) - ability.can :manage, :all - ability.can :manage, Article, user_id: user.id - expect(Article.accessible_by(ability)).to eq([article]) - end + it 'merges same joins into a single array' do + @ability.can :read, Article, project: { blocked: false } + @ability.can :read, Article, project: { admin: true } + expect(@ability.model_adapter(Article, :read).joins).to eq([:project]) + end - it 'should not execute a scope when checking ability on the class' do - relation = Article.where(secret: true) - @ability.can :read, Article, relation do |article| - article.secret == true - end + it 'merges nested and non-nested joins' do + @ability.can :read, Article, project: { blocked: false } + @ability.can :read, Article, project: { comments: { spam: true } } + expect(@ability.model_adapter(Article, :read).joins).to eq([{ project: [:comments] }]) + end - allow(relation).to receive(:count).and_raise('Unexpected scope execution.') + it 'merges :all conditions with other conditions' do + user = User.create! + article = Article.create!(user: user) + ability = Ability.new(user) + ability.can :manage, :all + ability.can :manage, Article, user_id: user.id + expect(Article.accessible_by(ability)).to eq([article]) + end - expect { @ability.can? :read, Article }.not_to raise_error + it 'should not execute a scope when checking ability on the class' do + relation = Article.where(secret: true) + @ability.can :read, Article, relation do |article| + article.secret == true end - it 'should ignore cannot rules with attributes when querying' do - user = User.create! - article = Article.create!(user: user) - ability = Ability.new(user) - ability.can :read, Article - ability.cannot :read, Article, :secret - expect(Article.accessible_by(ability)).to eq([article]) - end + allow(relation).to receive(:count).and_raise('Unexpected scope execution.') - context 'with namespaced models' do - before :each do - ActiveRecord::Schema.define do - create_table(:table_xes) do |t| - t.timestamps null: false - end - - create_table(:table_zs) do |t| - t.integer :table_x_id - t.integer :user_id - t.timestamps null: false - end - end + expect { @ability.can? :read, Article }.not_to raise_error + end - module Namespace - end + it 'should ignore cannot rules with attributes when querying' do + user = User.create! + article = Article.create!(user: user) + ability = Ability.new(user) + ability.can :read, Article + ability.cannot :read, Article, :secret + expect(Article.accessible_by(ability)).to eq([article]) + end - class Namespace::TableX < ActiveRecord::Base - has_many :table_zs + context 'with namespaced models' do + before :each do + ActiveRecord::Schema.define do + create_table(:table_xes) do |t| + t.timestamps null: false end - class Namespace::TableZ < ActiveRecord::Base - belongs_to :table_x - belongs_to :user + create_table(:table_zs) do |t| + t.integer :table_x_id + t.integer :user_id + t.timestamps null: false end end - it 'fetches all namespace::table_x when one is related by table_y' do - user = User.create! + module Namespace + end - ability = Ability.new(user) - ability.can :read, Namespace::TableX, table_zs: { user_id: user.id } + class Namespace::TableX < ActiveRecord::Base + has_many :table_zs + end - table_x = Namespace::TableX.create! - table_x.table_zs.create(user: user) - expect(Namespace::TableX.accessible_by(ability)).to eq([table_x]) + class Namespace::TableZ < ActiveRecord::Base + belongs_to :table_x + belongs_to :user end end - context 'when conditions are non iterable ranges' do - before :each do - ActiveRecord::Schema.define do - create_table(:courses) do |t| - t.datetime :start_at - end - end + it 'fetches all namespace::table_x when one is related by table_y' do + user = User.create! + + ability = Ability.new(user) + ability.can :read, Namespace::TableX, table_zs: { user_id: user.id } - class Course < ActiveRecord::Base + table_x = Namespace::TableX.create! + table_x.table_zs.create(user: user) + expect(Namespace::TableX.accessible_by(ability)).to eq([table_x]) + end + end + + context 'when conditions are non iterable ranges' do + before :each do + ActiveRecord::Schema.define do + create_table(:courses) do |t| + t.datetime :start_at end end - it 'fetches only the valid records' do - @ability.can :read, Course, start_at: 1.day.ago..1.day.from_now - Course.create!(start_at: 10.days.ago) - valid_course = Course.create!(start_at: Time.now) - - expect(Course.accessible_by(@ability)).to eq([valid_course]) + class Course < ActiveRecord::Base end end - context 'when a table references another one twice' do - before do - ActiveRecord::Schema.define do - create_table(:transactions) do |t| - t.integer :sender_id - t.integer :receiver_id - end - end + it 'fetches only the valid records' do + @ability.can :read, Course, start_at: 1.day.ago..1.day.from_now + Course.create!(start_at: 10.days.ago) + valid_course = Course.create!(start_at: Time.now) + + expect(Course.accessible_by(@ability)).to eq([valid_course]) + end + end - class Transaction < ActiveRecord::Base - belongs_to :sender, class_name: 'User', foreign_key: :sender_id - belongs_to :receiver, class_name: 'User', foreign_key: :receiver_id + context 'when a table references another one twice' do + before do + ActiveRecord::Schema.define do + create_table(:transactions) do |t| + t.integer :sender_id + t.integer :receiver_id end end - it 'can filter correctly on both associations' do - sender = User.create! - receiver = User.create! - t1 = Transaction.create!(sender: sender, receiver: receiver) - t2 = Transaction.create!(sender: receiver, receiver: sender) - - ability = Ability.new(sender) - ability.can :read, Transaction, sender: { id: sender.id } - ability.can :read, Transaction, receiver: { id: sender.id } - expect(Transaction.accessible_by(ability)).to eq([t1, t2]) + class Transaction < ActiveRecord::Base + belongs_to :sender, class_name: 'User', foreign_key: :sender_id + belongs_to :receiver, class_name: 'User', foreign_key: :receiver_id end end - context 'when a table is references multiple times' do - it 'can filter correctly on the different associations' do - u1 = User.create!(name: 'pippo') - u2 = User.create!(name: 'paperino') - - a1 = Article.create!(user: u1) - a2 = Article.create!(user: u2) - - ability = Ability.new(u1) - ability.can :read, Article, user: { id: u1.id } - ability.can :read, Article, mentioned_users: { name: u1.name } - ability.can :read, Article, mentioned_users: { mentioned_articles: { id: a2.id } } - ability.can :read, Article, mentioned_users: { articles: { user: { name: 'deep' } } } - ability.can :read, Article, mentioned_users: { articles: { mentioned_users: { name: 'd2' } } } - expect(Article.accessible_by(ability)).to eq([a1]) - end + it 'can filter correctly on both associations' do + sender = User.create! + receiver = User.create! + t1 = Transaction.create!(sender: sender, receiver: receiver) + t2 = Transaction.create!(sender: receiver, receiver: sender) + + ability = Ability.new(sender) + ability.can :read, Transaction, sender: { id: sender.id } + ability.can :read, Transaction, receiver: { id: sender.id } + expect(Transaction.accessible_by(ability)).to eq([t1, t2]) + end + end + + context 'when a table is references multiple times' do + it 'can filter correctly on the different associations' do + u1 = User.create!(name: 'pippo') + u2 = User.create!(name: 'paperino') + + a1 = Article.create!(user: u1) + a2 = Article.create!(user: u2) + + ability = Ability.new(u1) + ability.can :read, Article, user: { id: u1.id } + ability.can :read, Article, mentioned_users: { name: u1.name } + ability.can :read, Article, mentioned_users: { mentioned_articles: { id: a2.id } } + ability.can :read, Article, mentioned_users: { articles: { user: { name: 'deep' } } } + ability.can :read, Article, mentioned_users: { articles: { mentioned_users: { name: 'd2' } } } + expect(Article.accessible_by(ability)).to eq([a1]) end end end diff --git a/spec/cancan/model_adapters/conditions_extractor_spec.rb b/spec/cancan/model_adapters/conditions_extractor_spec.rb index a9caf241..7d05fb41 100644 --- a/spec/cancan/model_adapters/conditions_extractor_spec.rb +++ b/spec/cancan/model_adapters/conditions_extractor_spec.rb @@ -1,149 +1,147 @@ require 'spec_helper' -if defined? CanCan::ModelAdapters::ConditionsExtractor - RSpec.describe CanCan::ModelAdapters::ConditionsExtractor do - before do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define do - create_table(:categories) do |t| - t.string :name - t.boolean :visible - t.timestamps null: false - end - - create_table(:projects) do |t| - t.string :name - t.timestamps null: false - end - - create_table(:articles) do |t| - t.string :name - t.timestamps null: false - t.boolean :published - t.boolean :secret - t.integer :priority - t.integer :category_id - t.integer :user_id - end - - create_table(:comments) do |t| - t.boolean :spam - t.integer :article_id - t.timestamps null: false - end - - create_table(:legacy_mentions) do |t| - t.integer :user_id - t.integer :article_id - t.timestamps null: false - end - - create_table(:users) do |t| - t.timestamps null: false - end - - create_table(:transactions) do |t| - t.integer :sender_id - t.integer :receiver_id - t.integer :supervisor_id - end +RSpec.describe CanCan::ModelAdapters::ConditionsExtractor do + before do + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:categories) do |t| + t.string :name + t.boolean :visible + t.timestamps null: false end - class Project < ActiveRecord::Base + create_table(:projects) do |t| + t.string :name + t.timestamps null: false end - class Category < ActiveRecord::Base - has_many :articles + create_table(:articles) do |t| + t.string :name + t.timestamps null: false + t.boolean :published + t.boolean :secret + t.integer :priority + t.integer :category_id + t.integer :user_id end - class Article < ActiveRecord::Base - belongs_to :category - has_many :comments - has_many :mentions - has_many :mentioned_users, through: :mentions, source: :user - belongs_to :user + create_table(:comments) do |t| + t.boolean :spam + t.integer :article_id + t.timestamps null: false end - class Mention < ActiveRecord::Base - self.table_name = 'legacy_mentions' - belongs_to :user - belongs_to :article + create_table(:legacy_mentions) do |t| + t.integer :user_id + t.integer :article_id + t.timestamps null: false end - class Comment < ActiveRecord::Base - belongs_to :article + create_table(:users) do |t| + t.timestamps null: false end - class User < ActiveRecord::Base - has_many :articles - has_many :mentions - has_many :mentioned_articles, through: :mentions, source: :article + create_table(:transactions) do |t| + t.integer :sender_id + t.integer :receiver_id + t.integer :supervisor_id end + end - class Transaction < ActiveRecord::Base - belongs_to :sender, class_name: 'User', foreign_key: :sender_id - belongs_to :receiver, class_name: 'User', foreign_key: :receiver_id - belongs_to :supervisor, class_name: 'User', foreign_key: :supervisor_id - end + class Project < ActiveRecord::Base end - describe 'converts hash of conditions into database sql where format' do - it 'converts a simple association' do - conditions = described_class.new(User).tableize_conditions(articles: { id: 1 }) - expect(conditions).to eq(articles: { id: 1 }) - end + class Category < ActiveRecord::Base + has_many :articles + end - it 'converts a nested association' do - conditions = described_class.new(User).tableize_conditions(articles: { category: { id: 1 } }) - expect(conditions).to eq(categories: { id: 1 }) - end + class Article < ActiveRecord::Base + belongs_to :category + has_many :comments + has_many :mentions + has_many :mentioned_users, through: :mentions, source: :user + belongs_to :user + end - it 'converts two associations' do - conditions = described_class.new(User).tableize_conditions(articles: { id: 2, category: { id: 1 } }) - expect(conditions).to eq(articles: { id: 2 }, categories: { id: 1 }) - end + class Mention < ActiveRecord::Base + self.table_name = 'legacy_mentions' + belongs_to :user + belongs_to :article + end - it 'converts has_many through' do - conditions = described_class.new(Article).tableize_conditions(mentioned_users: { id: 1 }) - expect(conditions).to eq(users: { id: 1 }) - end + class Comment < ActiveRecord::Base + belongs_to :article + end - it 'converts associations named differently from the table' do - conditions = described_class.new(Transaction).tableize_conditions(sender: { id: 1 }) - expect(conditions).to eq(users: { id: 1 }) - end + class User < ActiveRecord::Base + has_many :articles + has_many :mentions + has_many :mentioned_articles, through: :mentions, source: :article + end - it 'converts associations properly when the same table is referenced twice' do - conditions = described_class.new(Transaction).tableize_conditions(sender: { id: 1 }, receiver: { id: 2 }) - expect(conditions).to eq(users: { id: 1 }, receivers_transactions: { id: 2 }) - end + class Transaction < ActiveRecord::Base + belongs_to :sender, class_name: 'User', foreign_key: :sender_id + belongs_to :receiver, class_name: 'User', foreign_key: :receiver_id + belongs_to :supervisor, class_name: 'User', foreign_key: :supervisor_id + end + end - it 'converts very complex nested sets' do - original_conditions = { user: { id: 1 }, - mentioned_users: { name: 'a name', - mentioned_articles: { id: 2 }, - articles: { user: { name: 'deep' }, - mentioned_users: { name: 'd2' } } } } - - conditions = described_class.new(Article).tableize_conditions(original_conditions) - expect(conditions).to eq(users: { id: 1 }, - mentioned_articles_users: { id: 2 }, - mentioned_users_articles: { name: 'a name' }, - users_articles: { name: 'deep' }, - mentioned_users_articles_2: { name: 'd2' }) - end + describe 'converts hash of conditions into database sql where format' do + it 'converts a simple association' do + conditions = described_class.new(User).tableize_conditions(articles: { id: 1 }) + expect(conditions).to eq(articles: { id: 1 }) + end - it 'converts complex nested sets with duplicates' do - original_conditions = { sender: { id: 'sender', articles: { id: 'article1' } }, - receiver: { id: 'receiver', articles: { id: 'article2' } } } + it 'converts a nested association' do + conditions = described_class.new(User).tableize_conditions(articles: { category: { id: 1 } }) + expect(conditions).to eq(categories: { id: 1 }) + end - conditions = described_class.new(Transaction).tableize_conditions(original_conditions) - expect(conditions).to eq(users: { id: 'sender' }, - articles: { id: 'article1' }, - receivers_transactions: { id: 'receiver' }, - articles_users: { id: 'article2' }) - end + it 'converts two associations' do + conditions = described_class.new(User).tableize_conditions(articles: { id: 2, category: { id: 1 } }) + expect(conditions).to eq(articles: { id: 2 }, categories: { id: 1 }) + end + + it 'converts has_many through' do + conditions = described_class.new(Article).tableize_conditions(mentioned_users: { id: 1 }) + expect(conditions).to eq(users: { id: 1 }) + end + + it 'converts associations named differently from the table' do + conditions = described_class.new(Transaction).tableize_conditions(sender: { id: 1 }) + expect(conditions).to eq(users: { id: 1 }) + end + + it 'converts associations properly when the same table is referenced twice' do + conditions = described_class.new(Transaction).tableize_conditions(sender: { id: 1 }, receiver: { id: 2 }) + expect(conditions).to eq(users: { id: 1 }, receivers_transactions: { id: 2 }) + end + + it 'converts very complex nested sets' do + original_conditions = { user: { id: 1 }, + mentioned_users: { name: 'a name', + mentioned_articles: { id: 2 }, + articles: { user: { name: 'deep' }, + mentioned_users: { name: 'd2' } } } } + + conditions = described_class.new(Article).tableize_conditions(original_conditions) + expect(conditions).to eq(users: { id: 1 }, + mentioned_articles_users: { id: 2 }, + mentioned_users_articles: { name: 'a name' }, + users_articles: { name: 'deep' }, + mentioned_users_articles_2: { name: 'd2' }) + end + + it 'converts complex nested sets with duplicates' do + original_conditions = { sender: { id: 'sender', articles: { id: 'article1' } }, + receiver: { id: 'receiver', articles: { id: 'article2' } } } + + conditions = described_class.new(Transaction).tableize_conditions(original_conditions) + expect(conditions).to eq(users: { id: 'sender' }, + articles: { id: 'article1' }, + receivers_transactions: { id: 'receiver' }, + articles_users: { id: 'article2' }) end end end From 6b2457ea9d798f1748852189adb10270fc3f2707 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 2 Feb 2019 19:18:22 +0100 Subject: [PATCH 29/54] Add integration tests --- cancancan.gemspec | 2 +- .../accessible_by_integration_spec.rb | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 spec/cancan/model_adapters/accessible_by_integration_spec.rb diff --git a/cancancan.gemspec b/cancancan.gemspec index 05716bc5..f90de120 100644 --- a/cancancan.gemspec +++ b/cancancan.gemspec @@ -22,5 +22,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'bundler', '~> 2.0' s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1' s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0' - s.add_development_dependency 'rubocop', '~> 0.59.2' + s.add_development_dependency 'rubocop', '~> 0.63.1' end diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb new file mode 100644 index 00000000..addad35c --- /dev/null +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +# integration tests for latest ActiveRecord version. +describe CanCan::ModelAdapters::ActiveRecord5Adapter do + let(:ability) { double.extend(CanCan::Ability) } + let(:users_table) { Post.table_name } + let(:posts_table) { Post.table_name } + let(:likes_table) { Like.table_name } + before :each do + ActiveRecord::Base.logger = Logger.new(STDOUT) + + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + ActiveRecord::Migration.verbose = false + + ActiveRecord::Schema.define do + create_table(:users) do |t| + t.string :name + t.timestamps null: false + end + + create_table(:posts) do |t| + t.string :title + t.boolean :published, default: true + t.integer :user_id + t.timestamps null: false + end + + create_table(:likes) do |t| + t.integer :post_id + t.integer :user_id + t.timestamps null: false + end + + create_table(:editors) do |t| + t.integer :post_id + t.integer :user_id + t.timestamps null: false + end + end + + class User < ActiveRecord::Base + has_many :posts + has_many :likes + has_many :editors + end + + class Post < ActiveRecord::Base + belongs_to :user + has_many :likes + has_many :editors + end + + class Like < ActiveRecord::Base + belongs_to :user + belongs_to :post + end + + class Editor < ActiveRecord::Base + belongs_to :user + belongs_to :post + end + end + + before do + @user1 = User.create! + @user2 = User.create! + @post1 = Post.create!(title: 'post1', user: @user1) + @post2 = Post.create!(user: @user1, published: false) + @post3 = Post.create!(user: @user2) + @like1 = Like.create!(post: @post1, user: @user1) + @like2 = Like.create!(post: @post1, user: @user2) + @editor1 = Editor.create(user: @user1, post: @post2) + ability.can :read, Post, user_id: @user1 + ability.can :read, Post, editors: { user_id: @user1 } + end + + describe 'preloading of associatons' do + it 'preloads associations correctly' do + posts = Post.accessible_by(ability).includes(likes: :user) + expect(posts[0].association(:likes)).to be_loaded + expect(posts[0].likes[0].association(:user)).to be_loaded + end + end + + describe 'filtering of results' do + it 'adds the where clause correctly' do + posts = Post.accessible_by(ability).where(published: true) + expect(posts.length).to eq 1 + end + end + + describe 'selecting custom columns' do + # TODO: it currently overrides + xit 'extracts custom columns correctly' do + posts = Post.accessible_by(ability).select('title as mytitle') + expect(posts[0].mytitle).to eq 'post1' + end + end +end From 98b2b511b4b47f88ae6efa377d75fa9a5ffed601 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 3 Feb 2019 09:28:34 +0100 Subject: [PATCH 30/54] Update Travis matrix (#564) Update Travis and Appraisal configurations --- .rubocop.yml | 2 +- .travis.yml | 7 +++---- Appraisals | 2 +- gemfiles/activerecord_4.2.0.gemfile | 20 ++++++++++++++++++++ gemfiles/activerecord_4.2.gemfile | 20 -------------------- gemfiles/activerecord_5.0.2.gemfile | 18 +++++++++--------- gemfiles/activerecord_5.0.gemfile | 19 ------------------- gemfiles/activerecord_5.1.0.gemfile | 18 +++++++++--------- gemfiles/activerecord_5.2.0.gemfile | 19 ------------------- gemfiles/activerecord_5.2.1.gemfile | 18 +++++++++--------- 10 files changed, 52 insertions(+), 91 deletions(-) create mode 100644 gemfiles/activerecord_4.2.0.gemfile delete mode 100644 gemfiles/activerecord_4.2.gemfile delete mode 100644 gemfiles/activerecord_5.0.gemfile delete mode 100644 gemfiles/activerecord_5.2.0.gemfile diff --git a/.rubocop.yml b/.rubocop.yml index cd03d3dc..34085479 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -39,7 +39,7 @@ Lint/AmbiguousBlockAssociation: AllCops: TargetRubyVersion: 2.2.0 Exclude: - - 'gemfiles/vendor/bundle/**/*' + - 'gemfiles/**/*' - 'Appraisals' inherit_from: .rubocop_todo.yml diff --git a/.travis.yml b/.travis.yml index 2b4c9ae2..41ee3bd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,16 +4,15 @@ sudo: false rvm: - 2.3.5 - 2.4.2 - - 2.5.0 - 2.5.1 - 2.6.0 - jruby-9.1.9.0 - jruby-9.2.5.0 gemfile: - - gemfiles/activerecord_4.2.gemfile + - gemfiles/activerecord_4.2.0.gemfile - gemfiles/activerecord_5.0.2.gemfile - gemfiles/activerecord_5.1.0.gemfile - - gemfiles/activerecord_5.2.0.gemfile + - gemfiles/activerecord_5.2.1.gemfile services: - mongodb matrix: @@ -24,7 +23,7 @@ matrix: - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.1.0.gemfile - rvm: jruby-9.1.9.0 - gemfile: gemfiles/activerecord_5.2.0.gemfile + gemfile: gemfiles/activerecord_5.2.1.gemfile - rvm: jruby-9.2.5.0 gemfile: gemfiles/activerecord_5.0.2.gemfile notifications: diff --git a/Appraisals b/Appraisals index b332afe0..f664eefe 100644 --- a/Appraisals +++ b/Appraisals @@ -1,4 +1,4 @@ -appraise 'activerecord_4.2' do +appraise 'activerecord_4.2.0' do gem 'activerecord', '~> 4.2.0', require: 'active_record' gem 'activesupport', '~> 4.2.0', require: 'active_support/all' gem 'actionpack', '~> 4.2.0', require: 'action_pack' diff --git a/gemfiles/activerecord_4.2.0.gemfile b/gemfiles/activerecord_4.2.0.gemfile new file mode 100644 index 00000000..9d7a4b7f --- /dev/null +++ b/gemfiles/activerecord_4.2.0.gemfile @@ -0,0 +1,20 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activerecord", "~> 4.2.0", require: "active_record" +gem "activesupport", "~> 4.2.0", require: "active_support/all" +gem "actionpack", "~> 4.2.0", require: "action_pack" +gem "nokogiri", "~> 1.6.8", require: "nokogiri" + +platforms :jruby do + gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.24" + gem "jdbc-sqlite3" +end + +platforms :ruby, :mswin, :mingw do + gem "sqlite3" + gem "pg", "~> 0.21" +end + +gemspec path: "../" diff --git a/gemfiles/activerecord_4.2.gemfile b/gemfiles/activerecord_4.2.gemfile deleted file mode 100644 index feca9d34..00000000 --- a/gemfiles/activerecord_4.2.gemfile +++ /dev/null @@ -1,20 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 4.2.0', require: 'action_pack' -gem 'activerecord', '~> 4.2.0', require: 'active_record' -gem 'activesupport', '~> 4.2.0', require: 'active_support/all' -gem 'nokogiri', '~> 1.6.8', require: 'nokogiri' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.24' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' diff --git a/gemfiles/activerecord_5.0.2.gemfile b/gemfiles/activerecord_5.0.2.gemfile index 40413f0f..40f67ff6 100644 --- a/gemfiles/activerecord_5.0.2.gemfile +++ b/gemfiles/activerecord_5.0.2.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.0.2', require: 'action_pack' -gem 'activerecord', '~> 5.0.2', require: 'active_record' -gem 'activesupport', '~> 5.0.2', require: 'active_support/all' +gem "activerecord", "~> 5.0.2", require: "active_record" +gem "activesupport", "~> 5.0.2", require: "active_support/all" +gem "actionpack", "~> 5.0.2", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_5.0.gemfile b/gemfiles/activerecord_5.0.gemfile deleted file mode 100644 index e50f52b5..00000000 --- a/gemfiles/activerecord_5.0.gemfile +++ /dev/null @@ -1,19 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 5.0.0.rc1', require: 'action_pack' -gem 'activerecord', '~> 5.0.0.rc1', require: 'active_record' -gem 'activesupport', '~> 5.0.0.rc1', require: 'active_support/all' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' diff --git a/gemfiles/activerecord_5.1.0.gemfile b/gemfiles/activerecord_5.1.0.gemfile index a9b1d41c..69f242d3 100644 --- a/gemfiles/activerecord_5.1.0.gemfile +++ b/gemfiles/activerecord_5.1.0.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.1.0', require: 'action_pack' -gem 'activerecord', '~> 5.1.0', require: 'active_record' -gem 'activesupport', '~> 5.1.0', require: 'active_support/all' +gem "activerecord", "~> 5.1.0", require: "active_record" +gem "activesupport", "~> 5.1.0", require: "active_support/all" +gem "actionpack", "~> 5.1.0", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/activerecord_5.2.0.gemfile b/gemfiles/activerecord_5.2.0.gemfile deleted file mode 100644 index 70192ab1..00000000 --- a/gemfiles/activerecord_5.2.0.gemfile +++ /dev/null @@ -1,19 +0,0 @@ -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'actionpack', '~> 5.2.0', require: 'action_pack' -gem 'activerecord', '~> 5.2.0', require: 'active_record' -gem 'activesupport', '~> 5.2.0', require: 'active_support/all' - -platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' -end - -platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' -end - -gemspec path: '../' diff --git a/gemfiles/activerecord_5.2.1.gemfile b/gemfiles/activerecord_5.2.1.gemfile index e08f8896..432586ea 100644 --- a/gemfiles/activerecord_5.2.1.gemfile +++ b/gemfiles/activerecord_5.2.1.gemfile @@ -1,19 +1,19 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'actionpack', '~> 5.2.1', require: 'action_pack' -gem 'activerecord', '~> 5.2.1', require: 'active_record' -gem 'activesupport', '~> 5.2.1', require: 'active_support/all' +gem "activerecord", "~> 5.2.1", require: "active_record" +gem "activesupport", "~> 5.2.1", require: "active_support/all" +gem "actionpack", "~> 5.2.1", require: "action_pack" platforms :jruby do - gem 'activerecord-jdbcsqlite3-adapter' - gem 'jdbc-sqlite3' + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" end platforms :ruby, :mswin, :mingw do - gem 'pg', '~> 0.21' - gem 'sqlite3' + gem "sqlite3" + gem "pg", "~> 0.21" end -gemspec path: '../' +gemspec path: "../" From b0e1167844b70003379d6b85aac0186ae6202b7c Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 3 Feb 2019 09:32:10 +0100 Subject: [PATCH 31/54] add TODO --- spec/cancan/model_adapters/accessible_by_integration_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb index addad35c..22a2a80a 100644 --- a/spec/cancan/model_adapters/accessible_by_integration_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -90,7 +90,7 @@ class Editor < ActiveRecord::Base end describe 'selecting custom columns' do - # TODO: it currently overrides + # TODO: it currently overrides the select statement. 3.0.0 fixes it. xit 'extracts custom columns correctly' do posts = Post.accessible_by(ability).select('title as mytitle') expect(posts[0].mytitle).to eq 'post1' From bbb5d7408983a443730816ff6750320267f248ba Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 3 Feb 2019 10:10:57 +0100 Subject: [PATCH 32/54] Update integration tests --- spec/README.md | 19 +++++++++++++ spec/README.rdoc | 27 ------------------- .../accessible_by_integration_spec.rb | 7 ++--- 3 files changed, 21 insertions(+), 32 deletions(-) create mode 100644 spec/README.md delete mode 100644 spec/README.rdoc diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 00000000..071cb1ac --- /dev/null +++ b/spec/README.md @@ -0,0 +1,19 @@ += CanCanCan Specs + +== Running the specs + +To run the specs first run the +bundle+ command to install the necessary gems. + + bundle + +Then run the appraisal command to install all the necessary test sets. + + bundle exec appraisal install + +You can then run all test sets: + + bundle exec appraisal rspec + +Or individual ones: + + bundle exec appraisal activerecord_5.2.0 rspec diff --git a/spec/README.rdoc b/spec/README.rdoc deleted file mode 100644 index df03a789..00000000 --- a/spec/README.rdoc +++ /dev/null @@ -1,27 +0,0 @@ -= CanCan Specs - -== Running the specs - -To run the specs first run the +bundle+ command to install the necessary gems and the +rake+ command to run the specs. - - bundle - -Then run the appraisal command to install all the necessary test sets: - - appraisal install - -You can then run all test sets: - - appraisal rake - -Or individual ones: - - appraisal activerecord_5.2.0 rake - -A list of the tests is in the +Appraisal+ file. - -The specs support Ruby 2.3+ - -== Model Adapters - -The model adapter ENV setting has been removed and replaced with the +Appraisal+ file. diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb index 22a2a80a..f50037fb 100644 --- a/spec/cancan/model_adapters/accessible_by_integration_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -1,14 +1,12 @@ require 'spec_helper' # integration tests for latest ActiveRecord version. -describe CanCan::ModelAdapters::ActiveRecord5Adapter do +RSpec.describe CanCan::ModelAdapters::ActiveRecord5Adapter do let(:ability) { double.extend(CanCan::Ability) } let(:users_table) { Post.table_name } let(:posts_table) { Post.table_name } let(:likes_table) { Like.table_name } before :each do - ActiveRecord::Base.logger = Logger.new(STDOUT) - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') ActiveRecord::Migration.verbose = false @@ -90,8 +88,7 @@ class Editor < ActiveRecord::Base end describe 'selecting custom columns' do - # TODO: it currently overrides the select statement. 3.0.0 fixes it. - xit 'extracts custom columns correctly' do + it 'extracts custom columns correctly' do posts = Post.accessible_by(ability).select('title as mytitle') expect(posts[0].mytitle).to eq 'post1' end From 54f307da46747c04561e555b40c6912b3fde6a48 Mon Sep 17 00:00:00 2001 From: nyamadori Date: Tue, 21 Nov 2017 18:39:39 +0900 Subject: [PATCH 33/54] Support translation of model_name Add spec example Add translations in another language Use I18n#with_locale instead of I18n#default_locale= --- lib/cancan/ability.rb | 11 ++++++++++- spec/cancan/ability_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 25117c3d..730a1ca0 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -178,11 +178,20 @@ def authorize!(action, subject, *args) def unauthorized_message(action, subject) keys = unauthorized_message_keys(action, subject) variables = { action: action.to_s } - variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase + variables[:subject] = translate_subject(subject) message = I18n.translate(keys.shift, variables.merge(scope: :unauthorized, default: keys + [''])) message.blank? ? nil : message end + def translate_subject(subject) + klass = (subject.class == Class ? subject : subject.class) + if klass.respond_to?(:model_name) + klass.model_name.human + else + klass.to_s.underscore.humanize.downcase + end + end + def attributes_for(action, subject) attributes = {} relevant_rules(action, subject).map do |rule| diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 4aad3d00..1de7a26e 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -521,6 +521,27 @@ class Container < Hash expect(@ability.unauthorized_message(:update, :missing)).to be_nil end + it "uses model's name in i18n" do + class Account + include ActiveModel::Model + end + + I18n.backend.store_translations :en, + activemodel: { models: { account: 'english name' } }, + unauthorized: { update: { all: '%{subject}' } } + I18n.backend.store_translations :ja, + activemodel: { models: { account: 'japanese name' } }, + unauthorized: { update: { all: '%{subject}' } } + + I18n.with_locale(:en) do + expect(@ability.unauthorized_message(:update, Account)).to eq('english name') + end + + I18n.with_locale(:ja) do + expect(@ability.unauthorized_message(:update, Account)).to eq('japanese name') + end + end + it 'uses symbol as subject directly' do I18n.backend.store_translations :en, unauthorized: { has: { cheezburger: 'Nom nom nom. I eated it.' } } expect(@ability.unauthorized_message(:has, :cheezburger)).to eq('Nom nom nom. I eated it.') From 9ee810daa644b9b82a70cab152be24224d1d638a Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 3 Feb 2019 10:30:18 +0100 Subject: [PATCH 34/54] Fix rubocop --- lib/cancan/ability.rb | 20 +++----------------- lib/cancan/unauthorized_message_resolver.rb | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 lib/cancan/unauthorized_message_resolver.rb diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 730a1ca0..af3b9dd3 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -1,5 +1,7 @@ require_relative 'ability/rules.rb' require_relative 'ability/actions.rb' +require_relative 'unauthorized_message_resolver.rb' + module CanCan # This module is designed to be included into an Ability class. This will # provide the "can" methods for defining and checking abilities. @@ -19,6 +21,7 @@ module CanCan module Ability include CanCan::Ability::Rules include CanCan::Ability::Actions + include CanCan::UnauthorizedMessageResolver # Check if the user has permission to perform a given action on an object. # @@ -175,23 +178,6 @@ def authorize!(action, subject, *args) subject end - def unauthorized_message(action, subject) - keys = unauthorized_message_keys(action, subject) - variables = { action: action.to_s } - variables[:subject] = translate_subject(subject) - message = I18n.translate(keys.shift, variables.merge(scope: :unauthorized, default: keys + [''])) - message.blank? ? nil : message - end - - def translate_subject(subject) - klass = (subject.class == Class ? subject : subject.class) - if klass.respond_to?(:model_name) - klass.model_name.human - else - klass.to_s.underscore.humanize.downcase - end - end - def attributes_for(action, subject) attributes = {} relevant_rules(action, subject).map do |rule| diff --git a/lib/cancan/unauthorized_message_resolver.rb b/lib/cancan/unauthorized_message_resolver.rb new file mode 100644 index 00000000..4063387f --- /dev/null +++ b/lib/cancan/unauthorized_message_resolver.rb @@ -0,0 +1,20 @@ +module CanCan + module UnauthorizedMessageResolver + def unauthorized_message(action, subject) + keys = unauthorized_message_keys(action, subject) + variables = { action: action.to_s } + variables[:subject] = translate_subject(subject) + message = I18n.translate(keys.shift, variables.merge(scope: :unauthorized, default: keys + [''])) + message.blank? ? nil : message + end + + def translate_subject(subject) + klass = (subject.class == Class ? subject : subject.class) + if klass.respond_to?(:model_name) + klass.model_name.human + else + klass.to_s.underscore.humanize.downcase + end + end + end +end From ce2203061f3737ec416b93d6d057520df4bed7cd Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 3 Feb 2019 10:38:44 +0100 Subject: [PATCH 35/54] Add CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c792f0d7..3658b301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * [#444](https://github.com/CanCanCommunity/cancancan/issues/444): Allow to use symbols when defining conditions over enums. ([@s-mage][]) * [#538](https://github.com/CanCanCommunity/cancancan/issues/538): Merge alias actions when merging abilities. ([@Jcambass][]) +* [#462](https://github.com/CanCanCommunity/cancancan/issues/462): Add support to translate the model name in messages. ([@nyamadori][]) ## 2.3.0 (Sep 16th, 2018) @@ -622,3 +623,4 @@ [@timraymond]: https://github.com/timraymond [@s-mage]: https://github.com/s-mage [@Jcambass]: https://github.com/Jcambass +[@nyamadori]: https://github.com/nyamadori From 91ef8f72d1be3672590312350d1018529b38ba54 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 3 Feb 2019 14:37:12 +0100 Subject: [PATCH 36/54] Fix integration tests for active record 4 --- .../model_adapters/accessible_by_integration_spec.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb index f50037fb..a457b87e 100644 --- a/spec/cancan/model_adapters/accessible_by_integration_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -87,10 +87,12 @@ class Editor < ActiveRecord::Base end end - describe 'selecting custom columns' do - it 'extracts custom columns correctly' do - posts = Post.accessible_by(ability).select('title as mytitle') - expect(posts[0].mytitle).to eq 'post1' + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + describe 'selecting custom columns' do + it 'extracts custom columns correctly' do + posts = Post.accessible_by(ability).select('title as mytitle') + expect(posts[0].mytitle).to eq 'post1' + end end end end From 7dee204c80f98ae85464fc54a20b55092c95329d Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 10 Feb 2019 19:02:34 +0100 Subject: [PATCH 37/54] Lock sqlite version to 1.3 (#565) --- .travis.yml | 4 ++-- Appraisals | 16 ++++++++-------- gemfiles/activerecord_4.2.0.gemfile | 2 +- gemfiles/activerecord_5.0.2.gemfile | 2 +- gemfiles/activerecord_5.1.0.gemfile | 2 +- ..._5.2.1.gemfile => activerecord_5.2.2.gemfile} | 8 ++++---- 6 files changed, 17 insertions(+), 17 deletions(-) rename gemfiles/{activerecord_5.2.1.gemfile => activerecord_5.2.2.gemfile} (54%) diff --git a/.travis.yml b/.travis.yml index 41ee3bd1..df571893 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ gemfile: - gemfiles/activerecord_4.2.0.gemfile - gemfiles/activerecord_5.0.2.gemfile - gemfiles/activerecord_5.1.0.gemfile - - gemfiles/activerecord_5.2.1.gemfile + - gemfiles/activerecord_5.2.2.gemfile services: - mongodb matrix: @@ -23,7 +23,7 @@ matrix: - rvm: jruby-9.1.9.0 gemfile: gemfiles/activerecord_5.1.0.gemfile - rvm: jruby-9.1.9.0 - gemfile: gemfiles/activerecord_5.2.1.gemfile + gemfile: gemfiles/activerecord_5.2.2.gemfile - rvm: jruby-9.2.5.0 gemfile: gemfiles/activerecord_5.0.2.gemfile notifications: diff --git a/Appraisals b/Appraisals index f664eefe..1d7f02f7 100644 --- a/Appraisals +++ b/Appraisals @@ -10,7 +10,7 @@ appraise 'activerecord_4.2.0' do end gemfile.platforms :ruby, :mswin, :mingw do - gem 'sqlite3' + gem 'sqlite3', '~> 1.3.0' gem 'pg', '~> 0.21' end end @@ -26,7 +26,7 @@ appraise 'activerecord_5.0.2' do end gemfile.platforms :ruby, :mswin, :mingw do - gem 'sqlite3' + gem 'sqlite3', '~> 1.3.0' gem 'pg', '~> 0.21' end end @@ -42,15 +42,15 @@ appraise 'activerecord_5.1.0' do end gemfile.platforms :ruby, :mswin, :mingw do - gem 'sqlite3' + gem 'sqlite3', '~> 1.3.0' gem 'pg', '~> 0.21' end end -appraise 'activerecord_5.2.1' do - gem 'activerecord', '~> 5.2.1', require: 'active_record' - gem 'activesupport', '~> 5.2.1', require: 'active_support/all' - gem 'actionpack', '~> 5.2.1', require: 'action_pack' +appraise 'activerecord_5.2.2' do + gem 'activerecord', '~> 5.2.2', require: 'active_record' + gem 'activesupport', '~> 5.2.2', require: 'active_support/all' + gem 'actionpack', '~> 5.2.2', require: 'action_pack' gemfile.platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' @@ -58,7 +58,7 @@ appraise 'activerecord_5.2.1' do end gemfile.platforms :ruby, :mswin, :mingw do - gem 'sqlite3' + gem 'sqlite3', '~> 1.3.0' gem 'pg', '~> 0.21' end end diff --git a/gemfiles/activerecord_4.2.0.gemfile b/gemfiles/activerecord_4.2.0.gemfile index 9d7a4b7f..ba98041f 100644 --- a/gemfiles/activerecord_4.2.0.gemfile +++ b/gemfiles/activerecord_4.2.0.gemfile @@ -13,7 +13,7 @@ platforms :jruby do end platforms :ruby, :mswin, :mingw do - gem "sqlite3" + gem "sqlite3", "~> 1.3.0" gem "pg", "~> 0.21" end diff --git a/gemfiles/activerecord_5.0.2.gemfile b/gemfiles/activerecord_5.0.2.gemfile index 40f67ff6..168bba4c 100644 --- a/gemfiles/activerecord_5.0.2.gemfile +++ b/gemfiles/activerecord_5.0.2.gemfile @@ -12,7 +12,7 @@ platforms :jruby do end platforms :ruby, :mswin, :mingw do - gem "sqlite3" + gem "sqlite3", "~> 1.3.0" gem "pg", "~> 0.21" end diff --git a/gemfiles/activerecord_5.1.0.gemfile b/gemfiles/activerecord_5.1.0.gemfile index 69f242d3..93e2a0a0 100644 --- a/gemfiles/activerecord_5.1.0.gemfile +++ b/gemfiles/activerecord_5.1.0.gemfile @@ -12,7 +12,7 @@ platforms :jruby do end platforms :ruby, :mswin, :mingw do - gem "sqlite3" + gem "sqlite3", "~> 1.3.0" gem "pg", "~> 0.21" end diff --git a/gemfiles/activerecord_5.2.1.gemfile b/gemfiles/activerecord_5.2.2.gemfile similarity index 54% rename from gemfiles/activerecord_5.2.1.gemfile rename to gemfiles/activerecord_5.2.2.gemfile index 432586ea..15abe446 100644 --- a/gemfiles/activerecord_5.2.1.gemfile +++ b/gemfiles/activerecord_5.2.2.gemfile @@ -2,9 +2,9 @@ source "https://rubygems.org" -gem "activerecord", "~> 5.2.1", require: "active_record" -gem "activesupport", "~> 5.2.1", require: "active_support/all" -gem "actionpack", "~> 5.2.1", require: "action_pack" +gem "activerecord", "~> 5.2.2", require: "active_record" +gem "activesupport", "~> 5.2.2", require: "active_support/all" +gem "actionpack", "~> 5.2.2", require: "action_pack" platforms :jruby do gem "activerecord-jdbcsqlite3-adapter" @@ -12,7 +12,7 @@ platforms :jruby do end platforms :ruby, :mswin, :mingw do - gem "sqlite3" + gem "sqlite3", "~> 1.3.0" gem "pg", "~> 0.21" end From 8054624191fec63d1215f25af43d7870476465c7 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 10 Feb 2019 19:05:55 +0100 Subject: [PATCH 38/54] Update CHANGELOG --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e213a12..d5054d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 3.0.0 * [#560](https://github.com/CanCanCommunity/cancancan/pull/560): Add support for Rails 6.0. ([@coorasse][]) * [#489](https://github.com/CanCanCommunity/cancancan/pull/489): Drop support for actions without a subject. ([@andrew-aladev][]) @@ -9,6 +9,8 @@ * [#538](https://github.com/CanCanCommunity/cancancan/issues/538): Merge alias actions when merging abilities. ([@Jcambass][]) * [#462](https://github.com/CanCanCommunity/cancancan/issues/462): Add support to translate the model name in messages. ([@nyamadori][]) +Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.com/CanCanCommunity/cancancan/wiki/Migrating-from-CanCanCan-2.x-to-3.0) + ## 2.3.0 (Sep 16th, 2018) * [#528](https://github.com/CanCanCommunity/cancancan/issues/528): Compress irrelevant rules before generating a query to optimize performances. ([@coorasse][]) From a68b21f02b1184702293f8c351452bf93f59c2ce Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Thu, 21 Feb 2019 19:31:22 +0100 Subject: [PATCH 39/54] avoid multiple queries on error pages (#566) --- lib/cancan/rule.rb | 7 +++++++ spec/cancan/rule_spec.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 6ba747fe..fb8881ff 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -29,6 +29,13 @@ def initialize(base_behavior, action, subject, *extra_args, &block) @block = block end + def inspect + repr = "#<#{self.class.name}" + repr << "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}" + repr << @conditions.inspect.to_s if [Hash, String].include?(@conditions.class) + repr << '>' + end + def can_rule? base_behavior end diff --git a/spec/cancan/rule_spec.rb b/spec/cancan/rule_spec.rb index 7d0966df..0f467bc1 100644 --- a/spec/cancan/rule_spec.rb +++ b/spec/cancan/rule_spec.rb @@ -58,4 +58,35 @@ expect(rule2.attributes).to eq [] expect(rule2.conditions).to eq %i[foo bar] end + + describe '#inspect' do + def count_queries(&block) + count = 0 + counter_f = lambda { |_name, _started, _finished, _unique_id, payload| + count += 1 unless payload[:name].in? %w[CACHE SCHEMA] + } + ActiveSupport::Notifications.subscribed(counter_f, 'sql.active_record', &block) + count + end + + before do + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:watermelons) do |t| + t.boolean :visible + end + end + + class Watermelon < ActiveRecord::Base + scope :visible, -> { where(visible: true) } + end + end + + it 'does not evaluate the conditions when they are scopes' do + rule = CanCan::Rule.new(true, :read, Watermelon, Watermelon.visible, {}, {}) + count = count_queries { rule.inspect } + expect(count).to eq 0 + end + end end From d619a86f7d722655661ce85c62e0d3aeac3bf68a Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 10 Feb 2019 21:22:58 +0100 Subject: [PATCH 40/54] Configure a db matrix --- .travis.yml | 35 +++++++------- Appraisals | 5 ++ gemfiles/activerecord_4.2.0.gemfile | 1 + gemfiles/activerecord_5.0.2.gemfile | 1 + gemfiles/activerecord_5.1.0.gemfile | 1 + gemfiles/activerecord_5.2.2.gemfile | 1 + gemfiles/activerecord_6.0.0.gemfile | 1 + .../accessible_by_integration_spec.rb | 2 +- .../active_record_4_adapter_spec.rb | 12 +---- .../active_record_5_adapter_spec.rb | 2 +- .../active_record_adapter_spec.rb | 46 ++++++++----------- .../conditions_extractor_spec.rb | 2 +- spec/support/sql_helpers.rb | 23 ++++++++++ 13 files changed, 74 insertions(+), 58 deletions(-) diff --git a/.travis.yml b/.travis.yml index b6dd6a10..6818e71e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: ruby cache: bundler sudo: false +addons: + postgresql: "9.6" rvm: - 2.3.5 - 2.4.2 @@ -17,28 +19,25 @@ gemfile: - gemfiles/activerecord_5.1.0.gemfile - gemfiles/activerecord_5.2.2.gemfile - gemfiles/activerecord_6.0.0.gemfile -services: - - mongodb +env: + - DB=sqlite + - DB=postgres matrix: fast_finish: true exclude: - - rvm: 2.3.5 - gemfile: gemfiles/activerecord_6.0.0.gemfile - - rvm: 2.4.2 - gemfile: gemfiles/activerecord_6.0.0.gemfile - - rvm: jruby-9.1.9.0 - gemfile: gemfiles/activerecord_5.0.2.gemfile - - rvm: jruby-9.1.9.0 - gemfile: gemfiles/activerecord_5.1.0.gemfile - - rvm: jruby-9.1.9.0 - gemfile: gemfiles/activerecord_5.2.2.gemfile - - rvm: jruby-9.1.9.0 - gemfile: gemfiles/activerecord_6.0.0.gemfile - - rvm: jruby-9.2.5.0 - gemfile: gemfiles/activerecord_5.0.2.gemfile - - rvm: jruby-9.2.5.0 - gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: 2.3.5 + gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: 2.4.2 + gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: jruby-9.1.9.0 + gemfile: gemfiles/activerecord_5.0.2.gemfile + - rvm: jruby-9.1.9.0 + gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: jruby-9.2.5.0 + gemfile: gemfiles/activerecord_5.0.2.gemfile + - rvm: jruby-9.2.5.0 + gemfile: gemfiles/activerecord_6.0.0.gemfile allow_failures: - rvm: ruby-head - rvm: jruby-head diff --git a/Appraisals b/Appraisals index 62dcb46b..bd574c1f 100644 --- a/Appraisals +++ b/Appraisals @@ -7,6 +7,7 @@ appraise 'activerecord_4.2.0' do gemfile.platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.24' gem 'jdbc-sqlite3' + gem 'jdbc-postgres' end gemfile.platforms :ruby, :mswin, :mingw do @@ -23,6 +24,7 @@ appraise 'activerecord_5.0.2' do gemfile.platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' gem 'jdbc-sqlite3' + gem 'jdbc-postgres' end gemfile.platforms :ruby, :mswin, :mingw do @@ -39,6 +41,7 @@ appraise 'activerecord_5.1.0' do gemfile.platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' gem 'jdbc-sqlite3' + gem 'jdbc-postgres' end gemfile.platforms :ruby, :mswin, :mingw do @@ -55,6 +58,7 @@ appraise 'activerecord_5.2.2' do gemfile.platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' gem 'jdbc-sqlite3' + gem 'jdbc-postgres' end gemfile.platforms :ruby, :mswin, :mingw do @@ -71,6 +75,7 @@ appraise 'activerecord_6.0.0' do platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' gem 'jdbc-sqlite3' + gem 'jdbc-postgres' end platforms :ruby, :mswin, :mingw do diff --git a/gemfiles/activerecord_4.2.0.gemfile b/gemfiles/activerecord_4.2.0.gemfile index ba98041f..f564b76c 100644 --- a/gemfiles/activerecord_4.2.0.gemfile +++ b/gemfiles/activerecord_4.2.0.gemfile @@ -10,6 +10,7 @@ gem "nokogiri", "~> 1.6.8", require: "nokogiri" platforms :jruby do gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.24" gem "jdbc-sqlite3" + gem "jdbc-postgres" end platforms :ruby, :mswin, :mingw do diff --git a/gemfiles/activerecord_5.0.2.gemfile b/gemfiles/activerecord_5.0.2.gemfile index 168bba4c..bec9ffcf 100644 --- a/gemfiles/activerecord_5.0.2.gemfile +++ b/gemfiles/activerecord_5.0.2.gemfile @@ -9,6 +9,7 @@ gem "actionpack", "~> 5.0.2", require: "action_pack" platforms :jruby do gem "activerecord-jdbcsqlite3-adapter" gem "jdbc-sqlite3" + gem "jdbc-postgres" end platforms :ruby, :mswin, :mingw do diff --git a/gemfiles/activerecord_5.1.0.gemfile b/gemfiles/activerecord_5.1.0.gemfile index 93e2a0a0..ff8f083b 100644 --- a/gemfiles/activerecord_5.1.0.gemfile +++ b/gemfiles/activerecord_5.1.0.gemfile @@ -9,6 +9,7 @@ gem "actionpack", "~> 5.1.0", require: "action_pack" platforms :jruby do gem "activerecord-jdbcsqlite3-adapter" gem "jdbc-sqlite3" + gem "jdbc-postgres" end platforms :ruby, :mswin, :mingw do diff --git a/gemfiles/activerecord_5.2.2.gemfile b/gemfiles/activerecord_5.2.2.gemfile index 15abe446..1389b1b7 100644 --- a/gemfiles/activerecord_5.2.2.gemfile +++ b/gemfiles/activerecord_5.2.2.gemfile @@ -9,6 +9,7 @@ gem "actionpack", "~> 5.2.2", require: "action_pack" platforms :jruby do gem "activerecord-jdbcsqlite3-adapter" gem "jdbc-sqlite3" + gem "jdbc-postgres" end platforms :ruby, :mswin, :mingw do diff --git a/gemfiles/activerecord_6.0.0.gemfile b/gemfiles/activerecord_6.0.0.gemfile index 56546ffa..13f124a2 100644 --- a/gemfiles/activerecord_6.0.0.gemfile +++ b/gemfiles/activerecord_6.0.0.gemfile @@ -9,6 +9,7 @@ gem "activesupport", "~> 6.0.0.beta1", require: "active_support/all" platforms :jruby do gem "activerecord-jdbcsqlite3-adapter" gem "jdbc-sqlite3" + gem "jdbc-postgres" end platforms :ruby, :mswin, :mingw do diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb index a457b87e..28e2d836 100644 --- a/spec/cancan/model_adapters/accessible_by_integration_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -7,7 +7,7 @@ let(:posts_table) { Post.table_name } let(:likes_table) { Like.table_name } before :each do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + connect_db ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define do diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index 85860345..d38b3a7d 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -4,7 +4,7 @@ describe CanCan::ModelAdapters::ActiveRecord4Adapter do context 'with sqlite3' do before :each do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + connect_db ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define do create_table(:parents) do |t| @@ -111,15 +111,7 @@ class Disc < ActiveRecord::Base context 'with postgresql' do before :each do - ActiveRecord::Base.establish_connection(adapter: 'postgresql', - database: 'postgres', - schema_search_path: 'public') - ActiveRecord::Base.connection.drop_database('cancan_postgresql_spec') - ActiveRecord::Base.connection.create_database('cancan_postgresql_spec', - 'encoding' => 'utf-8', - 'adapter' => 'postgresql') - ActiveRecord::Base.establish_connection(adapter: 'postgresql', - database: 'cancan_postgresql_spec') + connect_db ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define do create_table(:parents) do |t| diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index 75c7c997..e229ef7a 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -4,7 +4,7 @@ describe CanCan::ModelAdapters::ActiveRecord5Adapter do context 'with sqlite3' do before :each do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + connect_db ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define do diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index eb5071fb..bee6748f 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -2,24 +2,16 @@ describe CanCan::ModelAdapters::ActiveRecordAdapter do let(:true_v) do - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') - 1 - else - "'t'" - end + ActiveRecord::Base.connection.quoted_true end let(:false_v) do - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('6') - 0 - else - "'f'" - end + ActiveRecord::Base.connection.quoted_false end let(:false_condition) { "#{true_v}=#{false_v}" } before :each do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + connect_db ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define do create_table(:categories) do |t| @@ -125,14 +117,14 @@ class User < ActiveRecord::Base it 'fetches all articles when one can read all' do @ability.can :read, Article article = Article.create! - expect(Article.accessible_by(@ability)).to eq([article]) + expect(Article.accessible_by(@ability)).to match_array([article]) end it 'fetches only the articles that are published' do @ability.can :read, Article, published: true article1 = Article.create!(published: true) Article.create!(published: false) - expect(Article.accessible_by(@ability)).to eq([article1]) + expect(Article.accessible_by(@ability)).to match_array([article1]) end it 'fetches any articles which are published or secret' do @@ -142,7 +134,7 @@ class User < ActiveRecord::Base article2 = Article.create!(published: true, secret: true) article3 = Article.create!(published: false, secret: true) Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to eq([article1, article2, article3]) + expect(Article.accessible_by(@ability)).to match_array([article1, article2, article3]) end it 'fetches any articles which we are cited in' do @@ -152,7 +144,7 @@ class User < ActiveRecord::Base cited.mentioned_users << user @ability.can :read, Article, mentioned_users: { id: user.id } @ability.can :read, Article, mentions: { user_id: user.id } - expect(Article.accessible_by(@ability)).to eq([cited]) + expect(Article.accessible_by(@ability)).to match_array([cited]) end it 'fetches only the articles that are published and not secret' do @@ -162,14 +154,14 @@ class User < ActiveRecord::Base Article.create!(published: true, secret: true) Article.create!(published: false, secret: true) Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to eq([article1]) + expect(Article.accessible_by(@ability)).to match_array([article1]) end it 'only reads comments for articles which are published' do @ability.can :read, Comment, article: { published: true } comment1 = Comment.create!(article: Article.create!(published: true)) Comment.create!(article: Article.create!(published: false)) - expect(Comment.accessible_by(@ability)).to eq([comment1]) + expect(Comment.accessible_by(@ability)).to match_array([comment1]) end it 'should only read articles which are published or in visible categories' do @@ -178,7 +170,7 @@ class User < ActiveRecord::Base article1 = Article.create!(published: true) Article.create!(published: false) article3 = Article.create!(published: false, category: Category.create!(visible: true)) - expect(Article.accessible_by(@ability)).to eq([article1, article3]) + expect(Article.accessible_by(@ability)).to match_array([article1, article3]) end it 'should only read categories once even if they have multiple articles' do @@ -187,14 +179,14 @@ class User < ActiveRecord::Base category = Category.create! Article.create!(published: true, category: category) Article.create!(published: true, category: category) - expect(Category.accessible_by(@ability)).to eq([category]) + expect(Category.accessible_by(@ability)).to match_array([category]) end it 'only reads comments for visible categories through articles' do @ability.can :read, Comment, article: { category: { visible: true } } comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect(Comment.accessible_by(@ability)).to eq([comment1]) + expect(Comment.accessible_by(@ability)).to match_array([comment1]) end it 'allows conditions in SQL and merge with hash conditions' do @@ -204,14 +196,14 @@ class User < ActiveRecord::Base article2 = Article.create!(published: true, secret: true) article3 = Article.create!(published: false, secret: true) Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to eq([article1, article2, article3]) + expect(Article.accessible_by(@ability)).to match_array([article1, article2, article3]) end it 'allows a scope for conditions' do @ability.can :read, Article, Article.where(secret: true) article1 = Article.create!(secret: true) Article.create!(secret: false) - expect(Article.accessible_by(@ability)).to eq([article1]) + expect(Article.accessible_by(@ability)).to match_array([article1]) end it 'fetches only associated records when using with a scope for conditions' do @@ -220,7 +212,7 @@ class User < ActiveRecord::Base category2 = Category.create!(visible: true) article1 = Article.create!(secret: true, category: category1) Article.create!(secret: true, category: category2) - expect(category1.articles.accessible_by(@ability)).to eq([article1]) + expect(category1.articles.accessible_by(@ability)).to match_array([article1]) end it 'raises an exception when trying to merge scope with other conditions' do @@ -419,7 +411,7 @@ class Namespace::TableZ < ActiveRecord::Base table_x = Namespace::TableX.create! table_x.table_zs.create(user: user) - expect(Namespace::TableX.accessible_by(ability)).to eq([table_x]) + expect(Namespace::TableX.accessible_by(ability)).to match_array([table_x]) end end @@ -440,7 +432,7 @@ class Course < ActiveRecord::Base Course.create!(start_at: 10.days.ago) valid_course = Course.create!(start_at: Time.now) - expect(Course.accessible_by(@ability)).to eq([valid_course]) + expect(Course.accessible_by(@ability)).to match_array([valid_course]) end end @@ -468,7 +460,7 @@ class Transaction < ActiveRecord::Base ability = Ability.new(sender) ability.can :read, Transaction, sender: { id: sender.id } ability.can :read, Transaction, receiver: { id: sender.id } - expect(Transaction.accessible_by(ability)).to eq([t1, t2]) + expect(Transaction.accessible_by(ability)).to match_array([t1, t2]) end end @@ -486,7 +478,7 @@ class Transaction < ActiveRecord::Base ability.can :read, Article, mentioned_users: { mentioned_articles: { id: a2.id } } ability.can :read, Article, mentioned_users: { articles: { user: { name: 'deep' } } } ability.can :read, Article, mentioned_users: { articles: { mentioned_users: { name: 'd2' } } } - expect(Article.accessible_by(ability)).to eq([a1]) + expect(Article.accessible_by(ability)).to match_array([a1]) end end end diff --git a/spec/cancan/model_adapters/conditions_extractor_spec.rb b/spec/cancan/model_adapters/conditions_extractor_spec.rb index 7d05fb41..2745ea12 100644 --- a/spec/cancan/model_adapters/conditions_extractor_spec.rb +++ b/spec/cancan/model_adapters/conditions_extractor_spec.rb @@ -2,7 +2,7 @@ RSpec.describe CanCan::ModelAdapters::ConditionsExtractor do before do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + connect_db ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define do create_table(:categories) do |t| diff --git a/spec/support/sql_helpers.rb b/spec/support/sql_helpers.rb index d74017b5..9a7c1e3f 100644 --- a/spec/support/sql_helpers.rb +++ b/spec/support/sql_helpers.rb @@ -2,4 +2,27 @@ module SQLHelpers def normalized_sql(adapter) adapter.database_records.to_sql.strip.squeeze(' ') end + + def connect_db + ActiveRecord::Base.logger = nil + if ENV['DB'] == 'sqlite' + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + elsif ENV['DB'] == 'postgres' + connect_postgres + else + raise StandardError, 'database not supported' + end + end + + private + + def connect_postgres + ActiveRecord::Base.establish_connection(adapter: 'postgresql', host: 'localhost', + database: 'postgres', schema_search_path: 'public') + ActiveRecord::Base.connection.drop_database('cancan_postgresql_spec') + ActiveRecord::Base.connection.create_database('cancan_postgresql_spec', 'encoding' => 'utf-8', + 'adapter' => 'postgresql') + ActiveRecord::Base.establish_connection(adapter: 'postgresql', host: 'localhost', + database: 'cancan_postgresql_spec') + end end From cfdbc773d94f8d9f67f4014905f67af147539ec4 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 23 Feb 2019 08:17:48 +0100 Subject: [PATCH 41/54] Exclude tests from jruby --- spec/cancan/rule_spec.rb | 49 +++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/spec/cancan/rule_spec.rb b/spec/cancan/rule_spec.rb index 0f467bc1..900ef24b 100644 --- a/spec/cancan/rule_spec.rb +++ b/spec/cancan/rule_spec.rb @@ -59,34 +59,37 @@ expect(rule2.conditions).to eq %i[foo bar] end - describe '#inspect' do - def count_queries(&block) - count = 0 - counter_f = lambda { |_name, _started, _finished, _unique_id, payload| - count += 1 unless payload[:name].in? %w[CACHE SCHEMA] - } - ActiveSupport::Notifications.subscribed(counter_f, 'sql.active_record', &block) - count - end - before do - ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define do - create_table(:watermelons) do |t| - t.boolean :visible - end + unless RUBY_ENGINE == 'jruby' + describe '#inspect' do + def count_queries(&block) + count = 0 + counter_f = lambda { |_name, _started, _finished, _unique_id, payload| + count += 1 unless payload[:name].in? %w[CACHE SCHEMA] + } + ActiveSupport::Notifications.subscribed(counter_f, 'sql.active_record', &block) + count end - class Watermelon < ActiveRecord::Base - scope :visible, -> { where(visible: true) } + before do + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:watermelons) do |t| + t.boolean :visible + end + end + + class Watermelon < ActiveRecord::Base + scope :visible, -> { where(visible: true) } + end end - end - it 'does not evaluate the conditions when they are scopes' do - rule = CanCan::Rule.new(true, :read, Watermelon, Watermelon.visible, {}, {}) - count = count_queries { rule.inspect } - expect(count).to eq 0 + it 'does not evaluate the conditions when they are scopes' do + rule = CanCan::Rule.new(true, :read, Watermelon, Watermelon.visible, {}, {}) + count = count_queries { rule.inspect } + expect(count).to eq 0 + end end end end From 88fb074380fcef205de5945853c97a9776c76333 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 23 Feb 2019 08:23:26 +0100 Subject: [PATCH 42/54] Update 3.0.0 CHnagelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5054d65..8e357d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,13 @@ * [#560](https://github.com/CanCanCommunity/cancancan/pull/560): Add support for Rails 6.0. ([@coorasse][]) * [#489](https://github.com/CanCanCommunity/cancancan/pull/489): Drop support for actions without a subject. ([@andrew-aladev][]) * [#474](https://github.com/CanCanCommunity/cancancan/pull/474): Allow to add attribute-level rules. ([@phaedryx][]) -* [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed eager loading of associations for ActiveRecord 5. ([@kaspernj][]) +* [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed eager loading of associations for ActiveRecord >= 5.0. ([@kaspernj][]) * [#444](https://github.com/CanCanCommunity/cancancan/issues/444): Allow to use symbols when defining conditions over enums. ([@s-mage][]) * [#538](https://github.com/CanCanCommunity/cancancan/issues/538): Merge alias actions when merging abilities. ([@Jcambass][]) * [#462](https://github.com/CanCanCommunity/cancancan/issues/462): Add support to translate the model name in messages. ([@nyamadori][]) +* [#567](https://github.com/CanCanCommunity/cancancan/issues/567): Extensively run tests on different databases (sqlite and postgres). ([@coorasse][]) +* [#566](https://github.com/CanCanCommunity/cancancan/issues/566): Avoid queries on session dumps (speed up error pages). ([@coorasse][]) Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.com/CanCanCommunity/cancancan/wiki/Migrating-from-CanCanCan-2.x-to-3.0) From 23ac88c2aa7db77f08744dd3f5492af02a29cd03 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 23 Feb 2019 11:02:14 +0100 Subject: [PATCH 43/54] fix rubocop --- spec/cancan/rule_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/cancan/rule_spec.rb b/spec/cancan/rule_spec.rb index 900ef24b..aae4d81e 100644 --- a/spec/cancan/rule_spec.rb +++ b/spec/cancan/rule_spec.rb @@ -59,7 +59,6 @@ expect(rule2.conditions).to eq %i[foo bar] end - unless RUBY_ENGINE == 'jruby' describe '#inspect' do def count_queries(&block) From b5839455390ca8f941024485186e909065f9dd35 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 16 Mar 2019 10:24:55 +0100 Subject: [PATCH 44/54] Improve usage of the rules compressor --- Appraisals | 6 ++-- CHANGELOG.md | 2 +- gemfiles/activerecord_6.0.0.gemfile | 6 ++-- .../model_adapters/active_record_4_adapter.rb | 36 +++++++++---------- .../model_adapters/active_record_5_adapter.rb | 5 +-- .../model_adapters/active_record_adapter.rb | 24 ++++++------- lib/cancan/rule.rb | 14 +++++--- .../active_record_adapter_spec.rb | 21 +++++++++++ spec/cancan/rule_spec.rb | 12 ------- spec/support/sql_helpers.rb | 1 + 10 files changed, 69 insertions(+), 58 deletions(-) diff --git a/Appraisals b/Appraisals index bd574c1f..08ed5183 100644 --- a/Appraisals +++ b/Appraisals @@ -68,9 +68,9 @@ appraise 'activerecord_5.2.2' do end appraise 'activerecord_6.0.0' do - gem 'actionpack', '~> 6.0.0.beta1', require: 'action_pack' - gem 'activerecord', '~> 6.0.0.beta1', require: 'active_record' - gem 'activesupport', '~> 6.0.0.beta1', require: 'active_support/all' + gem 'actionpack', '~> 6.0.0.beta3', require: 'action_pack' + gem 'activerecord', '~> 6.0.0.beta3', require: 'active_record' + gem 'activesupport', '~> 6.0.0.beta3', require: 'active_support/all' platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e357d49..fce5921a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ * [#560](https://github.com/CanCanCommunity/cancancan/pull/560): Add support for Rails 6.0. ([@coorasse][]) * [#489](https://github.com/CanCanCommunity/cancancan/pull/489): Drop support for actions without a subject. ([@andrew-aladev][]) * [#474](https://github.com/CanCanCommunity/cancancan/pull/474): Allow to add attribute-level rules. ([@phaedryx][]) -* [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed eager loading of associations for ActiveRecord >= 5.0. ([@kaspernj][]) +* [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed automatic eager loading of associations for ActiveRecord >= 5.0. ([@kaspernj][]) * [#444](https://github.com/CanCanCommunity/cancancan/issues/444): Allow to use symbols when defining conditions over enums. ([@s-mage][]) * [#538](https://github.com/CanCanCommunity/cancancan/issues/538): Merge alias actions when merging abilities. ([@Jcambass][]) diff --git a/gemfiles/activerecord_6.0.0.gemfile b/gemfiles/activerecord_6.0.0.gemfile index 13f124a2..4132648a 100644 --- a/gemfiles/activerecord_6.0.0.gemfile +++ b/gemfiles/activerecord_6.0.0.gemfile @@ -2,9 +2,9 @@ source "https://rubygems.org" -gem "actionpack", "~> 6.0.0.beta1", require: "action_pack" -gem "activerecord", "~> 6.0.0.beta1", require: "active_record" -gem "activesupport", "~> 6.0.0.beta1", require: "active_support/all" +gem "actionpack", "~> 6.0.0.beta3", require: "action_pack" +gem "activerecord", "~> 6.0.0.beta3", require: "active_record" +gem "activesupport", "~> 6.0.0.beta3", require: "active_support/all" platforms :jruby do gem "activerecord-jdbcsqlite3-adapter" diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index 0598b270..93f9a750 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -3,26 +3,26 @@ module ModelAdapters class ActiveRecord4Adapter < ActiveRecordAdapter AbstractAdapter.inherited(self) - def self.for_class?(model_class) - version_lower?('5.0.0') && model_class <= ActiveRecord::Base - end + class << self + def for_class?(model_class) + version_lower?('5.0.0') && model_class <= ActiveRecord::Base + end - # TODO: this should be private - def self.override_condition_matching?(subject, name, _value) - subject.class.defined_enums.include?(name.to_s) - end + def override_condition_matching?(subject, name, _value) + subject.class.defined_enums.include?(name.to_s) + end - # TODO: this should be private - def self.matches_condition?(subject, name, value) - # Get the mapping from enum strings to values. - enum = subject.class.send(name.to_s.pluralize) - # Get the value of the attribute as an integer. - attribute = enum[subject.send(name)] - # Check to see if the value matches the condition. - if value.is_a?(Enumerable) - value.include? attribute - else - attribute == value + def matches_condition?(subject, name, value) + # Get the mapping from enum strings to values. + enum = subject.class.send(name.to_s.pluralize) + # Get the value of the attribute as an integer. + attribute = enum[subject.send(name)] + # Check to see if the value matches the condition. + if value.is_a?(Enumerable) + value.include? attribute + else + attribute == value + end end end diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index efc5a33c..424350a2 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -43,10 +43,7 @@ def sanitize_sql_activerecord5(conditions) conditions.stringify_keys! - predicate_builder - .build_from_hash(conditions) - .map { |b| visit_nodes(b) } - .join(' AND ') + predicate_builder.build_from_hash(conditions).map { |b| visit_nodes(b) }.join(' AND ') end def visit_nodes(node) diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 4342251b..fd7aefae 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -11,6 +11,11 @@ def self.version_lower?(version) Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version) end + def initialize(model_class, rules) + super + @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse + end + # Returns conditions intended to be used inside a database query. Normally you will not call this # method directly, but instead go through ModelAdditions#accessible_by. # @@ -27,13 +32,12 @@ def self.version_lower?(version) # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))" # def conditions - compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse conditions_extractor = ConditionsExtractor.new(@model_class) - if compressed_rules.size == 1 && compressed_rules.first.base_behavior + if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior # Return the conditions directly if there's just one definition - conditions_extractor.tableize_conditions(compressed_rules.first.conditions).dup + conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup else - extract_multiple_conditions(conditions_extractor, compressed_rules) + extract_multiple_conditions(conditions_extractor, @compressed_rules) end end @@ -47,7 +51,7 @@ def database_records if override_scope @model_class.where(nil).merge(override_scope) elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins) - mergeable_conditions? ? build_relation(conditions) : build_relation(*@rules.map(&:conditions)) + build_relation(conditions) else @model_class.all(conditions: conditions, joins: joins) end @@ -57,7 +61,7 @@ def database_records # See ModelAdditions#accessible_by def joins joins_hash = {} - @rules.reverse_each do |rule| + @compressed_rules.reverse_each do |rule| deep_merge(joins_hash, rule.associations_hash) end deep_clean(joins_hash) unless joins_hash.empty? @@ -81,12 +85,8 @@ def deep_merge(base_hash, added_hash) end end - def mergeable_conditions? - @rules.find(&:unmergeable?).blank? - end - def override_scope - conditions = @rules.map(&:conditions).compact + conditions = @compressed_rules.map(&:conditions).compact return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) } return conditions.first if conditions.size == 1 @@ -94,7 +94,7 @@ def override_scope end def raise_override_scope_error - rule_found = @rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) } + rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) } raise Error, 'Unable to merge an Active Record scope with other conditions. '\ "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability." diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index fb8881ff..a6780615 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -32,7 +32,11 @@ def initialize(base_behavior, action, subject, *extra_args, &block) def inspect repr = "#<#{self.class.name}" repr << "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}" - repr << @conditions.inspect.to_s if [Hash, String].include?(@conditions.class) + if with_scope? + repr << ", #{@conditions.where_values_hash}" + elsif [Hash, String].include?(@conditions.class) + repr << ", #{@conditions.inspect}" + end repr << '>' end @@ -45,7 +49,8 @@ def cannot_catch_all? end def catch_all? - [nil, false, [], {}, '', ' '].include? @conditions + (with_scope? && @conditions.where_values_hash.empty?) || + (!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions)) end # Matches both the action, subject, and attribute, not necessarily the conditions @@ -62,9 +67,8 @@ def only_raw_sql? @block.nil? && !conditions_empty? && !@conditions.is_a?(Hash) end - def unmergeable? - @conditions.respond_to?(:keys) && @conditions.present? && - (!@conditions.keys.first.is_a? Symbol) + def with_scope? + @conditions.is_a?(ActiveRecord::Relation) end def associations_hash(conditions = @conditions) diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index bee6748f..8a090aee 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -224,6 +224,19 @@ class User < ActiveRecord::Base 'Instead use a hash or SQL for read Article ability.') end + it 'does not raise an exception when the rule with scope is suppressed' do + @ability.can :read, Article, published: true + @ability.can :read, Article, Article.where(secret: true) + @ability.cannot :read, Article + expect(-> { Article.accessible_by(@ability) }).not_to raise_error + end + + it 'recognises empty scopes and compresses them' do + @ability.can :read, Article, published: true + @ability.can :read, Article, Article.all + expect(-> { Article.accessible_by(@ability) }).not_to raise_error + end + it 'does not allow to fetch records when ability with just block present' do @ability.can :read, Article do false @@ -323,6 +336,14 @@ class User < ActiveRecord::Base expect(@ability.model_adapter(Article, :read).joins).to be_nil end + it 'has nil joins if rules got compressed' do + @ability.can :read, Comment, article: { category: { visible: true } } + @ability.can :read, Comment + expect(@ability.model_adapter(Comment, :read)) + .to generate_sql("SELECT \"#{@comment_table}\".* FROM \"#{@comment_table}\"") + expect(@ability.model_adapter(Comment, :read).joins).to be_nil + end + it 'has nil joins if no nested hashes specified in conditions' do @ability.can :read, Article, published: false @ability.can :read, Article, secret: true diff --git a/spec/cancan/rule_spec.rb b/spec/cancan/rule_spec.rb index aae4d81e..b19a98bb 100644 --- a/spec/cancan/rule_spec.rb +++ b/spec/cancan/rule_spec.rb @@ -38,18 +38,6 @@ expect(rule.associations_hash).to eq({}) end - it 'is not mergeable if conditions are not simple hashes' do - meta_where = OpenStruct.new(name: 'metawhere', column: 'test') - @conditions[meta_where] = :bar - - expect(@rule).to be_unmergeable - end - - it 'is not mergeable if conditions is an empty hash' do - @conditions = {} - expect(@rule).to_not be_unmergeable - end - it 'allows nil in attribute spot for edge cases' do rule1 = CanCan::Rule.new(true, :action, :subject, nil, :var) expect(rule1.attributes).to eq [] diff --git a/spec/support/sql_helpers.rb b/spec/support/sql_helpers.rb index 9a7c1e3f..d196128d 100644 --- a/spec/support/sql_helpers.rb +++ b/spec/support/sql_helpers.rb @@ -4,6 +4,7 @@ def normalized_sql(adapter) end def connect_db + # ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.logger = nil if ENV['DB'] == 'sqlite' ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') From 8da72662016b521e78149dd8514b6369b1b419b0 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 16 Mar 2019 10:31:44 +0100 Subject: [PATCH 45/54] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fce5921a..4c7b8489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#489](https://github.com/CanCanCommunity/cancancan/pull/489): Drop support for actions without a subject. ([@andrew-aladev][]) * [#474](https://github.com/CanCanCommunity/cancancan/pull/474): Allow to add attribute-level rules. ([@phaedryx][]) * [#512](https://github.com/CanCanCommunity/cancancan/pull/512): Removed automatic eager loading of associations for ActiveRecord >= 5.0. ([@kaspernj][]) +* [#575](https://github.com/CanCanCommunity/cancancan/pull/575): Use the rules compressor when generating joins in accessible_by. ([@coorasse][]) * [#444](https://github.com/CanCanCommunity/cancancan/issues/444): Allow to use symbols when defining conditions over enums. ([@s-mage][]) * [#538](https://github.com/CanCanCommunity/cancancan/issues/538): Merge alias actions when merging abilities. ([@Jcambass][]) From fc88cd093cbac1c4401b589b1c0c88a924a54dbc Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 16 Mar 2019 11:02:55 +0100 Subject: [PATCH 46/54] Add explicit test for HABTM association --- .../has_and_belongs_to_many_spec.rb | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb diff --git a/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb new file mode 100644 index 00000000..ad556066 --- /dev/null +++ b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +RSpec.describe CanCan::ModelAdapters::ActiveRecord5Adapter do + let(:ability) { double.extend(CanCan::Ability) } + let(:users_table) { User.table_name } + let(:posts_table) { Post.table_name } + + before :all do + connect_db + ActiveRecord::Migration.verbose = false + + ActiveRecord::Schema.define do + create_table(:people) do |t| + t.string :name + t.timestamps null: false + end + + create_table(:houses) do |t| + t.boolean :restructured, default: true + t.timestamps null: false + end + + create_table(:houses_people) do |t| + t.integer :person_id + t.integer :house_id + t.timestamps null: false + end + end + + class Person < ActiveRecord::Base + has_and_belongs_to_many :houses + end + + class House < ActiveRecord::Base + has_and_belongs_to_many :people + end + end + + before do + @person1 = Person.create! + @person2 = Person.create! + @house1 = House.create!(people: [@person1]) + @house2 = House.create!(restructured: false, people: [@person1, @person2]) + @house3 = House.create!(people: [@person2]) + ability.can :read, House, people: { id: @person1.id } + end + + describe 'fetching of records' do + it 'it retreives the records correctly' do + houses = House.accessible_by(ability) + expect(houses).to match_array [@house2, @house1] + end + + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + it 'generates the correct query' do + expect(ability.model_adapter(House, :read)) + .to generate_sql("SELECT DISTINCT \"houses\".* + FROM \"houses\" + LEFT OUTER JOIN \"houses_people\" ON \"houses_people\".\"house_id\" = \"houses\".\"id\" + LEFT OUTER JOIN \"people\" ON \"people\".\"id\" = \"houses_people\".\"person_id\" + WHERE \"people\".\"id\" = #{@person1.id}") + end + end + end +end From 4254891b8b29a325c7fc9ff5a8d1e31db687a963 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 23 Feb 2019 17:01:29 +0100 Subject: [PATCH 47/54] Freeze strings --- .rubocop.yml | 3 --- .travis.yml | 2 ++ CHANGELOG.md | 1 + Gemfile | 2 ++ Rakefile | 2 ++ cancancan.gemspec | 2 ++ init.rb | 2 ++ lib/cancan.rb | 2 ++ lib/cancan/ability.rb | 2 ++ lib/cancan/ability/actions.rb | 2 ++ lib/cancan/ability/rules.rb | 2 ++ lib/cancan/ability/strong_parameter_support.rb | 2 ++ lib/cancan/conditions_matcher.rb | 2 ++ lib/cancan/controller_additions.rb | 2 ++ lib/cancan/controller_resource.rb | 2 ++ lib/cancan/controller_resource_builder.rb | 2 ++ lib/cancan/controller_resource_finder.rb | 2 ++ lib/cancan/controller_resource_loader.rb | 2 ++ lib/cancan/controller_resource_name_finder.rb | 2 ++ lib/cancan/controller_resource_sanitizer.rb | 2 ++ lib/cancan/exceptions.rb | 2 ++ lib/cancan/matchers.rb | 2 ++ lib/cancan/model_adapters/abstract_adapter.rb | 2 ++ .../model_adapters/active_record_4_adapter.rb | 2 ++ .../model_adapters/active_record_5_adapter.rb | 2 ++ .../model_adapters/active_record_adapter.rb | 2 ++ .../model_adapters/conditions_extractor.rb | 2 ++ lib/cancan/model_adapters/default_adapter.rb | 2 ++ lib/cancan/model_additions.rb | 2 ++ lib/cancan/parameter_validators.rb | 2 ++ lib/cancan/rule.rb | 16 +++++++++------- lib/cancan/rules_compressor.rb | 2 ++ lib/cancan/unauthorized_message_resolver.rb | 2 ++ lib/cancan/version.rb | 2 ++ lib/cancancan.rb | 2 ++ .../cancan/ability/ability_generator.rb | 2 ++ .../cancan/ability/templates/ability.rb | 2 ++ spec/cancan/ability_spec.rb | 2 ++ spec/cancan/controller_additions_spec.rb | 2 ++ spec/cancan/controller_resource_spec.rb | 2 ++ spec/cancan/exceptions_spec.rb | 2 ++ spec/cancan/matchers_spec.rb | 2 ++ .../accessible_by_integration_spec.rb | 2 ++ .../active_record_4_adapter_spec.rb | 2 ++ .../active_record_5_adapter_spec.rb | 2 ++ .../model_adapters/active_record_adapter_spec.rb | 2 ++ .../model_adapters/conditions_extractor_spec.rb | 2 ++ .../model_adapters/default_adapter_spec.rb | 2 ++ spec/cancan/rule_compressor_spec.rb | 2 ++ spec/cancan/rule_spec.rb | 2 ++ spec/changelog_spec.rb | 2 ++ spec/matchers.rb | 2 ++ spec/spec_helper.rb | 2 ++ spec/support/ability.rb | 2 ++ spec/support/sql_helpers.rb | 2 ++ 55 files changed, 114 insertions(+), 10 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index c7c1d81f..214673a5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,9 +4,6 @@ Style/Documentation: Style/NonNilCheck: IncludeSemanticChanges: true -Style/FrozenStringLiteralComment: - Enabled: false - Style/EmptyMethod: Enabled: false diff --git a/.travis.yml b/.travis.yml index 6818e71e..8ef48b7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,8 @@ env: matrix: fast_finish: true exclude: + - rvm: 2.2.6 + gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: 2.3.5 gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: 2.4.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7b8489..5fd0b190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#462](https://github.com/CanCanCommunity/cancancan/issues/462): Add support to translate the model name in messages. ([@nyamadori][]) * [#567](https://github.com/CanCanCommunity/cancancan/issues/567): Extensively run tests on different databases (sqlite and postgres). ([@coorasse][]) * [#566](https://github.com/CanCanCommunity/cancancan/issues/566): Avoid queries on session dumps (speed up error pages). ([@coorasse][]) +* [#568](https://github.com/CanCanCommunity/cancancan/issues/568): Automatically freeze strings in all files. ([@coorasse][]) Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.com/CanCanCommunity/cancancan/wiki/Migrating-from-CanCanCan-2.x-to-3.0) diff --git a/Gemfile b/Gemfile index fa75df15..7f4f5e95 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/Rakefile b/Rakefile index 5f62d901..91c402ae 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'bundler/gem_tasks' require 'rspec/core/rake_task' diff --git a/cancancan.gemspec b/cancancan.gemspec index f90de120..15d9e70f 100644 --- a/cancancan.gemspec +++ b/cancancan.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'cancan/version' diff --git a/init.rb b/init.rb index db9a9531..88adeaea 100644 --- a/init.rb +++ b/init.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + require 'cancan' diff --git a/lib/cancan.rb b/lib/cancan.rb index 86d419c4..25753539 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'cancan/version' require 'cancan/parameter_validators' require 'cancan/ability' diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 43462ba3..8b40c997 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'ability/rules.rb' require_relative 'ability/actions.rb' require_relative 'unauthorized_message_resolver.rb' diff --git a/lib/cancan/ability/actions.rb b/lib/cancan/ability/actions.rb index d8c69d7f..0382daa8 100644 --- a/lib/cancan/ability/actions.rb +++ b/lib/cancan/ability/actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module Ability module Actions diff --git a/lib/cancan/ability/rules.rb b/lib/cancan/ability/rules.rb index 7ab0f12d..0d1297be 100644 --- a/lib/cancan/ability/rules.rb +++ b/lib/cancan/ability/rules.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module Ability module Rules diff --git a/lib/cancan/ability/strong_parameter_support.rb b/lib/cancan/ability/strong_parameter_support.rb index 02134151..31da7457 100644 --- a/lib/cancan/ability/strong_parameter_support.rb +++ b/lib/cancan/ability/strong_parameter_support.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module Ability module StrongParameterSupport diff --git a/lib/cancan/conditions_matcher.rb b/lib/cancan/conditions_matcher.rb index 498e0c4b..caaf61df 100644 --- a/lib/cancan/conditions_matcher.rb +++ b/lib/cancan/conditions_matcher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ConditionsMatcher # Matches the block or conditions hash diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index 4a89edf7..68f949dc 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan # This module is automatically included into all controllers. # It also makes the "can?" and "cannot?" methods available to all views. diff --git a/lib/cancan/controller_resource.rb b/lib/cancan/controller_resource.rb index 4c77364a..d9f753a0 100644 --- a/lib/cancan/controller_resource.rb +++ b/lib/cancan/controller_resource.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'controller_resource_loader.rb' module CanCan # Handle the load and authorization controller logic diff --git a/lib/cancan/controller_resource_builder.rb b/lib/cancan/controller_resource_builder.rb index 6015ec3c..30a4b9ac 100644 --- a/lib/cancan/controller_resource_builder.rb +++ b/lib/cancan/controller_resource_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ControllerResourceBuilder protected diff --git a/lib/cancan/controller_resource_finder.rb b/lib/cancan/controller_resource_finder.rb index d4c223b3..742c8832 100644 --- a/lib/cancan/controller_resource_finder.rb +++ b/lib/cancan/controller_resource_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ControllerResourceFinder protected diff --git a/lib/cancan/controller_resource_loader.rb b/lib/cancan/controller_resource_loader.rb index 2186ddbe..8a018984 100644 --- a/lib/cancan/controller_resource_loader.rb +++ b/lib/cancan/controller_resource_loader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'controller_resource_finder.rb' require_relative 'controller_resource_name_finder.rb' require_relative 'controller_resource_builder.rb' diff --git a/lib/cancan/controller_resource_name_finder.rb b/lib/cancan/controller_resource_name_finder.rb index a4e86ffb..d3e289a1 100644 --- a/lib/cancan/controller_resource_name_finder.rb +++ b/lib/cancan/controller_resource_name_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ControllerResourceNameFinder protected diff --git a/lib/cancan/controller_resource_sanitizer.rb b/lib/cancan/controller_resource_sanitizer.rb index a424fc23..b42ccb8d 100644 --- a/lib/cancan/controller_resource_sanitizer.rb +++ b/lib/cancan/controller_resource_sanitizer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ControllerResourceSanitizer protected diff --git a/lib/cancan/exceptions.rb b/lib/cancan/exceptions.rb index 7cbba950..d66c5eb6 100644 --- a/lib/cancan/exceptions.rb +++ b/lib/cancan/exceptions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan # A general CanCan exception class Error < StandardError; end diff --git a/lib/cancan/matchers.rb b/lib/cancan/matchers.rb index 00d86059..7e5b206b 100644 --- a/lib/cancan/matchers.rb +++ b/lib/cancan/matchers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # RSpec 1 compatability if rspec_module == 'RSpec' diff --git a/lib/cancan/model_adapters/abstract_adapter.rb b/lib/cancan/model_adapters/abstract_adapter.rb index ac01dd5f..06cfd6c0 100644 --- a/lib/cancan/model_adapters/abstract_adapter.rb +++ b/lib/cancan/model_adapters/abstract_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ModelAdapters class AbstractAdapter diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index 93f9a750..4755240f 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ModelAdapters class ActiveRecord4Adapter < ActiveRecordAdapter diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index 424350a2..cac1ac4b 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ModelAdapters class ActiveRecord5Adapter < ActiveRecord4Adapter diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index fd7aefae..94bb810e 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'conditions_extractor.rb' require 'cancan/rules_compressor' module CanCan diff --git a/lib/cancan/model_adapters/conditions_extractor.rb b/lib/cancan/model_adapters/conditions_extractor.rb index bfe8e872..08991b4b 100644 --- a/lib/cancan/model_adapters/conditions_extractor.rb +++ b/lib/cancan/model_adapters/conditions_extractor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # this class is responsible of converting the hash of conditions # in "where conditions" to generate the sql query # it consists of a names_cache that helps calculating the next name given to the association diff --git a/lib/cancan/model_adapters/default_adapter.rb b/lib/cancan/model_adapters/default_adapter.rb index d76d87f4..5c2820e9 100644 --- a/lib/cancan/model_adapters/default_adapter.rb +++ b/lib/cancan/model_adapters/default_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ModelAdapters class DefaultAdapter < AbstractAdapter diff --git a/lib/cancan/model_additions.rb b/lib/cancan/model_additions.rb index 88cf8edc..96732133 100644 --- a/lib/cancan/model_additions.rb +++ b/lib/cancan/model_additions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan # This module adds the accessible_by class method to a model. It is included in the model adapters. module ModelAdditions diff --git a/lib/cancan/parameter_validators.rb b/lib/cancan/parameter_validators.rb index 546a4f1d..73e553f1 100644 --- a/lib/cancan/parameter_validators.rb +++ b/lib/cancan/parameter_validators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module ParameterValidators def valid_attribute_param?(attribute) diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index a6780615..eaf61efc 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'conditions_matcher.rb' module CanCan # This class is used internally and should only be called through Ability. @@ -31,13 +33,13 @@ def initialize(base_behavior, action, subject, *extra_args, &block) def inspect repr = "#<#{self.class.name}" - repr << "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}" - if with_scope? - repr << ", #{@conditions.where_values_hash}" - elsif [Hash, String].include?(@conditions.class) - repr << ", #{@conditions.inspect}" - end - repr << '>' + repr += "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}" + repr += if with_scope? + ", #{@conditions.where_values_hash}" + elsif [Hash, String].include?(@conditions.class) + ", #{@conditions.inspect}" + end + repr + '>' end def can_rule? diff --git a/lib/cancan/rules_compressor.rb b/lib/cancan/rules_compressor.rb index 15a954ac..01b9e325 100644 --- a/lib/cancan/rules_compressor.rb +++ b/lib/cancan/rules_compressor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'conditions_matcher.rb' module CanCan class RulesCompressor diff --git a/lib/cancan/unauthorized_message_resolver.rb b/lib/cancan/unauthorized_message_resolver.rb index 4063387f..55f3580b 100644 --- a/lib/cancan/unauthorized_message_resolver.rb +++ b/lib/cancan/unauthorized_message_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan module UnauthorizedMessageResolver def unauthorized_message(action, subject) diff --git a/lib/cancan/version.rb b/lib/cancan/version.rb index a8604adb..12bf0365 100644 --- a/lib/cancan/version.rb +++ b/lib/cancan/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CanCan VERSION = '2.3.0'.freeze end diff --git a/lib/cancancan.rb b/lib/cancancan.rb index 57d2517f..2093dee6 100644 --- a/lib/cancancan.rb +++ b/lib/cancancan.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'cancan' module CanCanCan diff --git a/lib/generators/cancan/ability/ability_generator.rb b/lib/generators/cancan/ability/ability_generator.rb index 595b0092..d2d2250d 100644 --- a/lib/generators/cancan/ability/ability_generator.rb +++ b/lib/generators/cancan/ability/ability_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Cancan module Generators class AbilityGenerator < Rails::Generators::Base diff --git a/lib/generators/cancan/ability/templates/ability.rb b/lib/generators/cancan/ability/templates/ability.rb index bced2859..ef26a8c3 100644 --- a/lib/generators/cancan/ability/templates/ability.rb +++ b/lib/generators/cancan/ability/templates/ability.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Ability include CanCan::Ability diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index e713581f..5e845b10 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CanCan::Ability do diff --git a/spec/cancan/controller_additions_spec.rb b/spec/cancan/controller_additions_spec.rb index 34ded4ca..98b2e664 100644 --- a/spec/cancan/controller_additions_spec.rb +++ b/spec/cancan/controller_additions_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CanCan::ControllerAdditions do diff --git a/spec/cancan/controller_resource_spec.rb b/spec/cancan/controller_resource_spec.rb index c6b2df10..b6e511bb 100644 --- a/spec/cancan/controller_resource_spec.rb +++ b/spec/cancan/controller_resource_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CanCan::ControllerResource do diff --git a/spec/cancan/exceptions_spec.rb b/spec/cancan/exceptions_spec.rb index e9227065..ee2a1f50 100644 --- a/spec/cancan/exceptions_spec.rb +++ b/spec/cancan/exceptions_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CanCan::AccessDenied do diff --git a/spec/cancan/matchers_spec.rb b/spec/cancan/matchers_spec.rb index 05a14b7b..985304ff 100644 --- a/spec/cancan/matchers_spec.rb +++ b/spec/cancan/matchers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe 'be_able_to' do diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb index 28e2d836..f5a2ae93 100644 --- a/spec/cancan/model_adapters/accessible_by_integration_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' # integration tests for latest ActiveRecord version. diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index d38b3a7d..d238ce1e 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' if CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index e229ef7a..fb0edd5a 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 8a090aee..a2284fa1 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CanCan::ModelAdapters::ActiveRecordAdapter do diff --git a/spec/cancan/model_adapters/conditions_extractor_spec.rb b/spec/cancan/model_adapters/conditions_extractor_spec.rb index 2745ea12..29c5fb67 100644 --- a/spec/cancan/model_adapters/conditions_extractor_spec.rb +++ b/spec/cancan/model_adapters/conditions_extractor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe CanCan::ModelAdapters::ConditionsExtractor do diff --git a/spec/cancan/model_adapters/default_adapter_spec.rb b/spec/cancan/model_adapters/default_adapter_spec.rb index 14f38ea1..a0c55a8e 100644 --- a/spec/cancan/model_adapters/default_adapter_spec.rb +++ b/spec/cancan/model_adapters/default_adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CanCan::ModelAdapters::DefaultAdapter do diff --git a/spec/cancan/rule_compressor_spec.rb b/spec/cancan/rule_compressor_spec.rb index b8b9ba9c..3f643ef2 100644 --- a/spec/cancan/rule_compressor_spec.rb +++ b/spec/cancan/rule_compressor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CanCan::RulesCompressor do diff --git a/spec/cancan/rule_spec.rb b/spec/cancan/rule_spec.rb index b19a98bb..9f99568f 100644 --- a/spec/cancan/rule_spec.rb +++ b/spec/cancan/rule_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'ostruct' # for OpenStruct below diff --git a/spec/changelog_spec.rb b/spec/changelog_spec.rb index f7487d77..468447fd 100644 --- a/spec/changelog_spec.rb +++ b/spec/changelog_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # credits to https://github.com/rubocop-hq/rubocop for this CHANGELOG checker RSpec.describe 'changelog' do subject(:changelog) do diff --git a/spec/matchers.rb b/spec/matchers.rb index 1827b153..73c157c6 100644 --- a/spec/matchers.rb +++ b/spec/matchers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + RSpec::Matchers.define :orderlessly_match do |original_string| match do |given_string| original_string.split('').sort == given_string.split('').sort diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 29feeca7..f8f51c16 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rubygems' require 'bundler/setup' diff --git a/spec/support/ability.rb b/spec/support/ability.rb index 869f0b62..a7abb720 100644 --- a/spec/support/ability.rb +++ b/spec/support/ability.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Ability include CanCan::Ability diff --git a/spec/support/sql_helpers.rb b/spec/support/sql_helpers.rb index d196128d..d4cf2e40 100644 --- a/spec/support/sql_helpers.rb +++ b/spec/support/sql_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SQLHelpers def normalized_sql(adapter) adapter.database_records.to_sql.strip.squeeze(' ') From 5f40bbcc08484645d40d14b64237855e9a256c8a Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 16 Mar 2019 13:32:07 +0100 Subject: [PATCH 48/54] Introduce a conditions normalizer --- lib/cancan.rb | 1 + .../model_adapters/active_record_adapter.rb | 1 + .../model_adapters/conditions_extractor.rb | 2 - .../model_adapters/conditions_normalizer.rb | 45 +++++++++ lib/cancan/rule.rb | 2 +- .../accessible_by_has_many_through_spec.rb | 98 +++++++++++++++++++ .../active_record_adapter_spec.rb | 35 +++++++ .../conditions_normalizer_spec.rb | 83 ++++++++++++++++ 8 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 lib/cancan/model_adapters/conditions_normalizer.rb create mode 100644 spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb create mode 100644 spec/cancan/model_adapters/conditions_normalizer_spec.rb diff --git a/lib/cancan.rb b/lib/cancan.rb index 86d419c4..8b77b767 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -13,6 +13,7 @@ if defined? ActiveRecord require 'cancan/model_adapters/conditions_extractor' + require 'cancan/model_adapters/conditions_normalizer' require 'cancan/model_adapters/active_record_adapter' require 'cancan/model_adapters/active_record_4_adapter' require 'cancan/model_adapters/active_record_5_adapter' diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index fd7aefae..16737e37 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -14,6 +14,7 @@ def self.version_lower?(version) def initialize(model_class, rules) super @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse + ConditionsNormalizer.normalize(model_class, @compressed_rules) end # Returns conditions intended to be used inside a database query. Normally you will not call this diff --git a/lib/cancan/model_adapters/conditions_extractor.rb b/lib/cancan/model_adapters/conditions_extractor.rb index bfe8e872..e0662937 100644 --- a/lib/cancan/model_adapters/conditions_extractor.rb +++ b/lib/cancan/model_adapters/conditions_extractor.rb @@ -27,8 +27,6 @@ def tableize_conditions(conditions, model_class = @root_model_class, path_to_key def calculate_result_hash(key, model_class, path_to_key, result_hash, value) reflection = model_class.reflect_on_association(key) - raise WrongAssociationName, "association #{key} not defined in model #{model_class.name}" unless reflection - nested_resulted = calculate_nested(model_class, result_hash, key, value.dup, path_to_key) association_class = reflection.klass.name.constantize tableize_conditions(nested_resulted, association_class, "#{path_to_key}_#{key}") diff --git a/lib/cancan/model_adapters/conditions_normalizer.rb b/lib/cancan/model_adapters/conditions_normalizer.rb new file mode 100644 index 00000000..46ceab71 --- /dev/null +++ b/lib/cancan/model_adapters/conditions_normalizer.rb @@ -0,0 +1,45 @@ +# this class is responsible of normalizing the hash of conditions +# by exploding has_many through associations +# when a condition is defined with an has_many thorugh association this is exploded in all its parts +# TODO: it could identify STI and normalize it +module CanCan + module ModelAdapters + class ConditionsNormalizer + class << self + def normalize(model_class, rules) + rules.each { |rule| rule.conditions = normalize_conditions(model_class, rule.conditions) } + end + + def normalize_conditions(model_class, conditions) + return conditions unless conditions.is_a? Hash + + conditions.each_with_object({}) do |(key, value), result_hash| + if value.is_a? Hash + result_hash.merge!(calculate_result_hash(model_class, key, value)) + else + result_hash[key] = value + end + result_hash + end + end + + private + + def calculate_result_hash(model_class, key, value) + reflection = model_class.reflect_on_association(key) + unless reflection + raise WrongAssociationName, "Association '#{key}' not defined in model '#{model_class.name}'" + end + + if reflection.options[:through].present? + key = reflection.options[:through] + value = { reflection.source_reflection_name => value } + reflection = model_class.reflect_on_association(key) + end + + { key => normalize_conditions(reflection.klass.name.constantize, value) } + end + end + end + end +end diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index a6780615..82fe916f 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -7,7 +7,7 @@ class Rule # :nodoc: include ConditionsMatcher include ParameterValidators attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes - attr_writer :expanded_actions + attr_writer :expanded_actions, :conditions # The first argument when initializing is the base_behavior which is a true/false # value. True for "can" and false for "cannot". The next two arguments are the action diff --git a/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb new file mode 100644 index 00000000..28e2d836 --- /dev/null +++ b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +# integration tests for latest ActiveRecord version. +RSpec.describe CanCan::ModelAdapters::ActiveRecord5Adapter do + let(:ability) { double.extend(CanCan::Ability) } + let(:users_table) { Post.table_name } + let(:posts_table) { Post.table_name } + let(:likes_table) { Like.table_name } + before :each do + connect_db + ActiveRecord::Migration.verbose = false + + ActiveRecord::Schema.define do + create_table(:users) do |t| + t.string :name + t.timestamps null: false + end + + create_table(:posts) do |t| + t.string :title + t.boolean :published, default: true + t.integer :user_id + t.timestamps null: false + end + + create_table(:likes) do |t| + t.integer :post_id + t.integer :user_id + t.timestamps null: false + end + + create_table(:editors) do |t| + t.integer :post_id + t.integer :user_id + t.timestamps null: false + end + end + + class User < ActiveRecord::Base + has_many :posts + has_many :likes + has_many :editors + end + + class Post < ActiveRecord::Base + belongs_to :user + has_many :likes + has_many :editors + end + + class Like < ActiveRecord::Base + belongs_to :user + belongs_to :post + end + + class Editor < ActiveRecord::Base + belongs_to :user + belongs_to :post + end + end + + before do + @user1 = User.create! + @user2 = User.create! + @post1 = Post.create!(title: 'post1', user: @user1) + @post2 = Post.create!(user: @user1, published: false) + @post3 = Post.create!(user: @user2) + @like1 = Like.create!(post: @post1, user: @user1) + @like2 = Like.create!(post: @post1, user: @user2) + @editor1 = Editor.create(user: @user1, post: @post2) + ability.can :read, Post, user_id: @user1 + ability.can :read, Post, editors: { user_id: @user1 } + end + + describe 'preloading of associatons' do + it 'preloads associations correctly' do + posts = Post.accessible_by(ability).includes(likes: :user) + expect(posts[0].association(:likes)).to be_loaded + expect(posts[0].likes[0].association(:user)).to be_loaded + end + end + + describe 'filtering of results' do + it 'adds the where clause correctly' do + posts = Post.accessible_by(ability).where(published: true) + expect(posts.length).to eq 1 + end + end + + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + describe 'selecting custom columns' do + it 'extracts custom columns correctly' do + posts = Post.accessible_by(ability).select('title as mytitle') + expect(posts[0].mytitle).to eq 'post1' + end + end + end +end diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 8a090aee..809e8c29 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -25,6 +25,10 @@ t.timestamps null: false end + create_table(:companies) do |t| + t.boolean :admin + end + create_table(:articles) do |t| t.string :name t.timestamps null: false @@ -32,12 +36,14 @@ t.boolean :secret t.integer :priority t.integer :category_id + t.integer :project_id t.integer :user_id end create_table(:comments) do |t| t.boolean :spam t.integer :article_id + t.integer :project_id t.timestamps null: false end @@ -54,18 +60,24 @@ end class Project < ActiveRecord::Base + has_many :comments end class Category < ActiveRecord::Base has_many :articles end + class Company < ActiveRecord::Base + end + class Article < ActiveRecord::Base belongs_to :category + belongs_to :company has_many :comments has_many :mentions has_many :mentioned_users, through: :mentions, source: :user belongs_to :user + belongs_to :project end class Mention < ActiveRecord::Base @@ -502,4 +514,27 @@ class Transaction < ActiveRecord::Base expect(Article.accessible_by(ability)).to match_array([a1]) end end + + context 'has_many through is defined and referenced differently' do + it 'recognises it and simplifies the query' do + u1 = User.create!(name: 'pippo') + u2 = User.create!(name: 'paperino') + + a1 = Article.create!(mentioned_users: [u1]) + a2 = Article.create!(mentioned_users: [u2]) + + ability = Ability.new(u1) + ability.can :read, Article, mentioned_users: { name: u1.name } + ability.can :read, Article, mentions: { user: { name: u2.name } } + expect(Article.accessible_by(ability)).to match_array([a1, a2]) + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + expect(ability.model_adapter(Article, :read)).to generate_sql(%( + SELECT DISTINCT "articles".* + FROM "articles" + LEFT OUTER JOIN "legacy_mentions" ON "legacy_mentions"."article_id" = "articles"."id" + LEFT OUTER JOIN "users" ON "users"."id" = "legacy_mentions"."user_id" + WHERE (("users"."name" = 'paperino') OR ("users"."name" = 'pippo')))) + end + end + end end diff --git a/spec/cancan/model_adapters/conditions_normalizer_spec.rb b/spec/cancan/model_adapters/conditions_normalizer_spec.rb new file mode 100644 index 00000000..fdfb7f52 --- /dev/null +++ b/spec/cancan/model_adapters/conditions_normalizer_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +RSpec.describe CanCan::ModelAdapters::ConditionsNormalizer do + before do + connect_db + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:articles) do |t| + end + + create_table(:users) do |t| + t.string :name + end + + create_table(:comments) do |t| + end + + create_table(:spread_comments) do |t| + t.integer :article_id + t.integer :comment_id + end + + create_table(:legacy_mentions) do |t| + t.integer :user_id + t.integer :article_id + end + end + + class Article < ActiveRecord::Base + has_many :spread_comments + has_many :comments, through: :spread_comments + has_many :mentions + has_many :mentioned_users, through: :mentions, source: :user + end + + class Comment < ActiveRecord::Base + has_many :spread_comments + has_many :articles, through: :spread_comments + end + + class SpreadComment < ActiveRecord::Base + belongs_to :comment + belongs_to :article + end + + class Mention < ActiveRecord::Base + self.table_name = 'legacy_mentions' + belongs_to :article + belongs_to :user + end + + class User < ActiveRecord::Base + has_many :mentions + has_many :mentioned_articles, through: :mentions, source: :article + end + end + + it 'simplifies has_many through associations' do + rule = CanCan::Rule.new(true, :read, Comment, articles: { mentioned_users: { name: 'pippo' } }) + CanCan::ModelAdapters::ConditionsNormalizer.normalize(Comment, [rule]) + expect(rule.conditions).to eq(spread_comments: { article: { mentions: { user: { name: 'pippo' } } } }) + end + + it 'normalizes the has_one through associations' do + class Supplier < ActiveRecord::Base + has_one :accountant + has_one :account_history, through: :accountant + end + + class Accountant < ActiveRecord::Base + belongs_to :supplier + has_one :account_history + end + + class AccountHistory < ActiveRecord::Base + belongs_to :accountant + end + + rule = CanCan::Rule.new(true, :read, Supplier, account_history: { name: 'pippo' }) + CanCan::ModelAdapters::ConditionsNormalizer.normalize(Supplier, [rule]) + expect(rule.conditions).to eq(accountant: { account_history: { name: 'pippo' } }) + end +end From 93ba1812b65d71ed8aea2cf5e83218259ed4484c Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 17 Mar 2019 14:11:38 +0100 Subject: [PATCH 49/54] Update CHANGELOG and version number --- CHANGELOG.md | 1 + lib/cancan/version.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd0b190..cb73ba29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#567](https://github.com/CanCanCommunity/cancancan/issues/567): Extensively run tests on different databases (sqlite and postgres). ([@coorasse][]) * [#566](https://github.com/CanCanCommunity/cancancan/issues/566): Avoid queries on session dumps (speed up error pages). ([@coorasse][]) * [#568](https://github.com/CanCanCommunity/cancancan/issues/568): Automatically freeze strings in all files. ([@coorasse][]) +* [#577](https://github.com/CanCanCommunity/cancancan/pull/577): Normalise rules traversing associations to reduce the number of joins. ([@coorasse][]) Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.com/CanCanCommunity/cancancan/wiki/Migrating-from-CanCanCan-2.x-to-3.0) diff --git a/lib/cancan/version.rb b/lib/cancan/version.rb index 12bf0365..d2233c43 100644 --- a/lib/cancan/version.rb +++ b/lib/cancan/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module CanCan - VERSION = '2.3.0'.freeze + VERSION = '3.0.0.rc1'.freeze end From 9c501c3ceda33a6609a1af5e6c1d5b4c0289a5ff Mon Sep 17 00:00:00 2001 From: Emanuel Campos Date: Fri, 29 Mar 2019 21:47:40 -0300 Subject: [PATCH 50/54] fix topic numbers --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dfbad7a1..934e297a 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ def show end ``` -### 3.1 Loaders +### 3.2 Loaders Setting this for every action can be tedious, therefore the `load_and_authorize_resource` method is provided to automatically authorize all actions in a RESTful style resource controller. @@ -94,7 +94,7 @@ See [Authorizing Controller Actions](https://github.com/CanCanCommunity/cancanca for more information. -### 3.2 Strong Parameters +### 3.3 Strong Parameters You have to sanitize inputs before saving the record, in actions such as `:create` and `:update`. From 233fc42775316e47a1b2c874c45a8a399ff019ed Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Tue, 2 Apr 2019 15:26:49 +0200 Subject: [PATCH 51/54] Update README --- README.md | 95 +++++++++++++++++++++++++------------------------------ 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index dfbad7a1..4346349b 100644 --- a/README.md +++ b/README.md @@ -6,46 +6,43 @@ [![Travis badge](https://travis-ci.org/CanCanCommunity/cancancan.svg?branch=develop)](https://travis-ci.org/CanCanCommunity/cancancan) [![Code Climate Badge](https://codeclimate.com/github/CanCanCommunity/cancancan.svg)](https://codeclimate.com/github/CanCanCommunity/cancancan) -[Wiki](https://github.com/CanCanCommunity/cancancan/wiki) | -[RDocs](http://rdoc.info/projects/CanCanCommunity/cancancan) | +[Wiki](https://github.com/CanCanCommunity/cancancan/wiki) | +[RDocs](http://rdoc.info/projects/CanCanCommunity/cancancan) | [Screencast 1](http://railscasts.com/episodes/192-authorization-with-cancan) | [Screencast 2](https://www.youtube.com/watch?v=cTYu-OjUgDw) -CanCanCan is an authorization library for Ruby >= 2.2.0 and Ruby on Rails >= 4.2 which restricts what resources a given user is allowed to access. +CanCanCan is an authorization library for Ruby >= 2.2.0 and Ruby on Rails >= 4.2 which restricts what +resources a given user is allowed to access. -All permissions can be defined in one or multiple ability files and not duplicated across controllers, views, and database queries, keeping your permissions logic in one place. +All permissions can be defined in one or multiple ability files and not duplicated across controllers, views, +and database queries, keeping your permissions logic in one place for easy maintenance and testing. It consists of two main parts: -1. **the authorizations definition library** that allows you to define the rules, for a user, -to access different objects, and provides helpers to check for those permissions. +1. **Authorizations library** that allows you to define the rules to access different objects, +and provides helpers to check for those permissions. -2. **controller helpers** that help to simplify the code in Rails Controllers by performing the loading and checking of permissions -of models for you in the controllers. +2. **Rails helpers** to simplify the code in Rails Controllers by performing the loading and checking of permissions +of models automatically and reduce duplicated code. ## Installation -Add this to your Gemfile: +Add this to your Gemfile: - gem 'cancancan', '~> 2.0' - -and run the `bundle install` command. - -For Rails < 4.2 use: + gem 'cancancan' - gem 'cancancan', '~> 1.10' - -## Getting Started +and run the `bundle install` command. -### 1. Define Abilities +## Define Abilities User permissions are defined in an `Ability` class. rails g cancan:ability -See [Defining Abilities](https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities) for details. +See [Defining Abilities](https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities) for details on how to +define your rules. -### 2. Check Abilities +## Check Abilities The current user's permissions can then be checked using the `can?` and `cannot?` methods in views and controllers. @@ -56,8 +53,9 @@ The current user's permissions can then be checked using the `can?` and `cannot? ``` See [Checking Abilities](https://github.com/CanCanCommunity/cancancan/wiki/checking-abilities) for more information +on how you can use these helpers. -### 3. Controller helpers +## Controller helpers CanCanCan expects a `current_user` method to exist in the controller. First, set up some authentication (such as [Devise](https://github.com/plataformatec/devise) or [Authlogic](https://github.com/binarylogic/authlogic)). @@ -76,8 +74,8 @@ end ### 3.1 Loaders -Setting this for every action can be tedious, therefore the `load_and_authorize_resource` method is provided to -automatically authorize all actions in a RESTful style resource controller. +Setting this for every action can be tedious, therefore the `load_and_authorize_resource` method is provided to +automatically authorize all actions in a RESTful style resource controller. It will use a before action to load the resource into an instance variable and authorize it for every action. ```ruby @@ -87,6 +85,10 @@ class ArticlesController < ApplicationController def show # @article is already loaded and authorized end + + def index + # @articles is already loaded with all articles the user is authorized to read + end end ``` @@ -102,7 +104,7 @@ For the `:update` action, CanCanCan will load and authorize the resource but *no ```ruby def update - if @article.update_attributes(update_params) + if @article.update(article_params) # hurray else render :edit @@ -110,12 +112,12 @@ def update end ... -def update_params +def article_params params.require(:article).permit(:body) end ``` -For the `:create` action, CanCanCan will try to initialize a new instance with sanitized input by seeing if your +For the `:create` action, CanCanCan will try to initialize a new instance with sanitized input by seeing if your controller will respond to the following methods (in order): 1. `create_params` @@ -146,7 +148,7 @@ class ArticlesController < ApplicationController end ``` -You can also use a string that will be evaluated in the context of the controller using `instance_eval` and needs to contain valid Ruby code. +You can also use a string that will be evaluated in the context of the controller using `instance_eval` and needs to contain valid Ruby code. load_and_authorize_resource param_method: 'permitted_params.article' @@ -156,9 +158,9 @@ Finally, it's possible to associate `param_method` with a Proc object which will See [Strong Parameters](https://github.com/CanCanCommunity/cancancan/wiki/Strong-Parameters) for more information. -### 4. Handle Unauthorized Access +## Handle Unauthorized Access -If the user authorization fails, a `CanCan::AccessDenied` exception will be raised. +If the user authorization fails, a `CanCan::AccessDenied` exception will be raised. You can catch this and modify its behavior in the `ApplicationController`. ```ruby @@ -176,7 +178,7 @@ end See [Exception Handling](https://github.com/CanCanCommunity/cancancan/wiki/exception-handling) for more information. -### 5. Lock It Down +## Lock It Down If you want to ensure authorization happens on every action in your application, add `check_authorization` to your `ApplicationController`. @@ -186,21 +188,10 @@ class ApplicationController < ActionController::Base end ``` -This will raise an exception if authorization is not performed in an action. -If you want to skip this, add `skip_authorization_check` to a controller subclass. +This will raise an exception if authorization is not performed in an action. +If you want to skip this, add `skip_authorization_check` to a controller subclass. See [Ensure Authorization](https://github.com/CanCanCommunity/cancancan/wiki/Ensure-Authorization) for more information. -## Version 2.0 - -Version 2.0 drops support for Mongoid and Sequel. - -Please use `gem 'cancancan', '~> 1.10'` for them. - -If you are interested in supporting them, contribute to the sibling gems `cancancan-sequel` and `cancancan-mongoid`. - -Version 2.0 drops also support for Rails < 4.2 and ruby < 2.2 so, again, use the version 1 of the Gem for these. - - ## Wiki Docs * [Defining Abilities](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities) @@ -212,8 +203,8 @@ Version 2.0 drops also support for Rails < 4.2 and ruby < 2.2 so, again, use the ## Mission -This repo is a continuation of the dead [CanCan](https://github.com/ryanb/cancan) project. -Our mission is to keep CanCan alive and moving forward, with maintenance fixes and new features. +This repo is a continuation of the dead [CanCan](https://github.com/ryanb/cancan) project. +Our mission is to keep CanCan alive and moving forward, with maintenance fixes and new features. Pull Requests are welcome! Any help is greatly appreciated, feel free to submit pull-requests or open issues. @@ -221,10 +212,10 @@ Any help is greatly appreciated, feel free to submit pull-requests or open issue ## Questions? -If you have any question or doubt regarding CanCanCan which you cannot find the solution to in the +If you have any question or doubt regarding CanCanCan which you cannot find the solution to in the [documentation](https://github.com/CanCanCommunity/cancancan/wiki) or our [mailing list](http://groups.google.com/group/cancancan), please -[open a question on Stackoverflow](http://stackoverflow.com/questions/ask?tags=cancancan) with tag +[open a question on Stackoverflow](http://stackoverflow.com/questions/ask?tags=cancancan) with tag [cancancan](http://stackoverflow.com/questions/tagged/cancancan) ## Bugs? @@ -234,14 +225,14 @@ If you find a bug please add an [issue on GitHub](https://github.com/CanCanCommu ## Development -CanCanCan uses [appraisals](https://github.com/thoughtbot/appraisal) to test the code base against multiple versions +CanCanCan uses [appraisals](https://github.com/thoughtbot/appraisal) to test the code base against multiple versions of Rails, as well as the different model adapters. When first developing, you need to run `bundle install` and then `appraisal install`, to install the different sets. You can then run all appraisal files (like CI does), with `appraisal rake` or just run a specific set `appraisal activerecord_5.0 rake`. -See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop/CONTRIBUTING.md) and +See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop/CONTRIBUTING.md) and [spec/README](https://github.com/CanCanCommunity/cancancan/blob/master/spec/README.rdoc) for more information. @@ -251,10 +242,10 @@ See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop Thanks to [Renuo AG](https://www.renuo.ch) for currently maintaining and supporting the project. -Also many thanks to the [CanCanCan contributors](https://github.com/CanCanCommunity/cancancan/contributors). +Also many thanks to the [CanCanCan contributors](https://github.com/CanCanCommunity/cancancan/contributors). See the [CHANGELOG](https://github.com/CanCanCommunity/cancancan/blob/master/CHANGELOG.md) for the full list. -CanCanCan was inspired by [declarative_authorization](https://github.com/stffn/declarative_authorization/) and -[aegis](https://github.com/makandra/aegis). +CanCanCan was inspired by [declarative_authorization](https://github.com/stffn/declarative_authorization/) and +[aegis](https://github.com/makandra/aegis). From 2028467d6d740d822687b94b6d1df965afa60e3a Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Tue, 2 Apr 2019 15:38:58 +0200 Subject: [PATCH 52/54] Use Post instead of Article --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 38dc85ea..cf6a0aca 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,25 @@ User permissions are defined in an `Ability` class. rails g cancan:ability +Here follows an example of rules defined to read a Post model. +```ruby +class Ability + include CanCan::Ability + + def initialize(user) + can :read, Post, public: true + + if user.present? # additional permissions for logged in users (they can read their own posts) + can :read, Post, user_id: user.id + + if user.admin? # additional permissions for administrators + can :read, post + end + end + end +end +``` + See [Defining Abilities](https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities) for details on how to define your rules. @@ -47,14 +66,27 @@ define your rules. The current user's permissions can then be checked using the `can?` and `cannot?` methods in views and controllers. ```erb -<% if can? :update, @article %> - <%= link_to "Edit", edit_article_path(@article) %> +<% if can? :read, @post %> + <%= link_to "View", @post %> <% end %> ``` See [Checking Abilities](https://github.com/CanCanCommunity/cancancan/wiki/checking-abilities) for more information on how you can use these helpers. +## Fetching records + +One of the key features of CanCanCan, compared to other authorization libraries, +is the possibility to retrieve all the objects that the user is authorized to, for example, read. +The following: + +```ruby + Post.accessible_by(current_ability) +``` + +will reuse your previously defined rules to ensure that the user retrieves only a list of posts he can read. +See [Fetching records](https://github.com/CanCanCommunity/cancancan/wiki/Fetching-Records) for details. + ## Controller helpers CanCanCan expects a `current_user` method to exist in the controller. @@ -67,8 +99,8 @@ The `authorize!` method in the controller will raise an exception if the user is ```ruby def show - @article = Article.find(params[:id]) - authorize! :read, @article + @post = Post.find(params[:id]) + authorize! :read, @post end ``` @@ -79,15 +111,15 @@ automatically authorize all actions in a RESTful style resource controller. It will use a before action to load the resource into an instance variable and authorize it for every action. ```ruby -class ArticlesController < ApplicationController +class PostsController < ApplicationController load_and_authorize_resource def show - # @article is already loaded and authorized + # @post is already loaded and authorized end def index - # @articles is already loaded with all articles the user is authorized to read + # @posts is already loaded with all posts the user is authorized to read end end ``` @@ -104,7 +136,7 @@ For the `:update` action, CanCanCan will load and authorize the resource but *no ```ruby def update - if @article.update(article_params) + if @post.update(post_params) # hurray else render :edit @@ -112,8 +144,8 @@ def update end ... -def article_params - params.require(:article).permit(:body) +def post_params + params.require(:post).permit(:body) end ``` @@ -121,7 +153,7 @@ For the `:create` action, CanCanCan will try to initialize a new instance with s controller will respond to the following methods (in order): 1. `create_params` -2. `_params` such as `article_params` (this is the default convention in rails for naming your param method) +2. `_params` such as `post_params` (this is the default convention in rails for naming your param method) 3. `resource_params` (a generically named method you could specify in each controller) Additionally, `load_and_authorize_resource` can now take a `param_method` option to specify a custom method in the controller to run to sanitize input. @@ -129,11 +161,11 @@ Additionally, `load_and_authorize_resource` can now take a `param_method` option You can associate the `param_method` option with a symbol corresponding to the name of a method that will get called: ```ruby -class ArticlesController < ApplicationController +class PostsController < ApplicationController load_and_authorize_resource param_method: :my_sanitizer def create - if @article.save + if @post.save # hurray else render :new @@ -143,18 +175,18 @@ class ArticlesController < ApplicationController private def my_sanitizer - params.require(:article).permit(:name) + params.require(:post).permit(:name) end end ``` You can also use a string that will be evaluated in the context of the controller using `instance_eval` and needs to contain valid Ruby code. - load_and_authorize_resource param_method: 'permitted_params.article' + load_and_authorize_resource param_method: 'permitted_params.post' Finally, it's possible to associate `param_method` with a Proc object which will be called with the controller as the only argument: - load_and_authorize_resource param_method: Proc.new { |c| c.params.require(:article).permit(:name) } + load_and_authorize_resource param_method: Proc.new { |c| c.params.require(:post).permit(:name) } See [Strong Parameters](https://github.com/CanCanCommunity/cancancan/wiki/Strong-Parameters) for more information. From c0575a3b2b386a014b987d199d5d0d2d89f5396b Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Wed, 3 Apr 2019 09:56:41 +0200 Subject: [PATCH 53/54] Improve README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cf6a0aca..cdf0bfa5 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,14 @@ on how you can use these helpers. ## Fetching records One of the key features of CanCanCan, compared to other authorization libraries, -is the possibility to retrieve all the objects that the user is authorized to, for example, read. +is the possibility to retrieve all the objects that the user is authorized to access. The following: ```ruby Post.accessible_by(current_ability) ``` -will reuse your previously defined rules to ensure that the user retrieves only a list of posts he can read. +will use your rules to ensure that the user retrieves only a list of posts that can be read. See [Fetching records](https://github.com/CanCanCommunity/cancancan/wiki/Fetching-Records) for details. ## Controller helpers From 7e24de14306234d8de6df10075ff6761f942aa1a Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Wed, 3 Apr 2019 10:55:09 +0200 Subject: [PATCH 54/54] Bump version --- lib/cancan/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cancan/version.rb b/lib/cancan/version.rb index d2233c43..a59709d6 100644 --- a/lib/cancan/version.rb +++ b/lib/cancan/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module CanCan - VERSION = '3.0.0.rc1'.freeze + VERSION = '3.0.0'.freeze end