Ruby and Active Directory may be worlds apart but can play nice together thanks to Active Directory’s LDAP underpinnings.


Active Directory is LDAP

Active Directory is a popular solution for managing authentication, authorization, and directory listings in computer networks. It is built on top of LDAP version 2 and 3 and a Microsoft variant of Kerberos and DNS.

LDAP is an open protocol designed for very fast reading. Data within LDAP is organized into a tree-like structure and queries are generally made against a specified subtree.

Ruby Speaks LDAP Well

net-ldap is a well-established Ruby gem for communicating with LDAP servers.

Let’s jump right in and search for a user in Active Directory.

require 'net-ldap'

  # Describe server's address and provide authentication
  server = {
    :host => 'ldap.example.com',
    :base => 'ou=People,dc=ldap,dc=example,dc=com',
    :port => 636,
    :encryption => :simple_tls,
    :auth => {
      :method => :simple,
      :username => 'somebody',
      :password => 'something'
    }
  }

  # 'Binding' is LDAP vernacular for 'connecting'
  conn = Net::LDAP.new(server)
  conn.bind

  results = conn.search(:filter => Net::LDAP::Filter.eq("sAMAccountName", 'a_loginid'))

  raise "LDAP error. Code: #{conn.get_operation_result.code }, Reason: #{conn.get_operation_result.message}" unless conn.get_operation_result.code == 0

  raise "No users found or too many users found." unless results.length == 1

  user = results[0]

  # Print their display name (typically their full name)
  puts user[:displayname]

  # Print their user creation date
  puts user[:whencreated]

  # Print their street address
  puts user[:streetaddress]

If you poke around the user object you’ll find quite a bit of information, a lot of it you may not need.

What about groups?

# Assuming we've binded 'conn' already in the example above ...

  results = conn.search(:filter => Net::LDAP::Filter.eq("cn", 'group_name'))

  raise "LDAP error. Code: #{conn.get_operation_result.code }, Reason: #{conn.get_operation_result.message}" unless conn.get_operation_result.code == 0

  raise "No groups found or too many groups found." unless results.length == 1

  group = results[0]

  # Print the group's name
  puts group[:name]

  # Print the group's members
  puts group[:member]

  # Count the group's members
  puts group[:member].length

It’s pretty straightforward once you know the layout of your Active Directory trees and what each object looks like.

How about adding a user to a group?

# Assuming 'user' and 'group' are objects we already fetched from Active Directory ...

  result = conn.modify(:dn => group[:distinguishedname][0],
    :operations => [ [ :add, :member, user[:distinguishedname][0] ] ])

  if conn.get_operation_result.code == LDAP_ALREADY_EXISTS
    puts "User already in group."
  elsif conn.get_operation_result.code != 0
    raise "LDAP error. Code: #{conn.get_operation_result.code }, Reason: #{conn.get_operation_result.message}"
  else
    if result == true
      puts "User added to group."
    else
      puts "Something went wrong."
    end
  end

There exists an active-directory gem to provide a more attractive and abstract experience when dealing with Active Directory if you prefer. At the time of this writing it does not support reading/writing users and groups from multiple, connected Active Directory servers so the above LDAP methods may prove useful to programmers in that scenario.