Alex Stockwell

UX, DevOps, Fatherhood, Miscellany. Not in that order.

Querying by Active Directory objectGUID, in Ruby and Python

Each example that follows includes a Ruby snippet first, followed by a Python snippet. See the gist for the complete files.

1. Setup your connections

Start off with some secure LDAP connections:

# conn.rb
require 'net/ldap'

@ldap = Net::LDAP.new(
    :host => '10.0.0.101',
    :port => 636,
    :encryption => :simple_tls,
    :auth => {
        :method   => :simple,
        :username => 'user',
        :password => 'password'
    }
)
raise "LDAP Bind failed to ldap" unless @ldap.bind
# conn.py
# after `pip install ldap3`
from ldap3 import Server, Connection, SIMPLE, SYNC, ASYNC, SUBTREE, ALL

s = Server('10.0.0.101', port = 636, use_ssl = True)
c = Connection(s, auto_bind = True, client_strategy = SYNC, user='user', password='password', authentication=SIMPLE)

Note: Remeber to unbind all LDAP connections when you’re done!

2. Parsing the GUID: Some background on objectGUID

Many LDAP browsers will display the objectGUID as an Octet String. We’ll choose the AD group GroupXYZ to use as an example: it’s objectGUID is {27E13532-C68A-46DB-989D-9FACED3A4E7F}.

However, when you query an object and display it’s objectGUID, it comes across as a bytearray/binary (BER) representation:

# query.rb
query = "(&(objectClass=group)(cn=GroupXYZ))"

results = @ldap.search(
    :filter => Net::LDAP::Filter.construct(query),
    :base => "DC=company,DC=com",
    :attributes => [:cn, :objectGUID],
)

p results.first.objectguid.first
# => "25\xE1'\x8A\xC6\xDBF\x98\x9D\x9F\xAC\xED:N\x7F"
# query.py
search_filter = '(&(objectClass=group)(cn={0}))'.format("GroupXYZ")

c.search(
    search_base = 'DC=company,DC=com',
    search_filter = search_filter,
    attributes = ['cn', 'objectGUID']
)

group = c.response
print(group[0]['attributes']['objectGUID'][0])
# => b"25\xe1'\x8a\xc6\xdbF\x98\x9d\x9f\xac\xed:N\x7f"

To further complicate things, these GUIDs are stored in Oracle Raw16 format, which is a binary storage structure that changes the byte order somewhat. So we’ll need some conversion helpers:

# guid_splitter.rb
module GuidSplitter
    def self.to_oracle_raw16(string, strip_dashes=true, dashify_result=false)
        oracle_format_indices = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]
        string = string.gsub("-"){ |match| "" } if strip_dashes
        parts = split_into_chunks(string)
        result = oracle_format_indices.map { |index| parts[index] }.reduce("", :+)
        if dashify_result
            result = [result[0..7], result[8..11], result[12..15], result[16..19], result[20..result.size]].join('-')
        end
        return result
    end

    def self.split_into_chunks(string, chunk_length=2)
        chunks = []
        while string.size >= chunk_length
            chunks << string[0, chunk_length]
            string = string[chunk_length, string.size]
        end
        chunks << string unless string.empty?
        return chunks
    end

    def self.pack_guid(string)
        [to_oracle_raw16(string)].pack('H*')
    end

    def self.unpack_guid(hex)
        to_oracle_raw16(hex.unpack('H*').first, true, true)
    end
end
# guid_splitter.py
def split_into_chunks(string, chunk_length=2):
    chunks = []
    while len(string) > 0:
        chunks.append(string[:chunk_length])
        string = string[chunk_length:]
    return chunks

def to_oracle_raw16(string, strip_dashes=True, dashify_result=False):
    oracle_format_indices = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]
    if strip_dashes:
        string = string.replace("-", "")
    parts = split_into_chunks(string)
    result = ""
    for index in oracle_format_indices:
        result = result + parts[index]
    if dashify_result:
        result = result[:8] + '-' + result[8:12] + '-' + result[12:16] + '-' + result[16:20] + '-' + result[20:]
    return result

def pack_guid(string):
    return bytearray.fromhex(to_oracle_raw16(string))

def unpack_guid(ba):
    hex_s = "".join("%02x" % b for b in ba)
    return to_oracle_raw16(hex_s, True, True)

These functions return the actual GUID’s of the objects in question:

# query.rb
# ...snip...
p GuidSplitter::unpack_guid(results.first.objectguid.first)
# => "27e13532-c68a-46db-989d-9faced3a4e7f"
# query.py
# ...snip...
print(unpack_guid(group[0]['attributes']['objectGUID'][0]))
# => 27e13532-c68a-46db-989d-9faced3a4e7f

Apologies the python code is not as polished, it wasn’t the end goal. Fortunately ldap3 comes with a handy function escape_bytes() that saves us some time also, so I’d encourage pythonistas to review that documentation.

3. Querying via objectGUID

So to go the other direction and query based on the GUID:

# query.rb
target_group_guid = "27e13532-c68a-46db-989d-9faced3a4e7f"

query = "(&(objectClass=group)(cn=#{ GuidSplitter::pack_guid(target_group_guid) }))"

results = @ldap.search(
    :filter => Net::LDAP::Filter.construct(query),
    :base => "DC=company,DC=com",
    :attributes => [:cn, :objectGUID],
)
p results.first.cn.first
# => "GroupXYZ"
# query.py
from ldap3.utils.conv import escape_bytes

target_group_guid = "27e13532-c68a-46db-989d-9faced3a4e7f"
packed = pack_guid(target_group_guid)

search_filter = '(&(objectClass=group)(objectGUID={0}))'.format(escape_bytes(packed))

c.search(
    search_base = 'DC=company,DC=com',
    search_filter = search_filter,
    attributes = ['cn', 'objectGUID']
)

group = c.response
print(group[0]['attributes']['cn'][0])
# => "GroupXYZ"

Using the code and techniques above, you can also go directly from one query to the next, using the values returned from AD to feed subsequent queries.

Happy coding!