From 18ce8ad800658629d4c1c2cb1382cd4794c2a5a3 Mon Sep 17 00:00:00 2001 From: olistik Date: Mon, 6 Apr 2026 02:00:04 +0200 Subject: [PATCH] feature: Handles ActiveRecord relation values in ConditionsMatcher Normalize ActiveRecord relation values in ConditionsMatcher When an ability condition uses an ActiveRecord relation such as: can :read, Article, id: Article.where(user_id: user.id).select(:id) `accessible_by` works correctly because the relation is translated to SQL, but `can?` previously failed during in-memory matching. This change converts relation values to their selected scalar values before matching so `can?` behaves consistently with `accessible_by`. Add a spec for relation-backed `id` conditions. --- lib/cancan/conditions_matcher.rb | 3 +++ .../model_adapters/active_record_adapter_spec.rb | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/cancan/conditions_matcher.rb b/lib/cancan/conditions_matcher.rb index c1a97b96..8120a00a 100644 --- a/lib/cancan/conditions_matcher.rb +++ b/lib/cancan/conditions_matcher.rb @@ -88,6 +88,9 @@ def matches_hash_conditions?(adapter, subject, conditions) end def condition_match?(attribute, value) + if defined?(ActiveRecord) && value.is_a?(ActiveRecord::Relation) + value = value.map { |v| v.attributes.compact_blank.values[0] } + end case value when Hash hash_condition_match?(attribute, value) diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 810af592..6ca40f8d 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -276,6 +276,19 @@ class User < ActiveRecord::Base expect(Article.accessible_by(@ability)).to match_array([article1]) end + it 'matches records for can? when a condition value is an ActiveRecord relation selecting ids' do + user = User.create!(name: 'Arthur Dent') + article = Article.create!(name: 'How to fly', user: user) + other_article = Article.create!(name: 'Mostly Harmless') + ability = Ability.new(user) + + ability.can :read, Article, id: Article.where(user_id: user.id).select(:id) + + expect(Article.accessible_by(ability)).to match_array([article]) + expect(ability.can?(:read, article)).to eq(true) + expect(ability.can?(:read, other_article)).to eq(false) + 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)