05
Jul
08

Inverting Permission-Based Filtering with named_scope

The addition of named_scope in Rails 2.1 has revealed several elegant approaches for modeling complex problem domains in ActiveRecord.  One I came across recently while working on an app with a somewhat complex permissions system was a permission-based filtering mechanism.  In this case I was dealing with permission for a given user to manage an “office”, while a user could be at one of three permission “levels”, one of which has specific office assignments (or it’s assumed all are manageable if user.can_manage_all_offices is true). Lot’s of necessary conditional logic there.

Now a normal approach to such a task to “show a list of offices that the user can manage” (for a drop-down for an interface perhaps) might be something like this:

# In controller:
if current_user.can_manage_company?
  @offices = Office.find(:all)
elsif current_user.office_access_level? && current_user.can_manage_all_offices?
  @offices.find(:all)
elsif current_user.office_access_level?
  @offices.find(:all, :conditions => {:id => current_user.manageable_offices.map(&:id) })
else
  @offices = []
end

But this approach starts at the user level and, using a lot of conditional logic baked right into places we don’t want, makes different calls to Office, which isn’t very DRY, and certainly not consistent with fat models skinny controllers.  So I considered inverting this approach and instead starting with an office, and asking it what “is manageable by” a given user.  Consider this alternative:

# In office.rb
named_scope :manageable_by, lambda {|user|
  case
  when user.can_manage_company? then {}
  when user.office_access_level? && user.can_manage_all_offices? then {}
  when user.office_access_level? then {:conditions => {:id => user.manageable_offices.map(&:id)}}
  else {:conditions => "1 = 0"}
  end
}

# Then in controller:
@offices = Office.manageable_by(current_user)

This seems much more elegant to me.  I’m in general finding a lot of opportunities for inverting the way I designed something without named_scope to be more model-centric, so this approach helps further the design principle of “fat models, skinny controllers”. Sure you could do this before by defining your own methods and using with_scope, but named_scope just makes it all the more elegant.

Another advantage with using the named scope stuff is that you can chain scopes together. Let’s say that in another controller I want to do the same thing but instead restrict results to active offices only. I can create an “active” named scope that scopes :conditions => {:active => true}, then in the controller simply do this instead:

@offices = Office.manageable_by(current_user).active

6 Responses to “Inverting Permission-Based Filtering with named_scope”


  1. 1 Chris Jul 12th, 2008 at 7:13 pm

    cool beans :D

  2. 2 Ash McKenzie Dec 23rd, 2008 at 11:11 pm

    Legend, thanks for this hot tip.. saved me at least 10 lines of code!

  3. 3 Justin Palmer Mar 12th, 2009 at 3:11 am

    Hey nice template by the way :)

    I have the following code that is now working as expected. Do you have any ideas?

    named_scope :by_company, lambda{{
    case
    when COMPANY_ID.nil? then false
    else {:conditions => ['company_id = ?', COMPANY_ID]}
    end
    }}

    Any help would be greatly appreciated.

    Thanks,

    Justin

  4. 4 Justin Palmer Mar 12th, 2009 at 3:13 am

    Sorry, I forgot to give the error:

    cet_named_scope.rb:13: odd number list for Hash

    Line 13 is the ‘end’ of the case statement.

  5. 5 benhughes Mar 12th, 2009 at 3:25 am

    Try this:

    named_scope :by_company, lambda { {:conditions => COMPANY_ID.nil? ? {} : {:company_id => COMPANY_ID} } }

    However if COMPANY_ID really is a constant (not sure how or why you are using it this way…) you don’t really even need the lambda since lazy evaluation is unnecessary:

    named_scope :by_company, {:conditions => COMPANY_ID.nil? ? {} : {:company_id => COMPANY_ID} }

  6. 6 Justin Palmer Mar 12th, 2009 at 3:30 am

    Hi Ben,

    Thanks, that worked perfect. I used:

    named_scope :by_company, {:conditions => COMPANY_ID.nil? ? {} : {:company_id => COMPANY_ID} }

    I see now where my syntax was wrong. I tried something like above, but kept getting errors.

    I appreciate the help.

    Regards,

    Justin


  • Ben Hughes

    I'm a freelance developer working with Ruby and other modern tools to build web applications, based currently out of Rochester, NY. I love to learn about new technologies and am always trying to achieve elegance and beauty through code.

    When I'm not writing software, I like to play tennis, dabble in jazz piano, and ponder economics. I'm a big fan of: world travel and cultures, jazz music, Korean food, coffee, and having interesting conversations.

  • Recommend Me
July 2008
M T W T F S S
« Jun   Nov »
 123456
78910111213
14151617181920
21222324252627
28293031