Skip to content

teach condition_match? to handle ActiveRecord::Relation values #889

Description

@apneadiving

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:

  1. accessible_by(ability) — builds SQL scopes. AR::Relation works perfectly here.
  2. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions