The Fairway Technologies Blog

blog

A Beginner's Guide to Querying LDAP with Ruby. Also Spaceballs.

In Blog, Ruby, software development No Comments

Overview

I'm at a point in my project where I need to look up stuff.  No doubt you're saying something like "Duh - programs need to look up stuff all the time, dummy!  THAT'S NOT SPECIAL!  TELL ME SOMETHING INTERESTING!"

Sheesh - you didn't even let me finish!  I'm at the point in my project where I need to look up user and e-mail and distribution list stuff.  That means I'm gonna have to query the LDAP server, which is something I haven't done in a while.  Oh sure, I've done it before.  But that was a long time ago in a technology stack far, far away.  How am I gonna get fancy LDAP goodies from the server using Ruby?

Net::LDAP, That's How!

Editorial Sidebar
LOOK! A Ruby library that doesn't have some clever/cutesy name! Holy crap on a crap cracker - it's a library with a name that says what it does! Glee!

Fortunately, Ruby has a pretty handy library for dealing with LDAP: Net::LDAP. Net::LDAP wraps up communicating w/LDAP in a neat little API that's pretty easy to use. While its certainly possible to add, modify and/or delete objects managed by your LDAP server, I'm going to concentrate on looking for stuff. After all, we can't eliminate the last prince in the galaxy if we can't find him, right?

Furthermore, Net::LDAP has pretty helpful documentation. They get extra respect knuckles for the "Quick Start for the Impatient" section, which had me off and running in a few short minutes. Mad props, you guys!

We Brake For Nobody:  Setting Up Your Connection

Before we can fire off our LDAP queries to find the last prince in the galaxy, we have to connect to our LDAP server.  Fortunately, Net::LDAP has made that pretty easy for us.  Here's one way to set up a connection:

ldap = Net::LDAP.new  :host => "galaxy.corp.com", # your LDAP host name or IP goes here,
:port => "389", # your LDAP host port goes here,
:encryption => :simple_tls,
:base => "DC=corp,DC=com", # the base of your AD tree goes here,
:auth => {
:method => :simple,
:username => "snotty@spaceball.gov", # a user w/sufficient privileges to read from AD goes here,
:password => "12345" # the user's password goes here
}

Here's another way to connect to your LDAP server:

credentials = {
:method => :simple,
:username => "snotty@spaceball.gov",
:password => "12345"
}

Net::LDAP.open(:host => "galaxy.corp.com", :port => 389, :encryption => :simple_tls, :base => "DC=corp,DC=com", :auth => credentials) do |ldap|
# Do all your LDAP stuff here...
ldap.search(...)
ldap.add(...)
ldap.add(...)
ldap.modify(...)
end

What's the difference? Glad you asked! When you call Net::LDAP.new you're merely setting up your LDAP connection - no network traffic is actually sent to the LDAP server. Rather, when you call ldap.add or ldap.search (as we'll see in a minute), ldap.bind is called implicitly. The call to ldap.bind opens a connection to the server, does its thing, then disconnects. Therefore, when you use the Net::LDAP.new method to set up your LDAP connection, each LDAP operation will create its own connection to the LDAP server.

On the other hand, the Net::LDAP.open method opens a connection to the LDAP server, runs all the user code supplied in the code block, then closes the connection when the end of the code block is reached.

What Do I Do? I Can't Make Decisions - I'm a President!

Armed with the understanding of how new differs from open, which method should you choose? Well, I tend to think of connections as precious resources that shouldn't be wasted. So, if you're only planning on running one operation against the LDAP server then either one should be fine. However, if you have a bunch of operations to run against the LDAP server, I suspect using open and executing all your operations within the code block will be a bit more efficient. You'll only have to pay the cost of opening a network connection to the LDAP server once, rather than opening a new connection for each operation.

Searching LDAP:  Comb the Desert!

Searchin', like pimpin', ain't easy. 

Super - we have a connection to our LDAP server.  Let's go find some stuff, shall we?  Yes - lets.  To build a search, we need a couple things:

  • Some criteria to search for (duh).
  • What we want to get back from our query.

Looking For Stuff

The search method comes to our rescue. Search takes a number of parameters, which you can see here in the Net::LDAP documentation. Here are a couple of random musings about the parameters:

  • The :base setting in the search method supercedes whatever value has been set for :base in your connection information (i.e. whatever's specified in the :base of your new or open method).
  • I think of the :attributes array as the list of columns I want to return from a SELECT statement in SQL.
  • Where possible, I think specifying the attributes is a good thing. Everyone frowns on SELECT * in SQL, right? Same deal here. Be a responsible adult and only take what you need.
  • If aren't going to use the results of your LDAP query outside of your code block (e.g. you're just checking for the existence of something), it might be worthwhile to set :return_result to false. Why bother to bring back a bunch of data if you're going to ignore it?  See the previous bullet about being a grown-up.
    • Also, see the answer to this stackoverflow post about setting the :return_result attribute.

Applying Filters

At the risk of stating the obvious (again), applying a filter (or two, or several) will allow us to narrow down what we're looking for. The Net::LDAP::Filter lets us do exactly that. The Net::LDAP::Filter has a number of methods that specify the type of filter we want to apply (e.g. equal, equal to or greater than, starts with, ends with, etc.)  You can see those methods and their details here.  After determining the type of filter you want to apply, you pass the attribute and the criteria to the filter method you've chosen, and presto!  A filter is born.  Here's an example of a very basic filter:

search_filter = Net::LDAP::Filter.eq("sAMAccountName", "lstarr")

The filter shown above will search for any object that has an sAMAccountName equal to exactly "lstarr". Just about all of the other Net::LDAP::Filter methods work in the same way. I encourage you to try them out on your own (but NOT AGAINST PRODUCTION!).

Want more than one filter? No problem - Net::LDAP::Filter's got you covered with its join method. Here's an example of using the join method to concatenate multiple filters together:

search_filter = Net::LDAP::Filter.eq("sAMAccountName", "DL-MegaMaid Zoo")
group_filter = Net::LDAP::Filter.eq("objectClass", "group")
composite_filter = Net::LDAP::Filter.join(search_filter, group_filter)

Using the filter above, we're searching for a group called "DL-MegaMaid Zoo". Presumably, this list contains everyone who cares about/is involved with Mega Maid's zoo. Hopefully an e-mail was sent to this group before the self-destruct sequence finished.

Concerning Search Results

There are a couple of things to note about search results:

  • Often times, the value of each attribute will be an array, even if that array only has one item. The "mail" attribute is a good example of this; it's possible for one account to have multiple e-mail addresses.
  • They say it in the docs, but it bears repeating: search queries the LDAP server and passes each entry to the caller-supplied block, as an object of type Net::LDAP::Entry. If the search returns 1000 entries, the block will be called 1000 times. If the search returns no entries, the block will not be called.

6? What Happened to 7?!

Why don't we take a 5 minute break?

Inevitably, errors happen. It's the sad truth of our profession. Net::LDAP allows us to get the response code and message from an operation with a nifty method called get_operation_result. Using this method, we can get the message that came back from the LDAP server in the event of an error.  If everything went awesome, the code will be 0 and the message will be "Success".  Otherwise, the code will be a number and the message; will be a brief description of what went wrong.  Here's what that might look like:

ldap = Net::LDAP.new	:host => "galaxy.corp.com",
:port => 389,
:encryption => :simple_tls,
:base => "DC=corp,DC=com",
:auth => {
:method => :simple,
:username => "snotty@spaceball.gov",
:password => "12345"
}
if ldap.bind
# Redundant? Sure - the code will be 0 and the message will be "Success".
puts "Connection successful! Code: #{ldap.get_operation_result.code}, message: #{ldap.get_operation_result.message}"
else
puts "Connection failed! Code: #{ldap.get_operation_result.code}, message: #{ldap.get_operation_result.message}"
end

Everybody Got That?

Good! As a quick re-cap, here's a quick example that puts our various code snippets together.

ldap = Net::LDAP.new  :host => "galaxy.corp.com", # your LDAP host name or IP goes here,
:port => "389", # your LDAP host port goes here,
:encryption => :simple_tls,
:base => "DC=corp,DC=com", # the base of your AD tree goes here,
:auth => {
:method => :simple,
:username => "snotty@spaceball.gov", # a user w/sufficient privileges to read from AD goes here,
:password => "12345" # the user's password goes here
}

# GET THE DISPLAY NAME AND E-MAIL ADDRESS FOR A SINGLE USER
search_param = "lstarr"
result_attrs = ["sAMAccountName", "displayName", "mail"]

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

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

If you want to see some examples of LDAP queries (how to get the e-mail address of a distribution list, how to get all the members of a distribution list), you can check out this gist.

Helpful LDAP Tools

What's a blog without links to neat-o tools?  There are a couple of programs out there that can be tremendously helpful in deciphering LDAP-land.  I've found these tools to be particularly useful in identifying the attributes that are available.

  • Apache Directory Studio:  Has installations for Mac OS X, Linux and Windows.
  • LDAP Admin:  This is a Windows-only program, but has been used with great success by other folks here at Fairway.
New Call-to-action

Sign Up For Our Monthly Newsletter

New Call-to-action