$theTitle=wp_title(" - ", false); if($theTitle != "") { ?>
About Virtualization, VDI, SBC, Application Compatibility and anything else I feel like
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:
Next declare the following types:
1 2 3 4 5 6 7 | 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:
1 2 3 4 5 6 7 8 9 10 11 | 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.
1 2 3 4 | // 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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.
1 2 | // 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:
1 2 3 4 5 6 7 | 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:
1 2 3 4 | { 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:
1 2 3 4 5 6 7 8 | 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:
1 2 3 4 5 6 7 | // 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:
1 2 | // 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 (11432 downloads )
4 Responses for "Random Active Directory Notes #3"
I have almost got this to work, but i can’t find the declaration for ADS_ATTR_INFO
Steve, it’s in the Jedi Apilib.
Hi,
I have test the IDirectorySearch, with your file from jwaActiveDS_TLB, this work on Win32, but the GetColumn does not work on Win64.
Do you know why ?
Thanks
Yannick
Hi Yannick,
I think the problem is in
ads_search_column = packed record
pszAttrName: PWideChar;
dwADsType: ADSTYPEENUM;
pADsValues: ^_adsvalue;
dwNumValues: LongWord;
hReserved: Pointer;
end;
try changing LongWord to DWORD or NativeUInt
Leave a reply