Struggling with some more advanced authorizations using CanCan

938 views
Skip to first unread message

Christian A. Strømmen

unread,
Nov 22, 2014, 9:00:19 AM11/22/14
to canc...@googlegroups.com
Company1 has sub-company Company2, Company2 has sub-company Company3 and Company3 has sub-company Company4.

User belongs to Company2 (not Company1).

User should then be able to manage all Companies starting at Company2, meaning it should be able to manage Company2, Company3 and Company4, but not Company1.

So I tried adding:
can :manage, Company, :company_id => user.company.id
can :manage, Company, :company => { :company_id => user.company.id }
can :manage, Company, :company => { :company => { :company_id => user.company.id } } 

So the 2 first lines here seem to work, and I can see both Company2 and Company3. But Company4 - which is supposed to be handled by the third line - does not seem to work.

So… Why isn't that line working? And how can I get this all on one line and handled unlimited levels of companies instead of adding one line for each new level I add?

brite

unread,
Nov 24, 2014, 11:38:36 AM11/24/14
to canc...@googlegroups.com
Self joins of an arbitrary length are difficult to express in a chained active record scope. What I would do, is build a scope on company that recursively looks up all the companies under its control, something like:

class Company < ActiveRecord::Base
 
# ..

 
def subsidary_of
   
# A list of companies under this company... recursively in Rails,
   
# raw SQL (recursive self referential lookup), or something like
   
# the ancestry gem
 
end
end

Then you can specify the Cancancan rule using the block syntax:

can :manage, Company do |company|
  company
== user.company || company.subsidary_of.include?(user.company)
end

Hope that helps!

Christian A. Strømmen

unread,
Nov 25, 2014, 7:41:28 AM11/25/14
to canc...@googlegroups.com
That was absolutely awesome, unfortunately I still run into problems here.

I implemented this in the Company class:
  def subsidiary_of
    supercompanies = []
    unless company.nil?
      supercompanies << company
      supercompanies + self.company.subsidiary_of
    end
    return supercompanies
  end

Which then returns an array of all super-companies of the current company. After I got that working I figured the problem was solved, but when I implement the can :manage rules you listed above I get this:

The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for :index Company(id: integer, name: string, description: text, company_id: integer, created_at: datetime, updated_at: datetime)


:(

brite

unread,
Nov 25, 2014, 12:30:10 PM11/25/14
to canc...@googlegroups.com

Hi Christian!

Look up more about Cancancan and block syntax, to be able to load and authorize collections (:index type of routes), you need to also specify a class level scope, since we can’t run each instance through the block to see which ones are valid.

Breaks down like this:

can [:ability], Model, Model.scope_to_select_on_index_action do |model_instance|
  model_instance
.condition_to_evaluate_for_new_create_edit_update_destroy
end


More info: https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities-with-Blocks#block-conditions-with-scopes

Christian A. Strømmen

unread,
Nov 26, 2014, 3:19:13 AM11/26/14
to canc...@googlegroups.com
This is awesome, definitely learned a lot here! :)

Looks like there's no way to do what I want though. I can't pass the user's company in for the scope_to_select_on_index_action, so the best I've found is this:
        can :manage, Company, Company.has_parent do |company|
            company == user.company || company.subsidiary_of.include?(user.company)
        end


And the has_parent is just:
  scope :has_parent, -> { 
    where("company_id is not null") 
  }

So now the user can read all companies (as long as they have a parent), but only edit his own company or any subcompany (or subcompany of subcompany, etc).

Is there anything I'm missing here? Is there a way to pass the model instance in for the scope_to_select_on_index_action?

brite

unread,
Nov 26, 2014, 3:29:33 AM11/26/14
to canc...@googlegroups.com
You can send parameters to scopes: Company.has_parent(user.company) and adjust your scope like:

def self.has_parent(company)
 
where(...)
end

Christian A. Strømmen

unread,
Nov 26, 2014, 3:50:13 AM11/26/14
to canc...@googlegroups.com
Ah, of course. So I tried:
        can :manage, Company, Company.subcompanies_of(user.company) do |company|
            company == user.company
        end

And:
  def self.subcompanies_of(the_company)
    subcompanies = [the_company]
    unless the_company.companies.nil?
      the_company.companies.each do |subcompany|
        subcompanies = subcompanies + Company.subcompanies_of(subcompany)
      end
    end
    return subcompanies
  end

Which (in my head) should allow me to list all companies that are subcompanies of the user's company (and his own), but only allow him to edit his own. Again though I'm hitting on a problem that I don't quite understand.

undefined method `include?' for #<Company:0x007fe4648d3768>



However I'm not doing include? anywhere in my code.

The funny thing is, if I just change the return value in my subcompanies_of() method and just return and empty array there, I get all companies listed.

I am so confused.

brite

unread,
Dec 1, 2014, 5:08:44 AM12/1/14
to canc...@googlegroups.com
Not sure about this one... sounds like you're returning an instance somewhere instead of an enumerable... but i'd need to see more.
Reply all
Reply to author
Forward
0 new messages