Rapid Searching

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,
[in] LPWSTR __RPC_FAR *pAttributeNames,
[in] DWORD dwNumberAttributes,
[out] PADS_SEARCH_HANDLE phSearchResult
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,
[out] LPWSTR __RPC_FAR *ppszColumnName
Get the name of the next column of retrieved data
GetColumn() [in] ADS_SEARCH_HANDLE hSearchResult,
[in] LPWSTR szColumnName, [out] PADS_SEARCH_COLUMN pSearchColumn
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.

© 1998 by Wrox Press. All rights reserved.