I ended up patching cancancan for the following usecase: teach condition_match? to handle ActiveRecord::Relation values.
Would it make sense to add it to the gem itself?
CONTEXT
CanCanCan ability conditions accept both arrays and AR relations as condition values:
can :read, UserContract, team_id: [1, 2, 3] # Array → IN list in SQL
can :read, UserContract, team_id: Team.select(:id) # Relation → subquery in SQL
We use AR::Relation to avoid IN lists with hundreds of bind parameters, which degrade query plan quality and prevent plan caching.
THE BUG
CanCanCan's condition_match? is used for two things:
- accessible_by(ability) — builds SQL scopes. AR::Relation works perfectly here.
- can?(permission, instance) — checks a single object in Ruby. Broken for relations.
The original code in conditions_matcher.rb:
when Enumerable then value.include?(attribute)
AR::Relation includes Enumerable, so it falls into this branch. But Enumerable#include?
iterates over the relation records and compares each with ==. Since the relation yields
AR objects (e.g., Team instances) and attribute is a primitive (e.g., integer team_id),
Team.new(id: 5) == 5 is always false — every instance-level can? check returns false.
THE FIX
Prepend a module that intercepts condition_match? when value is an AR::Relation,
using exists? with the model's primary key instead of Enumerable#include?.
All our subquery methods select(:id) from the canonical model, so primary_key is
always 'id' and value.where(pk => attribute).exists? is always correct.
module CanCanCanArRelationConditionMatch
private
def condition_match?(attribute, value)
if value.is_a?(ActiveRecord::Relation)
# See comment above. Must be checked before Enumerable since AR::Relation
# includes Enumerable but Enumerable#include? compares objects, not IDs.
value.where(value.primary_key => attribute).exists?
else
super
end
end
end
CanCan::ConditionsMatcher.prepend(CanCanCanArRelationConditionMatch)
I ended up patching cancancan for the following usecase: teach condition_match? to handle ActiveRecord::Relation values.
Would it make sense to add it to the gem itself?
CONTEXT
CanCanCan ability conditions accept both arrays and AR relations as condition values:
We use AR::Relation to avoid IN lists with hundreds of bind parameters, which degrade query plan quality and prevent plan caching.
THE BUG
CanCanCan's condition_match? is used for two things:
The original code in conditions_matcher.rb:
AR::Relation includes Enumerable, so it falls into this branch. But Enumerable#include?
iterates over the relation records and compares each with ==. Since the relation yields
AR objects (e.g., Team instances) and attribute is a primitive (e.g., integer team_id),
Team.new(id: 5) == 5 is always false — every instance-level can? check returns false.
THE FIX
Prepend a module that intercepts condition_match? when value is an AR::Relation,
using exists? with the model's primary key instead of Enumerable#include?.
All our subquery methods select(:id) from the canonical model, so primary_key is
always 'id' and value.where(pk => attribute).exists? is always correct.