Last time I talked briefly about IDirectoryObject and IDirectorySearch, let’s go into a little more detail today.

IDirectoryObject is an Interface that we can use to query anything in Active Directory, users, groups, organizational units, containers and so on.

I thought the best explanation would be to build a very small sample project, so let’s do that!

First we need some units, so please add the following units to your uses clause:

  • ComObj (for EOleException and it calls CoInitialize for us)
  • JwaWindows for the proper Adsi declarations

Next declare the following types:

type
  // For Delphi < 2009 use WideString
  UString = {$IFDEF UNICODE}UnicodeString{$ELSE}WideString{$ENDIF};

  // Array of ADS_ATTR_INFO records
  TAdsAttrInfoArray = array[0..ANYSIZE_ARRAY-1] of ADS_ATTR_INFO;
  PAdsAttrInfoArray = ^TAdsAttrInfoArray;

Now add a Button with the name StartButton and a Memo with the name Memo1 to your form. Place the code in the OnClick handler and declare some vars:

procedure TForm1.StartButtonClick(Sender: TObject);
var
  hr: HRESULT;
  IADsIntf: IADs;
  DefNamingContext: UString;
  DirObjIntf: IDirectoryObject;
  PathName: UString;
  Attributes: array of PWideChar;
  pAttributeEntries: PAdsAttrInfoArray;
  Count: DWORD;
  i: Integer;

First we will retrieve an IADs interface to a special PathName called rootDSE. rootDSE enables us to do a serverless binding to the default domain without knowing the domain name., this is perfect for our example code since you can all use it without changing the PathName.

 // Get IADs Interface to rootDSE
  hr := AdsGetObject(‘LDAP://rootDSE’, IID_IADs, Pointer(IADsIntf));
  if FAILED(hr) then
    Exit;

In real life you will probably raise an exception instead of just exiting on failure…

Now we are going to read a property of rootDSE called defaultNamingContext which contains the DNS domain name (egDC=domain,DC=local):

 try
    // The property defaultNamingContext contains the DNS domain name
    // eg DC=domain,DC=local
    DefNamingContext := IADsIntf.Get(‘defaultNamingContext’);
  except
    on E: EOleException do
    begin
      // Handle the error, we got no result!
      // E.ErrorCode contains the HRESULT
    end;
  end;

  Memo1.Lines.Add(Format(‘defaultNamingContext: %s’, [DefNamingContext]));
  // We have no further use for the IADs Interface
  IADsIntf := nil;

Notice that IADs.Get is declared as SafeCall so we need to wrap this in a try..except block, IADs.Get will return an EOleException in case of failure

The last step before we can obtain the IDirectoryObject Interface is to compose the PathName. I am going to work with the Administrator account as this account will probably exist in most domains. Don’t worry if you don’t have Admin privileges in your domain, any domain account can read properties.

 // Now build the Path String
  PathName := ‘LDAP://CN=Administrator,CN=Users,’ + DefNamingContext;

To obtain the IDirectoryObject Interface we can use AdsGetObject but this time we use IID_IDirectoryObject:

 hr := AdsGetObject(
    PWideChar(PathName),  // lpszPathName
    IID_IDirectoryObject, // refiid
    Pointer(DirObjIntf)); // ppObject

  if FAILED(hr) then
    Exit;

We will now read out two attributes from the Administrator account, the username (sAMAccountName) and the description (description). Note that attribute names in Active Directory do not use CamelCase like in Delphi but Hungarian notation. Confusingly enough Microsoft refers to CamelCase as Pascal Casing and to Hungarian Notation as Camel Casing.

Although IDirectoryObject is not case sensitive I always try to keep the same case, so I use sAMAccountName and not SamAccountName when I refer to the attribute.

We are going to use the GetObjectAttributes function to read Attributes values, its first parameter is an array of AttributeNames:

 { Set Attribute Array }
  SetLength(Attributes, 2);
  Attributes[0] := ‘sAMAccountName’;
  Attributes[1] := ‘description’;

The second parameter is the count of attributes, followed by a pointer to an array of ADS_ATTR_INFO records (PAdsAttrInfoArray) and the number of attributes returned:

 hr := DirObjIntf.GetObjectAttributes(
    @Attributes[0],                    // pAttributeNames
    Length(Attributes),                // dwNumberAttributes
    PADS_ATTR_INFO(pAttributeEntries), // ppAttributeEntries
    Count);                            // pdwNumAttributesReturned

  if FAILED(hr) then
    Exit;

It is important to note some things: the function can succeed but return less values than requested and the order of attributes returned is not necessarily the same as requested!

This means you should always check the pszAttrName member of the ADS_ATTR_INFO record and read the data from the pADsValues member:

 // Now loop through the results
  for i := 0 to Count – 1 do
  begin
    Memo1.Lines.Add(Format(‘Attribute %s has Value %s’,
      [pAttributeEntries^[i].pszAttrName,
       pAttributeEntries^[i].pADsValues^.CaseIgnoreString]));
  end;

When we are done we need to free the memory:

 // Don’t forget to free memory!
  FreeAdsMem(pAttributeEntries);

That’s it for today, you can find the sample project below. Next time I will discuss IDirectorySearch.

AdSample1 (137)

Related posts:

  1. Query Active Directory from Excel
  2. Reading accountExpires attribute from Active Directory (in Delphi)
  3. Random Active Directory Notes #1
  4. Random Active Directory Notes
  5. Random Active Directory Notes #2