chevinbrown
10/16/2013 - 6:06 PM

Some VERY basic LDAP interaction in Ruby using Net::LDAP.

Some VERY basic LDAP interaction in Ruby using Net::LDAP.

#######################################################################################################################
# This Gist is some crib notes/tests/practice/whatever for talking to Active Directory via LDAP.  The (surprisingly
# helpful) documentation for Net::LDAP can be found here: http://net-ldap.rubyforge.org/Net/LDAP.html
#######################################################################################################################

require 'rubygems'
require 'net/ldap'

#######################################################################################################################
# HELPER/UTILITY METHOD
#   This method interprets the response/return code from an LDAP bind operation (bind, search, add, modify, rename, 
#   delete).  This method isn't necessarily complete, but it's a good starting point for handling the response codes
#   from an LDAP bind operation.
#
#   Additional details for the get_operation_result method can be found here: 
#   http://net-ldap.rubyforge.org/Net/LDAP.html#method-i-get_operation_result
#######################################################################################################################
def get_ldap_response(ldap)
  msg = "Response Code: #{ ldap.get_operation_result.code }, Message: #{ ldap.get_operation_result.message }"

  raise msg unless ldap.get_operation_result.code == 0
end

#######################################################################################################################
# SET UP LDAP CONNECTION
# Setting up a connection to the LDAP server using .new() does not actually send any network traffic to the LDAP
# server.  When you call an operation on ldap (e.g. add or search), .bind is called implicitly.  *That's* when the
# connection is made to the LDAP server.  This means that each operation called on the ldap object will create its own
# network connection to the LDAP server.
#######################################################################################################################
ldap = Net::LDAP.new  :host => # your LDAP host name or IP goes here,
                      :port => # your LDAP host port goes here,
                      :encryption => :simple_tls,
                      :base => # the base of your AD tree goes here,
                      :auth => {
                        :method => :simple,
                        :username => # a user w/sufficient privileges to read from AD goes here,
                        :password => # the user's password goes here
                      }

#######################################################################################################################
# ALTERNATIVE FOR OPENING LDAP CONNECTION
# Instead of using .new, you can call .open.  Within .open's code block, you can perform whatever LDAP operations you
# need in the context of a single network connection.
#######################################################################################################################
host = # your LDAP host name or IP goes here
port = # your LDAP host port goes here
base = # the base of your AD tree goes here
credentials = {
  :method => :simple,
  :username => # a user w/sufficient privileges to read from AD goes here,
  :password => # the user's password goes here
}

Net::LDAP.open(:host => host, :port => port, :encryption => :simple_tls, :base => base, :auth => credentials) do |ldap|
	# Do all your LDAP stuff here...
end

#######################################################################################################################
# SOME SIMPLE LDAP SEARCHES
#######################################################################################################################

# GET THE DISPLAY NAME AND E-MAIL ADDRESS FOR A SINGLE USER
search_param = # the AD account goes here
result_attrs = ["sAMAccountName", "displayName", "mail"] # Whatever you want to bring back in your result set goes here

# Build filter
search_filter = Net::LDAP::Filter.eq("sAMAccountName", search_param)

# Execute search
ldap.search(:filter => search_filter, :attributes => result_attrs) { |item| 
  puts "#{item.sAMAccountName.first}: #{item.displayName.first} (#{item.mail.first})" 
}

get_ldap_response(ldap)

# ---------------------------------------------------------------------------------------------------------------------

# GET THE DISPLAY NAME AND E-MAIL ADDRESS FOR AN E-MAIL DISTRIBUTION LIST
search_param = # the name of the distribution list you're looking for goes here
result_attrs = ["sAMAccountName", "displayName", "mail"] # Whatever you want to bring back in your result set goes here

# Build filter
search_filter = Net::LDAP::Filter.eq("sAMAccountName", search_param)
group_filter = Net::LDAP::Filter.eq("objectClass", "group")
composite_filter = Net::LDAP::Filter.join(search_filter, group_filter)

# Execute search
ldap.search(:filter => composite_filter, :attributes => result_attrs) { |item| 
  puts "#{item.sAMAccountName.first}: #{item.displayName.first} (#{item.mail.first})" 
}

get_ldap_response(ldap)

# ---------------------------------------------------------------------------------------------------------------------

# GET THE MEMBERS OF AN E-MAIL DISTRIBUTION LIST
search_param = # the name of the distribution list you're looking for goes here
result_attrs = ["sAMAccountName", "displayName", "mail", "member"]

# Build filter
search_filter = Net::LDAP::Filter.eq("sAMAccountName", search_param)
group_filter = Net::LDAP::Filter.eq("objectClass", "group")
composite_filter = Net::LDAP::Filter.join(search_filter, group_filter)

# Execute search, extracting the AD account name from each member of the distribution list
ldap.search(:filter => composite_filter, :attributes => result_attrs) do |item| 
  puts "#{item.sAMAccountName.first}: #{item.displayName.first} (#{item.mail.first})"
  item.member.map { |m| puts "\taccount:  #{m.match(/(?<=\().+?(?=\))/)}" }
end

get_ldap_response(ldap)

# ---------------------------------------------------------------------------------------------------------------------

# GET THE DISPLAY NAME AND E-MAIL ADDRESS FOR ALL E-MAIL DISTRIBUTION LISTS
# Build filter
# This stackoverflow article was a HUGE help: http://stackoverflow.com/questions/6434752/better-way-to-query-an-ldap-users-via-ruby-net-ldap
group_filter = Net::LDAP::Filter.eq("objectClass", "group")
proxy_address_filter = Net::LDAP::Filter.eq("proxyAddresses", "*")
composite_filter = Net::LDAP::Filter.join(group_filter, proxy_address_filter)

# Execute search
ldap.search(:filter => composite_filter, :attributes => result_attrs) { |item| 
  puts "#{item.sAMAccountName.first}: #{item.mail.first}" 
}

get_ldap_response(ldap)

#######################################################################################################################
# LDAP FILTER EXAMPLES
#######################################################################################################################

# CONSTRUCT AN OR FILTER WITH SEVERAL EQUALS
# If you come across a situation where you need to search LDAP for this == x | this == y | this == z, there isn't an
# easy way to deal with it.  The Filter.intersect method takes two arguments, but that isn't enough (because we have
# 3 "OR" conditions).  Fortunately, Net::LDAP::Filter has a .construct method that will build a valid query string for
# us (with a little help):

names = ["lstarr", "barf", "dmatrix", "pvespa", "yogurt"]
filters = names.map { |name| Net::LDAP::Filter.eq("sAMAccountName", name) }
search_filter = Net::LDAP::Filter.construct("(|#{ filters.join("") })")

# search_filter => (|(|(|(|(sAMAccountName=lstarr)(sAMAccountName=barf))(sAMAccountName=dmatrix))(sAMAccountName=pvespa))(sAMAccountName=yogurt))

# Ugly, probabaly inefficient, but it'll work.  Now we can do this:
emails = []
ldap.search(:filter => search_filter, :attributes => ["mail"], :return_result => false) do |result|
    emails << (result.mail.is_a?(Array) ? result.mail.first : result.mail)
end