Question

I'm looking for the best way to build clean way to build role/authorisaton-based finders?

In my model schema, a user can have one of several (admin-defined) roles, such as Administrator, Regional Manager, Sales Assistant:

Example Given a User with a Regional Manager role and joined to a Region A, I would like to be able to query what other users she could see, e.g:

regional_manager_for_region_a.users 
  => [...] # Array of users joined to region a

regional_manager_for_region_b.users(:all, conditions => { :active => true })
  => [...] # Array of active users joined to region b

administrator.users
  => [...] # Array of all users in system

Thanks, greatly appreciate any help!

Was it helpful?

Solution

I think you need to put in place some authorization mechanism.

The best gem I know for this is declarative_authorization. I've personally used it on a production environment, and I'm satisfied with it. There's a railscast about it, too.

The idea is that you declare in one specific file (config/authorization_rules.rb) the "roles and permissions". You say things like "a manager can read only the clients associated with it" or "an administrator can read and write all the users". In your case, it would look like this:

authorization do

  role :guest do
    # actions here can be done by everyone, even not logged in people
  end

  role :user do
    includes :guest
    # actions here can be done by logged people
  end

  role :manager do
    includes :user #managers do everything users do, plus:

    has_permission_on :sales_region, :to => :read do
      if_attribute :id => is_in {user.sales_region_ids}
    end

    has_permission_on :users, :to => [:update, :read] do
      if_attribute :id => is {user.user_ids_by_sales_region} #defined on the model
    end
  end

  role :admin do
    includes :user
    has_permission_on [:sales_regions, :users], :to :manage
  end

end

privileges do
  privilege :manage do
    includes :create, :read, :update, :delete
  end
end

Once this is specified, you have modify your models so they use declarative_authorization. Also, let's define the user_ids_by_sales_region method

class User < ActiveRecord::Base

  using_access_control # this enables DA

  def users_by_sales_region
    sales_regions.collect{ |sr| sr.users }.flatten.uniq
  end

  def user_ids_by_sales_region
    users_by_sales_region.collect{ |u| u.id }
  end
end

You must also have a current_user method, and a way of getting the current user's role(s). See the "Providing the Plugin’s Requirements" section on the readme.

Then you can do what you want with with_permissions_to :

manager = User.find(...)
manager.users.with_permissions_to(:read) # the users from his region
manager.users.with_permissions_to(:read).find(:all, conditions => { :active => true })
manager.users.with_permissions_to(:write) #returns no users, managers can't edit them

admin = User.find(...)
admin.users.with_permissions_to(:write) #will return all users

This means a little bit of effort at the beginning, but simplifies the application greatly later on. Also, you have additional functionalities, such as hiding/showing parts of views depending on the permissions the current user has, as well as forbidding access to specific controller actions.

Also, it should work just fine with paginations, etc.

There's another declarative authorization gem called cancan. I don't have experience with this one, but if it is done by Ryan Bates, it must be good (he's got a railscast for it, too). However, I don't think it allows model extensions, which is what you seem to need now.

OTHER TIPS

My answer below is fine for simple finders; however, it isn't very flexible and isn't compatible with the will_paginate plugin. Does anyone know of a better way to cleanly scope the users @current_user is able to manage?

Thanks


Just answered my own question, by overriding the default association extension as below. It would still be great to know comments or alternatives though!

class User < ActiveRecord::Base
  has_many :users do
    def find(*args)
      scope = args.first || :all
      options = args.extract_options!

      return User.find(args.first, options) if proxy_owner.admin?

      users = []
      proxy_owner.sales_regions.collect do |sales_region|
        users += sales_region.users
      end

      users.uniq
    end
  end
end

Just to follow up my comment on egarcia's answer, I eventually settled on declaring named_scopes on the restricted models. For example:

# app/models/account.rb
class Account < ActiveRecord::Base
  named_scope :visible_to, lambda { |user| 
    return {} if user.can_see_all_accounts?
    { :conditions => ['sales_area_id IN (?)', user.sales_area_ids] } 
  }
end

# app/controllers/accounts_controller.rb
class AccountsController < ApplicationController
  def index
    @accounts = Account.visible_to(@current_user)
    ...
  end
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top