Skip to content

Fix can? for ActiveRecord relation-based conditions#890

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

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

Conversation

@olistik

@olistik olistik commented Apr 6, 2026

Copy link
Copy Markdown

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.

@olistik

olistik commented Apr 6, 2026

Copy link
Copy Markdown
Author

Fixes #889

@coorasse coorasse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very surprised that this was not covered already. Thank you. I'd love to see also a test covering the same but where we don't use id but, for example, name as a condition. This would make it clear that is not something specific for the primary key, but for any other column as well. Basically add a second test where we do the same check but with the ability

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

@coorasse coorasse self-assigned this Jun 27, 2026
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 olistik force-pushed the feature/can-support-ar-select branch from 18ce8ad to f1bf0b4 Compare June 28, 2026 14:57
@olistik

olistik commented Jun 28, 2026

Copy link
Copy Markdown
Author

@coorasse I can see the reason to add that second test example.
I added it here and then rebased. 🎩

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.

2 participants