Skip to content

fix: prevent accessible_by from permanently overwriting shared rule conditions#892

Open
SAY-5 wants to merge 1 commit into
CanCanCommunity:developfrom
SAY-5:fix/conditions-normalizer-mutation-876
Open

fix: prevent accessible_by from permanently overwriting shared rule conditions#892
SAY-5 wants to merge 1 commit into
CanCanCommunity:developfrom
SAY-5:fix/conditions-normalizer-mutation-876

Conversation

@SAY-5

@SAY-5 SAY-5 commented May 25, 2026

Copy link
Copy Markdown

What does this fix?

Calling accessible_by for one model class permanently overwrites rule.conditions on the shared Rule objects stored in the ability's @rules. A subsequent can? check for a different model that shares the same rule then uses the already-normalized conditions, which may reference associations that do not exist on that model, causing NoMethodError or wrong results.

Reproduction (from #876)

# Ability definition - rule applies to both Project and Activity
can :read, [Project, Activity], client: { id: current_client.id }

# Project belongs_to :client (direct)
# Activity has_many-through path to client via :project

# Step 1 - works fine before accessible_by is called
can?(:read, @project)  # => correct result

# Step 2 - triggers ConditionsNormalizer for Activity
# Rewrites rule.conditions from { client: { id: x } }
# to { project: { client: { id: x } } } on the SHARED Rule object
Activity.accessible_by(current_ability)

# Step 3 - now broken
can?(:read, @project)  # => NoMethodError: undefined method `project' for Project

Fix

ActiveRecordAdapter#initialize now maps @compressed_rules through Rule#dup before passing to StiNormalizer and ConditionsNormalizer. The dup gives each adapter its own copy of the rule shell, so conditions= assignments from the normalizers are local and never touch the originals in @rules.

@compressed_rules = base_rules.map(&:dup)

Tests

Added a regression spec to conditions_normalizer_spec.rb:

  • Confirms rule.conditions is unchanged after accessible_by is called (object identity preserved)
  • Fails without the fix (rule.conditions becomes the normalized form)
  • Passes with the fix

All 305 examples pass (0 failures, 2 pending) across both defined and random order.

Closes #876

…ng shared conditions

ConditionsNormalizer replaces rule.conditions with a newly computed hash
for each model class. When rules_compressor_enabled is false (the default
fallback), @compressed_rules references the same Rule objects stored in
the ability's shared @rules. Calling accessible_by for one model class
permanently overwrites conditions on those shared Rule objects, so
subsequent can? checks for other models using the same rule fail or raise
NoMethodError.

Fix by mapping @compressed_rules through Rule#dup before passing to the
normalizers. The dup gives each adapter its own copy of the Rule shell
so that conditions= writes are local and never touch the originals.

Fixes CanCanCommunity#876

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
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.

accessible_by permanently modifies a rule's conditions via normalize_conditions, causing can? to fail

1 participant