One of the advertised features of LDAP, as opposed to the standard Windows directory, is the ability to make highly efficient searches. The way to use this facility in ADSI is via one of the pure COM interfaces available, called IDirectorySearch
. This interface has the following methods:
Method | Parameters | Description |
SetSearchPreference() |
[in] PADS_SEARCHPREF_INFO pSearchPrefs, [in] DWORD dwNumPrefs |
Set up search preferences; this enables you to define time limits for the search, size limits, and so on |
ExecuteSearch() |
[in] LPWSTR pszSearchFilter, |
Start search; the filter specifies which properties are to be searched for, in LDAP format- |
AbandonSearch() |
[in] ADS_SEARCH_HANDLE hSearchResult |
Abandon a search |
GetFirstRow() |
[in] ADS_SEARCH_HANDLE hSearchResult |
Get the first row of retrieved data |
GetNextRow() |
[in] ADS_SEARCH_HANDLE hSearchResult |
Get the next row |
GetPreviousRow() |
[in] ADS_SEARCH_HANDLE hSearchResult |
Get the previous row |
GetNextColumnName() |
[in] ADS_SEARCH_HANDLE hSearchHandle, |
Get the name of the next column of retrieved data |
GetColumn() |
[in] ADS_SEARCH_HANDLE hSearchResult, |
Get the details of the property settings for the specified column into a column descriptor |
FreeColumn() |
[in] PADS_SEARCH_COLUMN pSearchColumn |
Free a column descriptor |
CloseSearchHandle() |
ADS_SEARCH_HANDLE hSearchResult |
Release search resources |
Much of this style of searching has strayed in from OLE DB. This is hardly surprising, as ADSI providers are OLE DB providers. This incidentally means that there is an OLE DB interface to the Active Directory database as well, but that's outside the scope of this book.
The idea is that you specify a search by saying what you are looking for (rows), and optimize it by saying what properties you're actually interested in (columns). The search then returns you a grid full of data, which you can then navigate your way around using the methods provided.
This sounds fine, but how do we get IDirectorySearch
in the first place? Easy! It's just another interface implemented by ADs
objects. Let's take it for a spin, and write ourselves a little search application.
Once again, we'll use MFC (and COM smart pointers). We need to get the user to specify the start path for the search, the search filter, and the list of properties to fetch. The dialog looks like this:
We have two edit fields at the top of the dialog:
IDC_PATH
— the user will use this box to specify the start path for the search
IDC_FILTER
— this is where the user enters the search filter
The two list boxes are:
IDC_PROPERTIES
— we'll display all the properties from the schema container in this list box, and the user can select the ones that they are interested in retrieving from this box
IDC_VALUES
— this is the list box where the results of the search will be displayed
The buttons are IDC_SEARCH
and IDOK
.
In order to help the user select the properties from a list, we'll provide them with a full list of the available ones. We'll populate this list in the OnInitDialog()
method by enumerating the schema:
BOOL CDirSearchDlg::OnInitDialog()
{
...
// Get hold of LDAP schema container
IADsContainerPtr pContainer;
HRESULT hr = ADsGetObject(L"LDAP://schema", IID_IADsContainer,
reinterpret_cast<void**>(&pContainer));
if (FAILED(hr))
{
DisplayComError(_T("Failed to get schema container"), hr);
}
// Construct enumerator
IEnumVARIANTPtr pEnum;
hr = ADsBuildEnumerator(pContainer, &pEnum);
if (FAILED(hr))
{
DisplayComError(_T("Failed to build enumerator"), hr);
return TRUE;
}
CListBox* pProperties =
static_cast<CListBox*>(GetDlgItem(IDC_PROPERTIES));
// Cycle through items in schema
while (true)
{
// Get next child (i.e. property, because we're in the schema)
_variant_t vChild;
hr = ADsEnumerateNext(pEnum, 1, &vChild, NULL);
if (hr != S_OK)
break;
// Get property interface
IADsPropertyPtr pChild;
hr = vChild.pdispVal->QueryInterface(IID_IADsProperty,
reinterpret_cast<void**>(&pChild));
if (hr != S_OK)
continue;
// Extract name and add to displayed list
BSTR bstrName;
pChild->get_Name(&bstrName);
CString csName = bstrName;
SysFreeString(bstrName);
pProperties->AddString(csName);
}
return TRUE; // return TRUE unless you set the focus to a control
}
Notice that we eliminate all the non-property objects by the simple expedient of QI
'ing for the IADsProperty
interface. This is derived from IADs
, so we get access to get_Name()
without any further QI
's. This is what our user sees:
Our user can now select whichever properties are to be fetched, a start point and a search path. Here is the code that handles the search:
void CDirSearchDlg::OnSearch()
{
// Get path from user input
CString csPath;
GetDlgItemText(IDC_PATH, csPath);
// Construct search object based on current path
IDirectorySearchPtr pSearch;
HRESULT hr = ADsGetObject(_bstr_t(csPath), IID_IDirectorySearch,
reinterpret_cast<void**>(&pSearch));
if (FAILED(hr))
{
DisplayComError(_T("Failed to get search object"), hr);
return;
}
Remember that the directory search was simply another interface on the ADS object?
// Get filter from user input
CString csFilter;
GetDlgItemText(IDC_FILTER, csFilter);
// Build list of selected properties
CListBox* pProperties =
static_cast<CListBox*>(GetDlgItem(IDC_PROPERTIES));
int nProp = pProperties->GetSelCount();
LPWSTR* pwstrProps = NULL;
if (nProp)
{
pwstrProps = new LPWSTR[nProp];
int prop = 0;
CString csProp;
int nItem = pProperties->GetCount();
for (int item = 0; item < nItem; item++)
{
if (pProperties->GetSel(item))
{
pProperties->GetText(item, csProp);
pwstrProps[prop++] = csProp.AllocSysString();
}
}
}
// Execute search based on filter and required properties
ADS_SEARCH_HANDLE hSearch;
hr = pSearch->ExecuteSearch(_bstr_t(csFilter), pwstrProps, nProp,
&hSearch);
for (int prop = 0; prop < nProp; prop++)
SysFreeString(pwstrProps[prop]);
delete[] pwstrProps;
if (FAILED(hr))
{
DisplayComError(_T("Failed to execute search"), hr);
return;
}
CListBox* pValues = static_cast<CListBox*>(GetDlgItem(IDC_VALUES));
pValues->ResetContent();
The critical line is that call to ExecuteSearch()
. We pass this the filter string and the list of properties that we want to fetch (from IDC_FILTER
and IDC_PROPERTIES
respectively), and it returns us a handle, hSearch
. We'll use this in the next code segment to traverse through the data. There's one row for each object found:
// Go through returned data, row by row
while (true)
{
hr = pSearch->GetNextRow(hSearch);
if (hr != S_OK)
break;
CString csValues;
There's also one column for every property that we specified:
// Go through row, column by column
while (true)
{
LPWSTR wstrColumn = NULL;
hr = pSearch->GetNextColumnName(hSearch, &wstrColumn);
if (hr != S_OK)
break;
// Get hold of column details
ADS_SEARCH_COLUMN column;
hr = pSearch->GetColumn(hSearch, wstrColumn, &column);
CoTaskMemFree(wstrColumn);
if (FAILED(hr))
{
DisplayComError(_T("Failed to get column"), hr);
pSearch->CloseSearchHandle(hSearch);
return;
}
// Extract column name ...
CString csAttr = column.pszAttrName;
CString csValue;
// ... and value
switch (column.pADsValues->dwType)
{
case ADSTYPE_CASE_EXACT_STRING:
case ADSTYPE_CASE_IGNORE_STRING:
case ADSTYPE_PRINTABLE_STRING:
csValue = column.pADsValues->PrintableString;
break;
default:
csValue.Format("(unsupported type %d)",
column.pADsValues->dwType);
break;
}
// Construct entry segment for displayed list
CString csItem;
csItem.Format("%s = %s", static_cast<LPCSTR>(csAttr),
static_cast<LPCSTR>(csValue));
// Append segment to current entry
if (!csValues.IsEmpty())
csValues += "\t";
csValues += csItem;
pSearch->FreeColumn(&column);
}
// Add entry to list
if (!csValues.IsEmpty())
pValues->AddString(csValues);
}
pSearch->CloseSearchHandle(hSearch);
}
So how do we actually use this? Well, let's just try specifying our domain pathname (LDAP:://DC=OverTheHills,O=Internet
in my case) for the start path, and, say, a search filter of adminDescription=Built-in account for administering the computer/domain
. Why? Well, I happen to know that this will find the Administrator
user object. (OK, I admit it. I only know this because I looked at the properties for the Administrator
user object using the first code sample in this chapter …):
Neat. More seriously, you can use this mechanism to find all users with a given attribute, or all computers in a particular geographical location, or frankly, wherever your imagination leads you.