#!/usr/bin/ruby # = ActiveLdap # # Copyright (C) 2004,2005 Will Drewry mailto:will@alum.bu.edu # Copyright (C) 2006-2010 Kouhei Sutou # # == Introduction # # ActiveLdap is a novel way of interacting with LDAP. Most interaction with # LDAP is done using clunky LDIFs, web interfaces, or with painful APIs that # required a thick reference manual nearby. ActiveLdap aims to fix that. # Inspired by ActiveRecord[http://activerecord.rubyonrails.org], ActiveLdap provides an # object oriented interface to LDAP entries. # # The target audience is system administrators and LDAP users everywhere that # need quick, clean access to LDAP in Ruby. # # === What's LDAP? # # LDAP stands for "Lightweight Directory Access Protocol." Basically this means # that it is the protocol used for accessing LDAP servers. LDAP servers # lightweight directories. An LDAP server can contain anything from a simple # digital phonebook to user accounts for computer systems. More and more # frequently, it is being used for the latter. My examples in this text will # assume some familiarity with using LDAP as a centralized authentication and # authorization server for Unix systems. (Unfortunately, I've yet to try this # against Microsoft's ActiveDirectory, despite what the name implies.) # # Further reading: # * RFC1777[http://www.faqs.org/rfcs/rfc1777.html] - Lightweight Directory Access Protocol # * OpenLDAP[http://www.openldap.org] # # === So why use ActiveLdap? # # Well if you like to fumble around in the dark, dank innards of LDAP, you can # quit reading now. However, if you'd like a cleaner way to integrate LDAP in to # your existing code, hopefully that's why you'll want to use ActiveLdap. # # Using LDAP directly (even with the excellent Ruby/LDAP), leaves you bound to # the world of the predefined LDAP API. While this API is important for many # reasons, having to extract code out of LDAP search blocks and create huge # arrays of LDAP.mod entries make code harder to read, less intuitive, and just # less fun to write. Hopefully, ActiveLdap will remedy all of these # problems! # # == Getting Started # # ActiveLdap does have some overhead when you get started. You must not # only install the package and all of it's requirements, but you must also make # customizations that will let it work in your environment. # # === Requirements # # * A Ruby implementation: Ruby[http://www.ruby-lang.org] 1.8.x, 1.9.1 or JRuby[http://jruby.codehaus.org/] # * A LDAP library: Ruby/LDAP[http://code.google.com/p/ruby-activeldap/wiki/RubyLDAP] (for Ruby), Net::LDAP[http://rubyforge.org/projects/net-ldap/] (for Ruby or JRuby) or JNDI (for JRuby) # * A LDAP server: OpenLDAP[http://www.openldap.org], etc # - Your LDAP server must allow root_dse queries to allow for schema queries # # === Installation # # Assuming all the requirements are installed, you can install by gem. # # # gem install activeldap # # Now as a quick test, you can run: # # $ irb -rubygems # irb> require 'active_ldap' # => true # irb> exit # # If the require returns false or an exception is raised, there has been a # problem with the installation. You may need to customize what setup.rb does on # install. # # == Usage # # This section covers using ActiveLdap from writing extension classes to # writing applications that use them. # # Just to give a taste of what's to come, here is a quick example using irb: # # irb> require 'active_ldap' # # Call setup_connection method for connect to LDAP server. In this case, LDAP server # is localhost, and base of LDAP tree is "dc=dataspill,dc=org". # # irb> ActiveLdap::Base.setup_connection :host => 'localhost', :base => 'dc=dataspill,dc=org' # # Here's an extension class that maps to the LDAP Group objects: # # irb> class Group < ActiveLdap::Base # irb* ldap_mapping # irb* end # # Here is the Group class in use: # # irb> all_groups = Group.find(:all, '*').collect {|group| group.cn} # => ["root", "daemon", "bin", "sys", "adm", "tty", ..., "develop"] # # irb> group = Group.find("develop") # => # ...> # # irb> group.cn # => "develop" # # irb> group.gid_number # => "1003" # # That's it! No let's get back in to it. # # === Extension Classes # # Extension classes are classes that are subclassed from ActiveLdap::Base. They # are used to represent objects in your LDAP server abstractly. # # ==== Why do I need them? # # Extension classes are what make ActiveLdap "active"! They do all the # background work to make easy-to-use objects by mapping the LDAP object's # attributes on to a Ruby class. # # # ==== Special Methods # # I will briefly talk about each of the methods you can use when defining an # extension class. In the above example, I only made one special method call # inside the Group class. More than likely, you will want to more than that. # # ===== ldap_mapping # # ldap_mapping is the only required method to setup an extension class for use # with ActiveLdap. It must be called inside of a subclass as shown above. # # Below is a much more realistic Group class: # # class Group < ActiveLdap::Base # ldap_mapping :dn_attribute => 'cn', # :prefix => 'ou=Groups', :classes => ['top', 'posixGroup'], # :scope => :one # end # # As you can see, this method is used for defining how this class maps in to LDAP. Let's say that # my LDAP tree looks something like this: # # * dc=dataspill,dc=org # |- ou=People,dc=dataspill,dc=org # |+ ou=Groups,dc=dataspill,dc=org # \ # |- cn=develop,ou=Groups,dc=dataspill,dc=org # |- cn=root,ou=Groups,dc=dataspill,dc=org # |- ... # # Under ou=People I store user objects, and under ou=Groups, I store group # objects. What |ldap_mapping| has done is mapped the class in to the LDAP tree # abstractly. With the given :dn_attributes and :prefix, it will only work for # entries under ou=Groups,dc=dataspill,dc=org using the primary attribute 'cn' # as the beginning of the distinguished name. # # Just for clarity, here's how the arguments map out: # # cn=develop,ou=Groups,dc=dataspill,dc=org # ^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ # :dn_attribute | | # :prefix | # :base from setup_connection # # :scope tells ActiveLdap to only search under ou=Groups, and not to look deeper # for dn_attribute matches. # (e.g. cn=develop,ou=DevGroups,ou=Groups,dc=dataspill,dc=org) # You can choose value from between :sub, :one and :base. # # Something's missing: :classes. :classes is used to tell ActiveLdap what # the minimum requirement is when creating a new object. LDAP uses objectClasses # to define what attributes a LDAP object may have. ActiveLdap needs to know # what classes are required when creating a new object. Of course, you can leave # that field out to default to ['top'] only. Then you can let each application # choose what objectClasses their objects should have by calling the method e.g. # Group#add_class(*values). # # Note that is can be very important to define the default :classes value. Due to # implementation choices with most LDAP servers, once an object is created, its # structural objectclasses may not be removed (or replaced). Setting a sane default # may help avoid programmer error later. # # :classes isn't the only optional argument. If :dn_attribute is left off, # it defaults to super class's value or 'cn'. If :prefix is left off, # it will default to 'ou=PluralizedClassName'. In this # case, it would be 'ou=Groups'. # # :classes should be an Array. :dn_attribute should be a String and so should # :prefix. # # # ===== belongs_to # # This method allows an extension class to make use of other extension classes # tying objects together across the LDAP tree. Often, user objects will be # members of, or belong_to, Group objects. # # * dc=dataspill,dc=org # |+ ou=People,dc=dataspill,dc=org # \ # |- uid=drewry,ou=People,dc=dataspill,dc=org # |- ou=Groups,dc=dataspill,dc=org # # # In the above tree, one such example would be user 'drewry' who is a part of the # group 'develop'. You can see this by looking at the 'memberUid' field of 'develop'. # # irb> develop = Group.find('develop') # => ... # irb> develop.memberUid # => ['drewry', 'builder'] # # If we look at the LDAP entry for 'drewry', we do not see any references to # group 'develop'. In order to remedy that, we can use belongs_to # # irb> class User < ActiveLdap::Base # irb* ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People', :classes => ['top','account'] # irb* belongs_to :groups, :class_name => 'Group', :many => 'memberUid', :foreign_key => 'uid' # irb* end # # Now, class User will have a method called 'groups' which will retrieve all # Group objects that a user is in. # # irb> me = User.find('drewry') # irb> me.groups # => # # Enumerable object # irb> me.groups.each { |group| p group.cn };nil # "cdrom" # "audio" # "develop" # => nil # (Note: nil is just there to make the output cleaner...) # # TIP: If you weren't sure what the distinguished name attribute was for Group, # you could also do the following: # # irb> me.groups.each { |group| p group.id };nil # "cdrom" # "audio" # "develop" # => nil # # Now let's talk about the arguments. The first argument is the name of the # method you wish to create. In this case, we created a method called groups # using the symbol :groups. The next collection of arguments are actually a Hash # (as with ldap_mapping). :class_name should be a string that has the name of a # class you've already included. If you class is inside of a module, be sure to # put the whole name, e.g. :class_name => "MyLdapModule::Group". :foreign_key # tells belongs_to what attribute Group objects have that match the # :many. :many is the name of the local attribute whose value # should be looked up in Group under the foreign key. If :foreign_key is left # off of the argument list, it is assumed to be the dn_attribute. With this in # mind, the above definition could become: # # irb> class User < ActiveLdap::Base # irb* ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People', :classes => ['top','account'] # irb* belongs_to :groups, :class_name => 'Group', :many => 'memberUid' # irb* end # # In addition, you can do simple membership tests by doing the following: # # irb> me.groups.member? 'root' # => false # irb> me.groups.member? 'develop' # => true # # ===== has_many # # This method is the opposite of belongs_to. Instead of checking other objects in # other parts of the LDAP tree to see if you belong to them, you have multiple # objects from other trees listed in your object. To show this, we can just # invert the example from above: # # class Group < ActiveLdap::Base # ldap_mapping :dn_attribute => 'cn', :prefix => 'ou=Groups', :classes => ['top', 'posixGroup'] # has_many :members, :class_name => "User", :wrap => "memberUid", :primary_key => 'uid' # end # # Now we can see that group develop has user 'drewry' as a member, and it can # even return all responses in object form just like belongs_to methods. # # irb> develop = Group.find('develop') # => ... # irb> develop.members # => # # Enumerable object # irb> develop.members.map{|member| member.id} # => ["drewry", "builder"] # # The arguments for has_many follow the exact same idea that belongs_to's # arguments followed. :wrap's contents are used to search for matching # :primary_key content. If :primary_key is not specified, it defaults to the # dn_attribute of the specified :class_name. # # === Using these new classes # # These new classes have many method calls. Many of them are automatically # generated to provide access to the LDAP object's attributes. Other were defined # during class creation by special methods like belongs_to. There are a few other # methods that do not fall in to these categories. # # # ==== .find # # .find is a class method that is accessible from # any subclass of Base that has 'ldap_mapping' called. When # called .first(:first) returns the first match of the given class. # # irb> Group.find(:first, 'deve*").cn # => "develop" # # In this simple example, Group.find took the search string of 'deve*' and # searched for the first match in Group where the dn_attribute matched the # query. This is the simplest example of .find. # # irb> Group.find(:all).collect {|group| group.cn} # => ["root", "daemon", "bin", "sys", "adm", "tty", ..., "develop"] # # Here .find(:all) returns all matches to the same query. Both .find(:first) and # .find(:all) also can take more expressive arguments: # # irb> Group.find(:all, :attribute => 'gidNumber', :value => '1003').collect {|group| group.cn} # => ["develop"] # # So it is pretty clear what :attribute and :value do - they are used to query as # :attribute=:value. # # If :attribute is unspecified, it defaults to the dn_attribute. # # It is also possible to override :attribute and :value by specifying :filter. This # argument allows the direct specification of a LDAP filter to retrieve objects by. # # ==== .search # .search is a class method that is accessible from any subclass of Base, and Base. # It lets the user perform an arbitrary search against the current LDAP connection # irrespetive of LDAP mapping data. This is meant to be useful as a utility method # to cover 80% of the cases where a user would want to use Base.connection directly. # # irb> Base.search(:base => 'dc=example,dc=com', :filter => '(uid=roo*)', # :scope => :sub, :attributes => ['uid', 'cn']) # => [["uid=root,ou=People,dc=dataspill,dc=org",{"cn"=>["root"], "uidNumber"=>["0"]}] # You can specify the :filter, :base, :scope, and :attributes, but they all have defaults -- # * :filter defaults to objectClass=* - usually this isn't what you want # * :base defaults to the base of the class this is executed from (as set in ldap_mapping) # * :scope defaults to :sub. Usually you won't need to change it (You can choose value also from between :one and :base) # * :attributes defaults to [] and is the list of attributes you want back. Empty means all of them. # # ==== #valid? # # valid? is a method that verifies that all attributes that are required by the # objects current objectClasses are populated. # # ==== #save # # save is a method that writes any changes to an object back to the LDAP server. # It automatically handles the addition of new objects, and the modification of # existing ones. # # ==== .exists? # # exists? is a simple method which returns true is the current object exists in # LDAP, or false if it does not. # # irb> User.exists?("dshadsadsa") # => false # # # === ActiveLdap::Base # # ActiveLdap::Base has come up a number of times in the examples above. Every # time, it was being used as the super class for the wrapper objects. While this # is it's main purpose, it also handles quite a bit more in the background. # # ==== What is it? # # ActiveLdap::Base is the heart of ActiveLdap. It does all the schema # parsing for validation and attribute-to-method mangling as well as manage the # connection to LDAP. # # ===== setup_connection # # Base.setup_connection takes many (optional) arguments and is used to # connect to the LDAP server. Sometimes you will want to connect anonymously # and other times over TLS with user credentials. Base.setup_connection is # here to do all of that for you. # # # By default, if you call any subclass of Base, such as Group, it will call # Base.setup_connection() if these is no active LDAP connection. If your # server allows anonymous binding, and you only want to access data in a # read-only fashion, you won't need to call Base.setup_connection. Here # is a fully parameterized call: # # Base.setup_connection( # :host => 'ldap.dataspill.org', # :port => 389, # :base => 'dc=dataspill,dc=org', # :logger => logger_object, # :bind_dn => "uid=drewry,ou=People,dc=dataspill,dc=org", # :password_block => Proc.new { 'password12345' }, # :allow_anonymous => false, # :try_sasl => false # ) # # There are quite a few arguments, but luckily many of them have safe defaults: # * :host defaults to "127.0.0.1". # * :port defaults to nil. 389 is applied if not specified. # * :bind_dn defaults to nil. anonymous binding is applied if not specified. # * :logger defaults to a Logger object that prints fatal messages to stderr # * :password_block defaults to nil # * :allow_anonymous defaults to true # * :try_sasl defaults to false - see Advanced Topics for more on this one. # # # Most of these are obvious, but I'll step through them for completeness: # * :host defines the LDAP server hostname to connect to. # * :port defines the LDAP server port to connect to. # * :method defines the type of connection - :tls, :ssl, :plain # * :base specifies the LDAP search base to use with the prefixes defined in all # subclasses. # * :bind_dn specifies what your server expects when attempting to bind with # credentials. # * :logger accepts a custom logger object to integrate with any other logging # your application uses. # * :password_block, if defined, give the Proc block for acquiring the password # * :password, if defined, give the user's password as a String # * :store_password indicates whether the password should be stored, or if used # whether the :password_block should be called on each reconnect. # * :allow_anonymous determines whether anonymous binding is allowed if other # bind methods fail # * :try_sasl, when true, tells ActiveLdap to attempt a SASL-GSSAPI bind # * :sasl_quiet, when true, tells the SASL libraries to not spew messages to STDOUT # * :sasl_options, if defined, should be a hash of options to pass through. This currently only works with the ruby-ldap adapter, which currently only supports :realm, :authcid, and :authzid. # * :retry_limit - indicates the number of attempts to reconnect that will be undertaken when a stale connection occurs. -1 means infinite. # * :retry_wait - seconds to wait before retrying a connection # * :scope - dictates how to find objects. (Default: :one) # * :timeout - time in seconds - defaults to disabled. This CAN interrupt search() requests. Be warned. # * :retry_on_timeout - whether to reconnect when timeouts occur. Defaults to true # See lib/configuration.rb(ActiveLdap::Configuration::DEFAULT_CONFIG) for defaults for each option # # Base.setup_connection just setups connection # configuration. A connection is connected and bound when it # is needed. It follows roughly the following approach: # # * Connect to host:port using :method # # * If bind_dn and password_block/password, attempt to bind with credentials. # * If that fails or no password_block and anonymous allowed, attempt to bind # anonymously. # * If that fails, error out. # # On connect, the configuration options passed in are stored # in an internal class variable which is used to cache the # information without ditching the defaults passed in from # configuration.rb # # ===== connection # # Base.connection returns the ActiveLdap::Connection object. # # === Exceptions # # There are a few custom exceptions used in ActiveLdap. They are detailed below. # # ==== DeleteError # # This exception is raised when #delete fails. It will include LDAP error # information that was passed up during the error. # # ==== SaveError # # This exception is raised when there is a problem in #save updating or creating # an LDAP entry. Often the error messages are cryptic. Looking at the server # logs or doing an Ethereal[http://www.ethereal.com] dump of the connection will # often provide better insight. # # ==== AuthenticationError # # This exception is raised during Base.setup_connection if no valid authentication methods # succeeded. # # ==== ConnectionError # # This exception is raised during Base.setup_connection if no valid # connection to the LDAP server could be created. Check you # Base.setup_connection arguments, and network connectivity! Also check # your LDAP server logs to see if it ever saw the request. # # ==== ObjectClassError # # This exception is raised when an object class is used that is not defined # in the schema. # # === Others # # Other exceptions may be raised by the Ruby/LDAP module, or by other subsystems. # If you get one of these exceptions and think it should be wrapped, write me an # email and let me know where it is and what you expected. For faster results, # email a patch! # # === Putting it all together # # Now that all of the components of ActiveLdap have been covered, it's time # to put it all together! The rest of this section will show the steps to setup # example user and group management scripts for use with the LDAP tree described # above. # # All of the scripts here are in the package's examples/ directory. # # ==== Setting up # # Create directory for scripts. # # mkdir -p ldapadmin/objects # # In ldapadmin/objects/ create the file user.rb: # # require 'objects/group' # # class User < ActiveLdap::Base # ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People', :classes => ['person', 'posixAccount'] # belongs_to :groups, :class_name => 'Group', :many => 'memberUid' # end # # In ldapadmin/objects/ create the file group.rb: # # class Group < ActiveLdap::Base # ldap_mapping :classes => ['top', 'posixGroup'], :prefix => 'ou=Groups' # has_many :members, :class_name => "User", :wrap => "memberUid" # has_many :primary_members, :class_name => 'User', :foreign_key => 'gidNumber', :primary_key => 'gidNumber' # end # # Now, we can write some small scripts to do simple management tasks. # # ==== Creating LDAP entries # # Now let's create a really dumb script for adding users - ldapadmin/useradd: # # #!/usr/bin/ruby -W0 # # base = File.expand_path(File.join(File.dirname(__FILE__), "..")) # $LOAD_PATH << File.join(base, "lib") # $LOAD_PATH << File.join(base, "examples") # # require 'rubygems' # require 'active_ldap' # require 'objects/user' # require 'objects/group' # # argv, opts, options = ActiveLdap::Command.parse_options do |opts, options| # opts.banner += " USER_NAME CN UID" # end # # if argv.size == 3 # name, cn, uid = argv # else # $stderr.puts opts # exit 1 # end # # pwb = Proc.new do |user| # ActiveLdap::Command.read_password("[#{user}] Password: ") # end # # ActiveLdap::Base.setup_connection(:password_block => pwb, # :allow_anonymous => false) # # if User.exists?(name) # $stderr.puts("User #{name} already exists.") # exit 1 # end # # user = User.new(name) # user.add_class('shadowAccount') # user.cn = cn # user.uid_number = uid # user.gid_number = uid # user.home_directory = "/home/#{name}" # user.sn = "somesn" # unless user.save # puts "failed" # puts user.errors.full_messages # exit 1 # end # # ==== Managing LDAP entries # # Now let's create another dumb script for modifying users - ldapadmin/usermod: # # #!/usr/bin/ruby -W0 # # base = File.expand_path(File.join(File.dirname(__FILE__), "..")) # $LOAD_PATH << File.join(base, "lib") # $LOAD_PATH << File.join(base, "examples") # # require 'rubygems' # require 'active_ldap' # require 'objects/user' # require 'objects/group' # # argv, opts, options = ActiveLdap::Command.parse_options do |opts, options| # opts.banner += " USER_NAME CN UID" # end # # if argv.size == 3 # name, cn, uid = argv # else # $stderr.puts opts # exit 1 # end # # pwb = Proc.new do |user| # ActiveLdap::Command.read_password("[#{user}] Password: ") # end # # ActiveLdap::Base.setup_connection(:password_block => pwb, # :allow_anonymous => false) # # unless User.exists?(name) # $stderr.puts("User #{name} doesn't exist.") # exit 1 # end # # user = User.find(name) # user.cn = cn # user.uid_number = uid # user.gid_number = uid # unless user.save # puts "failed" # puts user.errors.full_messages # exit 1 # end # # ==== Removing LDAP entries # Now let's create more one for deleting users - ldapadmin/userdel: # # #!/usr/bin/ruby -W0 # # base = File.expand_path(File.join(File.dirname(__FILE__), "..")) # $LOAD_PATH << File.join(base, "lib") # $LOAD_PATH << File.join(base, "examples") # # require 'rubygems' # require 'active_ldap' # require 'objects/user' # require 'objects/group' # # argv, opts, options = ActiveLdap::Command.parse_options do |opts, options| # opts.banner += " USER_NAME" # end # # if argv.size == 1 # name = argv.shift # else # $stderr.puts opts # exit 1 # end # # pwb = Proc.new do |user| # ActiveLdap::Command.read_password("[#{user}] Password: ") # end # # ActiveLdap::Base.setup_connection(:password_block => pwb, # :allow_anonymous => false) # # unless User.exists?(name) # $stderr.puts("User #{name} doesn't exist.") # exit 1 # end # # User.destroy(name) # # === Advanced Topics # # Below are some situation tips and tricks to get the most out of ActiveLdap. # # # ==== Binary data and other subtypes # # Sometimes, you may want to store attributes with language specifiers, or # perhaps in binary form. This is (finally!) fully supported. To do so, # follow the examples below: # # irb> user = User.new('drewry') # => ... # # This adds a cn entry in lang-en and whatever the server default is. # irb> user.cn = [ 'wad', {'lang-en' => ['wad', 'Will Drewry']} ] # => ... # irb> user.cn # => ["wad", {"lang-en-us" => ["wad", "Will Drewry"]}] # # Now let's add a binary X.509 certificate (assume objectClass is correct) # irb> user.user_certificate = File.read('example.der') # => ... # irb> user.save # # So that's a lot to take in. Here's what is going on. I just set the LDAP # object's cn to "wad" and cn:lang-en-us to ["wad", "Will Drewry"]. # Anytime a LDAP subtype is required, you must encapsulate the data in a Hash. # # But wait a minute, I just read in a binary certificate without wrapping it up. # So any binary attribute _that requires ;binary subtyping_ will automagically # get wrapped in {'binary' => value} if you don't do it. This keeps your #writes # from breaking, and my code from crying. For correctness, I could have easily # done the following: # # irb> user.user_certificate = {'binary' => File.read('example.der')} # # You should note that some binary data does not use the binary subtype all the time. # One example is jpegPhoto. You can use it as jpegPhoto;binary or just as jpegPhoto. # Since the schema dictates that it is a binary value, ActiveLdap will write # it as binary, but the subtype will not be automatically appended as above. The # use of the subtype on attributes like jpegPhoto is ultimately decided by the # LDAP site policy and not by any programmatic means. # # The only subtypes defined in LDAPv3 are lang-* and binary. These can be nested # though: # # irb> user.cn = [{'lang-ja' => {'binary' => 'some Japanese'}}] # # As I understand it, OpenLDAP does not support nested subtypes, but some # documentation I've read suggests that Netscape's LDAP server does. I only # have access to OpenLDAP. If anyone tests this out, please let me know how it # goes! # # # And that pretty much wraps up this section. # # ==== Further integration with your environment aka namespacing # # If you want this to cleanly integrate into your system-wide Ruby include path, # you should put your extension classes inside a custom module. # # # Example: # # ./myldap.rb: # require 'active_ldap' # require 'myldap/user' # require 'myldap/group' # module MyLDAP # end # # ./myldap/user.rb: # module MyLDAP # class User < ActiveLdap::Base # ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People', :classes => ['top', 'account', 'posixAccount'] # belongs_to :groups, :class_name => 'MyLDAP::Group', :many => 'memberUid' # end # end # # ./myldap/group.rb: # module MyLDAP # class Group < ActiveLdap::Base # ldap_mapping :classes => ['top', 'posixGroup'], :prefix => 'ou=Groups' # has_many :members, :class_name => 'MyLDAP::User', :wrap => 'memberUid' # has_many :primary_members, :class_name => 'MyLDAP::User', :foreign_key => 'gidNumber', :primary_key => 'gidNumber' # end # end # # Now in your local applications, you can call # # require 'myldap' # # MyLDAP::Group.new('foo') # ... # # and everything should work well. # # # ==== force array results for single values # # Even though ActiveLdap attempts to maintain programmatic ease by # returning Array values only. By specifying 'true' as an argument to # any attribute method you will get back a Array if it is single value. # Here's an example: # # irb> user = User.new('drewry') # => ... # irb> user.cn(true) # => ["Will Drewry"] # # ==== Dynamic attribute crawling # # If you use tab completion in irb, you'll notice that you /can/ tab complete the dynamic # attribute methods. You can still see which methods are for attributes using # Base#attribute_names: # # irb> d = Group.new('develop') # => ... # irb> d.attribute_names # => ["gidNumber", "cn", "memberUid", "commonName", "description", "userPassword", "objectClass"] # # # ==== Juggling multiple LDAP connections # # In the same vein as the last tip, you can use multiple LDAP connections by # per class as follows: # # irb> anon_class = Class.new(Base) # => ... # irb> anon_class.setup_connection # => ... # irb> auth_class = Class.new(Base) # => ... # irb> auth_class.setup_connection(:password_block => lambda{'mypass'}) # => ... # # This can be useful for doing authentication tests and other such tricks. # # ==== :try_sasl # # If you have the Ruby/LDAP package with the SASL/GSSAPI patch from Ian # MacDonald's web site, you can use Kerberos to bind to your LDAP server. By # default, :try_sasl is false. # # Also note that you must be using OpenLDAP 2.1.29 or higher to use SASL/GSSAPI # due to some bugs in older versions of OpenLDAP. # # ==== Don't be afraid! [Internals] # # Don't be afraid to add more methods to the extensions classes and to # experiment. That's exactly how I ended up with this package. If you come up # with something cool, please share it! # # The internal structure of ActiveLdap::Base, and thus all its subclasses, is # still in flux. I've tried to minimize the changes to the overall API, but # the internals are still rough around the edges. # # ===== Where's ldap_mapping data stored? How can I get to it? # # When you call ldap_mapping, it overwrites several class methods inherited # from Base: # * Base.base() # * Base.required_classes() # * Base.dn_attribute() # You can access these from custom class methods by calling MyClass.base(), # or whatever. There are predefined instance methods for getting to these # from any new instance methods you define: # * Base#base() # * Base#required_classes() # * Base#dn_attribute() # # ===== What else? # # Well if you want to use the LDAP connection for anything, I'd suggest still # calling Base.connection to get it. There really aren't many other internals # that need to be worried about. You could get the LDAP schema with # Base.schema. # # The only other useful tricks are dereferencing and accessing the stored # data. Since LDAP attributes can have multiple names, e.g. cn or commonName, # any methods you write might need to figure it out. I'd suggest just # calling self[attribname] to get the value, but if that's not good enough, # you can call look up the stored name by #to_real_attribute_name as follows: # # irb> User.find(:first).instance_eval do # irb> to_real_attribute_name('commonName') # irb> end # => 'cn' # # This tells you the name the attribute is stored in behind the scenes (@data). # Again, self[attribname] should be enough for most extensions, but if not, # it's probably safe to dabble here. # # Also, if you like to look up all aliases for an attribute, you can call the # following: # # irb> User.schema.attribute_type 'cn', 'NAME' # => ["cn", "commonName"] # # This is discovered automagically from the LDAP server's schema. # # == Limitations # # === Speed # # Currently, ActiveLdap could be faster. I have some recursive type # checking going on which slows object creation down, and I'm sure there # are many, many other places optimizations can be done. Feel free # to send patches, or just hang in there until I can optimize away the # slowness. # # == Feedback # # Any and all feedback and patches are welcome. I am very excited about this # package, and I'd like to see it prove helpful to more people than just myself. # $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "..", "net-ldap", "lib")) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "..", "ldap", "lib")) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "..", "ldap")) require_gem_if_need = Proc.new do |library_name, gem_name, *gem_args| gem_name ||= library_name begin if !gem_args.empty? and Object.const_defined?(:Gem) gem gem_name, *gem_args end require library_name rescue LoadError require 'rubygems' gem gem_name, *gem_args require library_name end end require_gem_if_need.call("active_support", "activesupport", "= 2.3.8") if ActiveSupport.const_defined?(:Dependencies) dependencies = ActiveSupport::Dependencies else dependencies = Dependencies end if dependencies.respond_to?(:load_paths) dependencies.load_paths << File.expand_path(File.dirname(__FILE__)) end module ActiveLdap VERSION = "1.2.2" end if RUBY_PLATFORM.match('linux') require 'active_ldap/timeout' else require 'active_ldap/timeout_stub' end require_gem_if_need.call("active_record", "activerecord", "= 2.3.8") begin require_gem_if_need.call("locale", nil, "= 2.0.5") require_gem_if_need.call("gettext", nil, "= 2.1.0") require_gem_if_need.call("gettext_activerecord", nil, "= 2.1.0") rescue LoadError end require 'active_ldap/get_text' require 'active_ldap/compatible' require 'active_ldap/base' require 'active_ldap/distinguished_name' require 'active_ldap/ldif' require 'active_ldap/xml' require 'active_ldap/associations' require 'active_ldap/attributes' require 'active_ldap/configuration' require 'active_ldap/connection' require 'active_ldap/operations' require 'active_ldap/object_class' require 'active_ldap/human_readable' require 'active_ldap/acts/tree' require 'active_ldap/populate' require 'active_ldap/escape' require 'active_ldap/user_password' require 'active_ldap/helper' require 'active_ldap/validations' require 'active_ldap/callbacks' ActiveLdap::Base.class_eval do include ActiveLdap::Associations include ActiveLdap::Attributes include ActiveLdap::Configuration include ActiveLdap::Connection include ActiveLdap::Operations include ActiveLdap::ObjectClass include ActiveLdap::HumanReadable include ActiveLdap::Acts::Tree include ActiveLdap::Validations include ActiveLdap::Callbacks end unless defined?(ACTIVE_LDAP_CONNECTION_ADAPTERS) ACTIVE_LDAP_CONNECTION_ADAPTERS = %w(ldap net_ldap jndi) end ACTIVE_LDAP_CONNECTION_ADAPTERS.each do |adapter| require "active_ldap/adapter/#{adapter}" end