Skip to content

Instantly share code, notes, and snippets.

@jeffjohnson9046
Last active January 5, 2024 07:11
Show Gist options
  • Save jeffjohnson9046/7012167 to your computer and use it in GitHub Desktop.
Save jeffjohnson9046/7012167 to your computer and use it in GitHub Desktop.
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
@thesp0nge
Copy link

You must add :base=>"" to your ldap.search calls in order to succeed

@nanunh
Copy link

nanunh commented Jan 15, 2016

Struggled with this for a while but finally got it work. In my case I had to set :base="com.xyz.intranet" where com.xyz.intranet is the root of the ldap directory tree that I had read access. If you do not know the root (base) level in the ldap (or active) directory tree that you have read access to, I would suggest you install a ldap browser like "JXplorer - an open source LDAP browser" and find out what your "base" value should be. Play with JXplorer for 15 minutes and your should be able to find the appropriate "base" value for your ldap environment. Then use that value in your code and all of the above things should work fine...

@jedrekdomanski
Copy link

jedrekdomanski commented Jun 6, 2017

uids = ["cu2300", "aa9939"]
filters = uids.map { |uid| Net::LDAP::Filter.eq("uid", uid) }
search_filter = Net::LDAP::Filter.construct("(|#{ filters.join("") })")

emails = []
ldap.search(:filter => search_filter, :attributes => ["mail"], :return_result => true) do |result|
emails << (result.mail.is_a?(Array) ? result.mail.first : result.mail)
end

p ldap.get_operation_result
p emails

image

This does not work. It should return some records, I am sure those users exist.

@xtrasimplicity
Copy link

With filtering, you can also use .ne (as opposed to .eq) to exclude entries.
i.e.
Net::LDAP::Filter.ne('MemberOf', DN_OF_GROUP_TO_EXCLUDE) excludes members who are present in the supplied group.

http://www.rubydoc.info/github/ruby-ldap/ruby-net-ldap/Net%2FLDAP%2FFilter.ne

@rmuktader
Copy link

Is there a way to print out the actual commands being sent to LDAP?

@dem972
Copy link

dem972 commented Aug 10, 2019

Hello ,

Thank you or that the code seems good to perform some request over ldap.
My question is how can I add this to my code should I add the ldap sample as controller after install the net ldap gem ?

Thank you.

@jeffjohnson9046
Copy link
Author

jeffjohnson9046 commented Aug 14, 2019

@dem972: I wouldn't put this type of code directly in a controller. In my opinion, controllers are supposed to be pretty "dumb", with just enough logic to unpack the request and route it to where it's supposed to go. I'd probably do something like this:

  • Create a user repository that handles querying your LDAP server. This is where the code that queries the LDAP server (i.e. some of the code from this gist) goes
  • Create a service class that calls the user repository and does something with the response (or handles exceptions)
  • Have my controller call a method in the service class; this way the controller doesn't have to know anything about how the LDAP user data is retrieved; all it knows is that it needs some user data
  • If you need to, you might want to wrap

The basic idea here is that you're abstracting away the interaction with the LDAP server, so that you can easily swap it out for something else later down the line (e.g. maybe you get user info from a MySQL database instead of an LDAP server [for whatever reason]).

I haven't done any Ruby in a long time, but in pseudocode it might look something like this:

# LdapUserRepository.rb
require 'net/ldap'

class LdapUserRepository
    def find_user_by_email(email_address)
        # open a connection to the LDAP server and query for the user's email address
    end

    def find_group(group_name)
        # open a connection to the LDAP server and query for a group of users
    end

    def find_by_some_attribute(attribute_value)
         # open a connection to the LDAP server and find a collection of users based on some attribute
    end
end

# LdapUserService.rb
class LdapUserService
    def find_user(email_address)
        user = nil

        begin
           # NOTE:  This is probably not the right place to create a new instance of your LdapUserRepository.  I just did
           # it here for the sake of this example
            ldapRepository = new LdapUserRepository
            user = ldapRepository.find_user_by_email(email_address)
            # do some other work with the user here
        rescue
            # do something meaningful with the exception
        ensure
            # clean up any resources if there's anything you need to do
        end

        return user
    end

     # other methods
end

# LdapUserController.rb
class UserController
    # controller methods here
end

All of this is merely a suggestion that might not fit your project. But this is the general approach I'd try to start with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment