Rails 3.2.6 breaks conditions in nested hashes #646

Open
edruder opened this Issue Jun 13, 2012 · 40 comments

Comments

Projects
None yet
9 participants

edruder commented Jun 13, 2012

Given a stock Rails 3.2.x app with the following AR models (and migrations):

class A < ActiveRecord::Base
  belongs_to :b
end

class B < ActiveRecord::Base
  belongs_to :c
end

class C < ActiveRecord::Base
  attr_accessible :foo
end

And the following ability in CanCan:

can :read, A, :b => {:c => {:foo => true}}

In Rails 3.2.6, the call to load_resource will result in bad SQL being executed (and failing). In Rails 3.2.5, it works as expected.

As I read it, a comment on the Rails bug that causes this, rails/rails#6718, states that the ActiveRecord behavior that CanCan depends on for this to work is unintended.

Collaborator

andhapp commented Jun 13, 2012

@eruder: Thanks for reporting this. Would you like to take a stab at it and create a pull request?

edruder commented Jun 13, 2012

@andhapp If I get some time, I'd love to give it a shot.

BTW, the Rails bug has been closed, as the behavior is unintended and the query can be written without the nesting. I.e., the ActiveRecord query that the CanCan ability (above) ultimately resolves to is:

A.joins(:b => :c).where(:b => {:cs => {:foo => true}})

And the following AR query is equivalent, and works in Rails 3.2.5 & 3.2.6:

A.joins(:b => :c).where(:cs => {:foo => true})

You can rephrase the CanCan ability using a scope on A and a block. I didn't come up with (but didn't try very hard) an equivalent ability using hash notation that works in Rails 3.2.6--is there one?

the8472 commented Jun 18, 2012

You can rephrase the CanCan ability using a scope on A and a block.

That doesn't work with multiple cancan rules as you can only have one scoped rule per class. Therefore it's currently not possible to combine multiple complex rules.

Simplified Example:

can :edit, Comment, {:thread => {:owner_id => @current_user.id}
can :edit, Comment, {:thread => :board => :users => {:id => :@current_user.id, :is_mod => true}

This requires joins and nesting with the current cancan syntax. Using a scope + block means i'd have to automatically generate the scope and the block separately from merged hashes. Something that cancan did for me so far.

the8472 commented Jun 18, 2012

Since I need this working right now: monkeypatch/fix, white-hot from the foundry, untested, use at your own risk etc. etc. https://gist.github.com/2948257 (requires squeel)

rafakuch commented Oct 6, 2012

I built a method to dig into the conditions hash and get the portions that matters...

Not fully tested but It seems to work... Any thoughts would be appreciated!

https://gist.github.com/3842277

the8472 commented Oct 8, 2012

Any thoughts would be appreciated!

You might want to use association reflections on the classes to determine the actual table names.

E.g. can :manage, A, {:foo => {:bar => :baz}} should result in something like this:

table_name = A.reflect_on_association(:foo).klass.table_name
col_name = :"#{table_name}.bar"

conditions = {col_name => "baz"}

does anybody have a working production-quality fix for this yet?

I tried the referenced gist above (https://gist.github.com/3842277).
That fix does not work with multiple conditions in the last hash.

For example:

can :manage, Note, :board => {:project => {:participations => {:user_id => user.id, :permission => "owner"}}}

I changed the gist slightly to work with multiple conditions. see https://gist.github.com/4031511

(I did not try https://gist.github.com/2948257, because i did not want to introduce another dependency)

rafakuch commented Nov 7, 2012

@vollnhals you're right, I haven't noticed that

Well, at least I know I'm hitting a known bug. Is there any plan to roll https://gist.github.com/4031511 into 2.0?

Additionally, trying @vollnhals's fix, I've still got an issue where a join isn't being performed on the column specified as :source in the has_many :through relationship. Example:

ability.rb
can :read, :submissions, competition: { judges: {id: user.id } }

competition.rb
has_many :judges, :through => :submissions, :conditions => {submissions: {role: 2}}, :source => :user

So I get an error no such column: users.id because the inner join on Users never happens.

Last comment, I promise. Also hitting this issue with @vollnhals's fix: undefined methodto_sym' for Thu, 08 Nov 2012:Date`
which is coming from this ability:

cannot :read, :competitions, ['deadline < ?', Date.today] do |c|
  c.expired
end

the8472 commented Nov 8, 2012

Yet another issue with the AR-Adapter...

Here are my collected fixes for the AR adapter (again, relying on squeel), maybe it will help: https://gist.github.com/4039777

  • converts inner joins to outer joins as necessary
  • allows for nested conditions
  • since it's using squeel it will give nested joins a better treatment and match conditions and joined table names appropriately
  • can merge scope based conditions (no more warnings about unmergeable conditions), this part is far from perfect though

@the8472, looks great. Dumb question: what needs to go in the Squeel initializer for everything to work as designed?

the8472 commented Nov 8, 2012

This is what I'm using right now:

Squeel.configure do |config|
  config.load_core_extensions :hash, :symbol
end

At least the symbol extensions are necessary (for the outer joins), the hash ones probably not.

You have to be aware of this caveat though: https://github.com/ernie/squeel#symbols-as-the-value-side-of-a-condition-in-normal-where-clauses

If you use symbols instead of strings as values in any active record conditions hash in your application you'll get some breakage.

The symbol conditions seems to be my issue. Here we go a-replacing.

@the8472 , this doesn't seem to resolve the issue of creating the join when there's a has_many :through, :source, and seems to introduce a new issue with a scope based condition (drops the name of the column for some reason). Probably user error on my end, in any case.

the8472 commented Nov 8, 2012

I think I am using has_many through in my cancan abilities and they're working. So your problem might be that the conditions don't work properly with nested hashes.

Try this:

has_many :judge_submissions, :class_name => Submission.name, :conditions => {role: 2} # just guessing at your structure here
has_many :judges, :through => :judge_submissions, :source => :user

I usually auto-generated through conditions in pairs for that reason.

the8472 commented Nov 8, 2012

Probably user error on my end, in any case.

Not necessarily. This monkey patch is only tailored to the use-cases that i've needed fixed. There might be edge-cases that are not covered.

It's by no means a clean solution. It's more of a stop-gap measure to work around an otherwise - from my perspective - totally broken cancan.

Per your comment, #646 (comment), I updated my model, which makes a ton of sense. Looks like things are working in that area. Thanks so much.

Finally, any thoughts on why this rule would break things?

cannot :read, :competitions, ['deadline < ?', Date.today] do |c|
  c.expired
end

Given this scope:

scope :expired, -> { where("deadline < ?", Date.today) }

the8472 commented Nov 8, 2012

Here's my cancan rule, does it look right?

Pass in the class directly, I don't think cancan does singularize magic: can :read, Submission, ...

Finally, any thoughts on why this rule would break things?

The where clause/scope goes to the 3rd parameter, the ruby evaluation for already-loaded instances into the block. And with my patch + scopes in a cannot rule... might very well break.

Thanks for the response. I'll debug now.

I'm using CanCan 2.0, which is why I'm passing the plural symbol (per the documentation...?)

Also could be why I'm running into extra special breaking cases.

Finally, the conditions are all working now, but the accessible_by call is breaking. Much more tractable.

the8472 commented Nov 8, 2012

Oh, this was written for 1.6.8

Figured.

For what it's worth, clobbered the last line of the case statement and everything seems to be working. Have to write some tests to make sure I'm not missing anything, but manual testing looks ok.

the8472 commented Nov 8, 2012

clobbered the last line of the case statement

Careful with that one, it's responsible for combining multiple conditions, especially negated ones.

I saw. I found the fact that anything worked afterwards surprising. Still digging.

the8472 commented Nov 15, 2012

I completely rewrote the patch since i realized that i was doing more work to accomodate existing infrastructure.

It's basically a pure squeel-adapter now replacing the AR adapter.

https://gist.github.com/4039777/9309e24065d4f3eaadbd5e7ff447acac53bd9deb

Again, mostly untested.

Excellent, I'll give it a test run shortly.

One issue I'm hitting with this:

model:

has_many :judge_submissions, :class_name => Submission.name, :conditions => {role: 2}
has_many :judges, :through => :judge_submissions, :source => :user

generated query:
"judges"."id" = 9

It seems like it's not picking up the source and conditions across the split association.

Actually, is the :source at all. Collapsed my relations and ran into the same issue.

the8472 commented Nov 16, 2012

Updated the gist, try that.

That seems to have fixed the issue. I'm still have a problem with the combined sql/block rule definitions. eg,

cannot :read, :competitions, ['deadline < ?', Date.today] do |c|
  c.deadline < Date.today
end

but everything else seems to be working. (I'm still in cancan 2.0, so that could well be the reason for the issue)

the8472 commented Nov 16, 2012

I would recommend using scopes for the SQL there (i'm not using raw sql statements, hence i didn't think about supporting them). Should be easy enough to fix though.

And wrt. to blocks. I think that changed in cancan 2. Have a look at the wiki and the can-rule API in the sourcecode.

the8472 commented Nov 16, 2012

Ok, raw SQL should work too now with 1.6.8. Everything else should just be a matter of the rule definition API.

Switched to scope + block and everything seems to be working on sqlite and postgres -- great patch! Thanks.

dieb commented Jan 11, 2013

Hello!

I just flipped through this thread because I had the same problem. Thanks everyone for all the comments.

Could someone clear this for me: Is this a bug on AR or cancan?

Anyone working on fixing this?

I was wondering if it would be possible to fix this within cancan (that is, without third-parties).

Thanks!

the8472 commented Jan 11, 2013

@dieb, it's mostly a bug in cancan since it relied on AR behavior which was unintended.

But the real problem is that cancan's rules cannot be mapped completely to the active record api. They can only be mapped to arel or squeel nodes. The main problem is the lack of outer joins and aliasing. Even if you want to approximate the behavior with active record you still have to perform reflection on the model relations to extract the proper table names for qualified column names. @rafakuch posted a very crude approach like that above, but as i said, it's not a correct representation of cancan rule semantics.

This is definitely going to be more of an issue now that people are being urged to use rails 3.2.11 for the latest security patches. Is cancan even being actively developed anymore?

xhoy commented Jul 1, 2014

Thanks for your submission! The ryanb/cancan repository has been inactive since Sep 06, 2013.
Since only Ryan himself has commit permissions, the CanCan project is on a standstill.

CanCan has many open issues, including missing support for Rails 4. To keep CanCan alive, an active fork exists at cancancommunity/cancancan. The new gem is cancancan. More info is available at #994.

If your pull request or issue is still applicable, it would be really appreciated if you resubmit it to CanCanCan.

We hope to see you on the other side!

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