If you followed my previous tutorial on implementing pass-through authentication to LDAP with Authlogic, you might be wondering how it can be extended to give different permissions to members of different LDAP groups. ActiveLdap and declarative_authorization make this incredibly simple.
As with the first tutorial, I’ve pushed a branch based on authlogic_example
that you can use as a concrete example.
First, let’s bring in the declarative_authorization gem:
# My example code was written before GitHub decided that they don't want to # build gems anymore but this line should do the trick. config.gem "declarative_authorization", :source => 'http://gemcutter.org' |
Now, we need to make an ActiveLdap model class like LdapUser
but for groups. We’ll call this LdapGroup
:
class LdapGroup < ActiveLdap::Base ldap_mapping :dn_attribute => "cn", :scope => :sub, :prefix => "ou=groups,o=users" has_many :members, :class_name => "LdapUser", :wrap => "member", :primary_key => "dn", :foreign_key => "dn" end |
The ldap_mapping
part is very similar to the previous one for LdapUser
, but the has_many
is where the real ActiveLdap magic starts!
We are letting ActiveLdap know that an LdapGroup
has LdapUser
members, which are defined in the multi-valued attribute member
, where each of the values is the DN of an LdapUser
. If you’re looking for a bit more detail, refer to the ActiveLdap documentation. Keep in mind that the code above is for the specific schema used by my example application and you may have to change it to match yours.
With that, we should be able to list all of our LDAP groups and find the members of any particular one in the console:
>> LdapGroup.all.collect { |g| g.cn } => ["super_admin"] >> LdapGroup.find("super_admin").members.collect { |m| m.dn } => ["uid=ebianco,o=users,dc=example,dc=com"] |
Now we’ll create an association between LdapUser
and LdapGroup
so that given a user, we can figure out which groups he/she is a member of. We’re going to tell ActiveLdap that a user’s groups are those groups in which the user’s DN appears in the group’s member
attribute:
belongs_to :groups, :class_name => "LdapGroup", :many => "member", :foreign_key => "dn" |
Let’s check our handiwork in the console:
>> LdapUser.find("ebianco").groups.collect { |g| g.cn } => ["super_admin"] |
Now why did we go through all of this effort? declarative_authorization allows us to provide an instance method for User
, #role_symbols
, that returns an array of symbols representing the list of roles that the user has. With our ActiveLdap setup, mapping a user’s roles to LDAP groups becomes a one-liner:
def role_symbols ldap_entry.groups.collect { |g| g.cn.to_sym } end |
From here, we can define the authorization rules for our LDAP-based roles. declarative_authorization reads these from config/authorization_rules.rb
:
authorization do role :super_admin do has_permission_on :users, :to => :manage end role :guest do has_permission_on :users, :to => :read has_permission_on :users, :to => [:create, :update, :destroy] do if_attribute :id => is {user.id} end end end privileges do privilege :manage do includes :create, :read, :update, :destroy end end |
Now we can use declarative_authorization as usual. I won’t go through all the details but if you want a primer on using declarative_authorization, you can start here and browse the commits on my authlogic_example
branch.
A nice expansion to this would involve caching the user data (specifically group memberships) in the database, updated every login. Currently it queries Active Directory every time a page is loaded, and I found that it would cause a broken pipe error if I left it idle for a bit while logged in.
@Joe: Perhaps you could take a similar approach to the one I took for caching the LDAP entry in my previous post. That is, the first time you determine role symbols for a User, store the array in an instance variable and that way you don’t have to look it up again until you build a fresh User instance for that person (which I think would happen at next login).
But there’s a disadvantage to caching that information: if you accidentally gave a person a role they shouldn’t have and fix that by removing them from the group, you still have to wait for that person to log out (or the session to expire) before the change will take effect. But if you have a large number of users that might be your best option.
@enrico: If you are referring to “@ldap_entry ||= LdapUser.find(self.login)”, I found that only reduces the LDAP queries per page from four to two. I made a small paste running your example code to demonstrate:
http://rails.pastebin.com/m74ed7f8e
As far as I can tell, ‘session’ storage is only accessible through controllers, and models can’t access controller-level variables (such as current_user). Considering “declarative authorization” requires role_symbols in the model, I have been unable to figure out how to cache group membership properly.
I made a bit of a lame solution, but given:
1. My system doesn’t need to have multiple roles per user,
2. I don’t plan to ever change the role names, and
3. I’m not a database normal form zealot,
it addresses my issue.
The user session ‘create’ code grabs user data, runs a check on the relevant domain security groups, and saves the highest level I care about as the user’s role. Whenever DA asks for ‘role_symbols’, it receives the user’s role put in array form.
## xxxxxxxxxxxxxx_add_role_to_users.rb
add_column :users, :role, :string, :null => false, :default => “Guest”
## user_sessions_controller.rb
def create
@user_session = UserSession.new(params[:user_session])
@groups = LdapUser.find(@user_session.login).groups.collect(&:cn.to_sym)
@curuser = User.find_by_login(@user_session.login)
if @groups.include?(“Inventory Admins”) then
@curuser.update_attribute(:role, “admin”)
elsif @groups.include?(“Inventory Users”) then
@curuser.update_attribute(:role, “user”)
else
@curuser.update_attribute(:role, “guest”)
end
if @user_session.save
flash[:notice] = “Login successful!”
redirect_back_or_default account_url
else
render :action => :new
end
end
## user.rb
def role_symbols
[ self.role.to_sym() ]
end
Yeah, looking into it, caching things the way I was only kept the information stored for the request and not for the entire session as I previously thought.
So you’re storing the role information in the database, then?
It seems that the biggest obstacle to caching the role information in the session properly is that declarative_authorization expects the roles to be defined in the User model but Authlogic doesn’t store the User instance in the session directly. Depending on your session storage mechanism, storing the User instance might be impractical.
Out of curiosity, which lower-level Ruby LDAP library are you using? My posts set up ActiveLdap with net-ldap but I’ve actually switched to ruby-ldap (ActiveLdap supports both!) because I was having an issue similar to yours with Active Directory where looking up an entry would fail after the connection had been idle for a while. If you’re using net-ldap, try switching to ruby-ldap and see if it works better for you.