Skip to content

STI ability overwrites previously defined STI rules #874

@23tux

Description

@23tux

Steps to reproduce

The order of can statements affects the SQL that is generated:

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"
  gem "rails", "= 7.1.5.1"
  gem "cancancan", "= 3.6.1", require: false # require false to force rails to be required first
  gem "sqlite3"
end

require "active_record"
require "cancancan"
require "minitest/autorun"
require "logger"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :vehicles, force: true do |t|
    t.string :type
  end
end

class Vehicle < ActiveRecord::Base; end
class Car < Vehicle; end

class Ability
  include CanCan::Ability

  def initialize
    can :index, Vehicle
    can :index, Car
  end
end

class BugTest < Minitest::Test
  def test_bug
    vehicle = Vehicle.create!
    car = Car.create!

    ability = Ability.new

    # These assertions pass
    assert_equal true, ability.can?(:index, Vehicle)
    assert_equal true, ability.can?(:index, Car)
    assert_equal [vehicle, car], Vehicle.accessible_by(ability, :index).to_a
  end
end
D, [2025-01-14T13:10:25.471243 #1978] DEBUG -- :   Vehicle Load (0.1ms)  SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" = ?  [["type", "Car"]]
F

Failure:
BugTest#test_bug [bug.rb:46]:
--- expected
+++ actual
@@ -1 +1 @@
-[#<Vehicle id: 1, type: nil>, #<Car id: 2, type: "Car">]
+[#<Car id: 2, type: "Car">]

Expected behavior

When using Vehicle.accessible_by(ability, :index) I would expect to have access to all vehicles, the generated SQL produces an OR statement in cancancan 3.5.0 but truncates the statement to only include the last defined can :index, XXXX rule.

Actual behavior

Demonstrated best by looking at the SQL:

class Ability
  include CanCan::Ability

  def initialize
    can :index, Vehicle
    can :index, Car
  end
end

Vehicle.accessible_by(Ability.new, :index).to_sql
=> SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" = 'Car'

Switching the order of the can statements:

class Ability
  include CanCan::Ability

  def initialize
    can :index, Car
    can :index, Vehicle
  end
end

Vehicle.accessible_by(Ability.new, :index).to_sql
=> SELECT "vehicles".* FROM "vehicles"

IMO it should not matter, in which order the can statements are written.

System configuration

Rails version: Tested in 7.1 and 7.2

Ruby version: 3.3.3

CanCanCan version 3.6.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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