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)(objectGUID=#{ 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!