Rails dynamic finders based on role
-
07-07-2019 - |
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!
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