Skip to content

Fix can? for ActiveRecord relation-based conditions#890

Open
olistik wants to merge 1 commit intoCanCanCommunity:developfrom
olistik:feature/can-support-ar-select
Open

Fix can? for ActiveRecord relation-based conditions#890
olistik wants to merge 1 commit intoCanCanCommunity:developfrom
olistik:feature/can-support-ar-select

Conversation

@olistik
Copy link
Copy Markdown

@olistik olistik commented Apr 6, 2026

This PR fixes an inconsistency between accessible_by and can? when an ability condition uses an ActiveRecord::Relation as the condition value.

Example:

can :read, Article, id: Article.where(user_id: user.id).select(:id)

Before this change:

  • Article.accessible_by(ability) correctly returns the expected records
  • ability.can?(:read, article) incorrectly returns false

After this change:

  • accessible_by continues to behave correctly
  • can? now returns the expected result for matching records

Problem

accessible_by works because ActiveRecord conditions are translated into SQL.

However, can? performs an in-memory condition match, and when the condition value is an ActiveRecord::Relation, it was comparing the record attribute against relation objects rather than against the selected scalar values.

This caused behavior like:

user = User.create!(name: 'Arthur Dent')
article = Article.create!(name: 'How to fly', user:)

ability = Ability.new(user)
ability.can :read, Article, id: Article.where(user_id: user.id).select(:id)

Article.accessible_by(ability) # => includes article
ability.can?(:read, article)   # => false (before)
ability.can?(:read, article)   # => true  (after)

Change

This PR updates CanCan::ConditionsMatcher#condition_match? so that when a condition value is an ActiveRecord::Relation, it is normalized into the selected record values before performing the in-memory match.

This makes can? consistent with accessible_by for relation-backed conditions such as:

id: Article.where(user_id: user.id).select(:id)

Test coverage

Added a spec in:

spec/cancan/model_adapters/active_record_adapter_spec.rb

The example verifies that:

  • accessible_by returns the expected matching record
  • can? returns true for the matching record
  • can? returns false for a non-matching record

Why this matters

Using subqueries / relation-backed conditions is already a valid and useful pattern in ability definitions, especially when trying to avoid eager-loading IDs into Ruby arrays.

This change ensures that:

  • SQL-backed authorization (accessible_by)
  • in-memory authorization (can?)

follow the same logic and produce consistent results.


Notes for reviewers

This change is intentionally narrow and only affects in-memory condition matching when a condition value is an ActiveRecord::Relation.

It does not change SQL generation or accessible_by behavior, which was already correct.

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.
@olistik
Copy link
Copy Markdown
Author

olistik commented Apr 6, 2026

Fixes #889

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant