Directory Services

Retrieving Deleted Objects

Deleted objects are stored in the Deleted Objects container. The Deleted Objects container is not normally visible, but the Deleted Objects container can be bound to by a member of the administrators group. The contents of the Deleted Objects container can be enumerated and individual deleted object attributes can be obtained using the IDirectorySearch interface with the ADS_SEARCHPREF_TOMBSTONE search preference.

The Deleted Objects container can be obtained by binding to the well-known GUID GUID_DELETED_OBJECTS_CONTAINER defined in Ntdsapi.h. For more information about binding to well-known GUIDs, see Binding to Well-Known Objects Using WKGUID.

Specify the ADS_FAST_BIND option when binding to the Deleted Objects container. This means that the ADSI interfaces used to work with an Active Directory object, such as IADs and IADsPropertyList, cannot be used on the Deleted Objects container. For more information and a code example that shows how to bind to the Deleted Objects container, see the GetDeletedObjectsContainer example function below.

Enumerating Deleted Objects

The IDirectorySearch interface is used to search for deleted objects.

To enumerate deleted objects

  1. Obtain the IDirectorySearch interface for the Deleted Objects container. This is accomplished by binding to the Deleted Objects container and requesting the IDirectorySearch interface. For more information and a code example that shows how to bind to the Deleted Objects container, see the GetDeletedObjectsContainer example function below.
  2. Set the ADS_SEARCHPREF_SEARCH_SCOPE search preference to ADS_SCOPE_ONELEVEL using the IDirectorySearch::SetSearchPreference method. The ADS_SCOPE_SUBTREE preference can also be used, but the Deleted Objects container is only one level, so using ADS_SCOPE_SUBTREE is redundant.
  3. Set the ADS_SEARCHPREF_TOMBSTONE search preference to TRUE. This causes the search to include deleted objects.
  4. Set the ADS_SEARCHPREF_PAGESIZE search preference to a value less than, or equal to, 1000. This is optional, but if this is not done, no more than 1000 deleted objects can be retrieved.
  5. Set the search filter in the IDirectorySearch::ExecuteSearch call to "(isDeleted=TRUE)". This causes the search to only retrieve objects with the isDeleted attribute set to TRUE.

For a code example code that shows how to enumerate deleted objects, see the EnumDeletedObjects example function below.

The search can be narrowed further by adding to the search filter as shown in LDAP Dialect. For example, to search for all of the deleted objects with a name that begins with "Jeff", the search filter would be set to "(&(isDeleted=TRUE)(cn=Jeff*))".

Because deleted objects have most of their attributes removed when they are deleted, it is not possible to bind directly to a deleted object. The ADS_FAST_BIND option must be specified when binding to a deleted object. This means that the ADSI interfaces used to work with an Active Directory object, such as IADs and IADsPropertyList, cannot be used on a deleted object container.

Finding a Specific Deleted Object

It is also possible to find a specific deleted object. If the objectGUID of the object is known, it can be used to search for the object with that specific objectGUID. For more information and a code example that shows how to find a specific deleted object, see FindDeletedObjectByGUID below.

Example Code

The following code example shows how to bind to the Deleted Objects container.

//***************************************************************************
//
//  GetDeletedObjectsContainer()
//
//  Binds to the Deleted Object container.
//
//***************************************************************************

HRESULT GetDeletedObjectsContainer(IADsContainer **ppContainer)
{
	if(NULL == ppContainer)
	{
		return E_INVALIDARG;
}

	HRESULT hr;
	IADs *pRoot;

	*ppContainer = NULL;

	// Bind to the rootDSE object.
	hr = ADsOpenObject(L"LDAP://rootDSE",
					NULL,
					NULL,
					ADS_SECURE_AUTHENTICATION,
					IID_IADs,
					(LPVOID*)&pRoot);
	if(SUCCEEDED(hr))
	{
		VARIANT var;
	
		VariantInit(&var);

		// Get the current domain DN.
		hr = pRoot->Get(CComBSTR("defaultNamingContext"), &var);
		if(SUCCEEDED(hr))
		{
			// Build the binding string.
			LPWSTR pwszFormat = L"LDAP://<WKGUID=%s,%s>";
			LPWSTR pwszPath;

			pwszPath = new WCHAR[wcslen(pwszFormat) + wcslen(GUID_DELETED_OBJECTS_CONTAINER_W) + wcslen(var.bstrVal)];
			if(NULL != pwszPath)
			{
				swprintf(pwszPath, pwszFormat, GUID_DELETED_OBJECTS_CONTAINER_W, var.bstrVal);

				// Bind to the object.
				hr = ADsOpenObject(pwszPath,
								NULL,
								NULL,
								ADS_FAST_BIND | ADS_SECURE_AUTHENTICATION,
								IID_IADsContainer,
								(LPVOID*)ppContainer);

				delete pwszPath;
		}
			else
			{
				hr = E_OUTOFMEMORY;
		}

			VariantClear(&var); 
	}

		pRoot->Release(); 
}

	return hr;
}

The following code example shows how to enumerate the objects in the Deleted Objects container.

//***************************************************************************
//
//  EnumDeletedObjects()
//
//  Enumerates all of the objects in the Deleted Objects container.
//
//***************************************************************************

HRESULT EnumDeletedObjects()
{
	HRESULT hr;
	IADsContainer *pDeletedObjectsCont = NULL;
	IDirectorySearch *pSearch = NULL;

	hr = GetDeletedObjectsContainer(&pDeletedObjectsCont);
	if(FAILED(hr))
	{
		goto cleanup;
}

	hr = pDeletedObjectsCont->QueryInterface(IID_IDirectorySearch, (LPVOID*)&pSearch); 
	if(FAILED(hr))
	{
		goto cleanup;
}

	ADS_SEARCH_HANDLE hSearch;

	// Only search for direct children of the container.
	ADS_SEARCHPREF_INFO rgSearchPrefs[3];
	rgSearchPrefs[0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
	rgSearchPrefs[0].vValue.dwType = ADSTYPE_INTEGER;
	rgSearchPrefs[0].vValue.Integer = ADS_SCOPE_ONELEVEL;

	// Search for deleted objects.
	rgSearchPrefs[1].dwSearchPref = ADS_SEARCHPREF_TOMBSTONE;
	rgSearchPrefs[1].vValue.dwType = ADSTYPE_BOOLEAN;
	rgSearchPrefs[1].vValue.Boolean = TRUE;

	// Set the page size.
	rgSearchPrefs[2].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
	rgSearchPrefs[2].vValue.dwType = ADSTYPE_INTEGER;
	rgSearchPrefs[2].vValue.Integer = 1000;

	// Set the search preference
	hr = pSearch->SetSearchPreference(rgSearchPrefs, ARRAYSIZE(rgSearchPrefs));
	if(FAILED(hr))
	{
		goto cleanup;
}

	// Set the search filter.
	LPWSTR pszSearch = L"(cn=*)";

	// Set the attributes to retrieve.
	LPWSTR rgAttributes[] = {L"cn", L"distinguishedName", L"lastKnownParent"};

	// Execute the search
	hr = pSearch->ExecuteSearch(	pszSearch,
									rgAttributes,
									ARRAYSIZE(rgAttributes),
									&hSearch);
	if(SUCCEEDED(hr))
	{ 
		// Call IDirectorySearch::GetNextRow() to retrieve the next row of data
		while(S_OK == (hr = pSearch->GetNextRow(hSearch)))
		{
			ADS_SEARCH_COLUMN col;
			UINT i;
		
			// Enumerate the retrieved attributes.
			for(i = 0; i < ARRAYSIZE(rgAttributes); i++)
			{
				hr = pSearch->GetColumn(hSearch, rgAttributes[i], &col);
				if(SUCCEEDED(hr))
				{
					switch(col.dwADsType)
					{
						case ADSTYPE_CASE_IGNORE_STRING:
						case ADSTYPE_DN_STRING:
						case ADSTYPE_PRINTABLE_STRING:
						case ADSTYPE_NUMERIC_STRING:
						case ADSTYPE_OCTET_STRING:
							wprintf(L"%s: ", rgAttributes[i]); 
							for(DWORD x = 0; x < col.dwNumValues; x++)
							{
								wprintf(col.pADsValues[x].CaseIgnoreString); 
								if((x + 1) < col.dwNumValues)
								{
									wprintf(L","); 
							}
						}
							wprintf(L"\n"); 
							break;
				}

					pSearch->FreeColumn(&col);
			}
		}

			wprintf(L"\n");
	}

		// Close the search handle to clean up.
		pSearch->CloseSearchHandle(hSearch);
}

cleanup:

	if(pDeletedObjectsCont)
	{
		pDeletedObjectsCont->Release();
}

	if(pSearch)
	{
		pSearch->Release();
}

	return hr;
}

The following code example shows how to find a specific deleted object from the object's objectGUID property.

HRESULT FindDeletedObjectByGUID(IADs *padsDomain, GUID *pguid)
{
	HRESULT hr;
	IDirectorySearch *pSearch = NULL;
	LPWSTR pwszGuid = NULL;

	hr = padsDomain->QueryInterface(IID_IDirectorySearch, (LPVOID*)&pSearch); 
	if(FAILED(hr))
	{
		goto cleanup;
}

	ADS_SEARCH_HANDLE hSearch;

	// Search the entire tree.
	ADS_SEARCHPREF_INFO rgSearchPrefs[2];
	rgSearchPrefs[0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
	rgSearchPrefs[0].vValue.dwType = ADSTYPE_INTEGER;
	rgSearchPrefs[0].vValue.Integer = ADS_SCOPE_SUBTREE;

	// Search for deleted objects.
	rgSearchPrefs[1].dwSearchPref = ADS_SEARCHPREF_TOMBSTONE;
	rgSearchPrefs[1].vValue.dwType = ADSTYPE_BOOLEAN;
	rgSearchPrefs[1].vValue.Boolean = TRUE;

	// Set the search preference.
	hr = pSearch->SetSearchPreference(rgSearchPrefs, 2);
	if(FAILED(hr))
	{
		goto cleanup;
}

	// Set the search filter.
	hr = ADsEncodeBinaryData((LPBYTE)pguid, sizeof(GUID), &pwszGuid);
	if(FAILED(hr))
	{
		goto cleanup;
}

	LPWSTR pwszFormat = L"(objectGUID=%s)";

	LPWSTR pwszSearch = new WCHAR[lstrlenW(pwszFormat) + lstrlenW(pwszGuid) + 1];
	if(NULL == pwszSearch)
	{
		goto cleanup;
}

	wsprintfW(pwszSearch, pwszFormat, pwszGuid);

	FreeADsMem(pwszGuid);

	// Set the attributes to retrieve.
	LPWSTR rgAttributes[] = {L"cn", L"distinguishedName", L"lastKnownParent"};

	// Execute the search.
	hr = pSearch->ExecuteSearch(	pwszSearch,
									rgAttributes,
									3,
									&hSearch);
	if(SUCCEEDED(hr))
	{ 
		// Call IDirectorySearch::GetNextRow() to retrieve the next row of data.
		while(S_OK == (hr = pSearch->GetNextRow(hSearch)))
		{
			ADS_SEARCH_COLUMN col;
			UINT i;
		
			// Enumerate the retrieved attributes.
			for(i = 0; i < ARRAYSIZE(rgAttributes); i++)
			{
				hr = pSearch->GetColumn(hSearch, rgAttributes[i], &col);
				if(SUCCEEDED(hr))
				{
					switch(col.dwADsType)
					{
						case ADSTYPE_CASE_IGNORE_STRING:
						case ADSTYPE_DN_STRING:
						case ADSTYPE_PRINTABLE_STRING:
						case ADSTYPE_NUMERIC_STRING:
							wprintf(L"%s: ", rgAttributes[i]); 
							for(DWORD x = 0; x < col.dwNumValues; x++)
							{
								wprintf(col.pADsValues[x].CaseIgnoreString); 
								if((x + 1) < col.dwNumValues)
								{
									wprintf(L","); 
							}
						}
							wprintf(L"\n"); 
							break;

						case ADSTYPE_OCTET_STRING:
							wprintf(L"%s: ", rgAttributes[i]); 
							for(DWORD x = 0; x < col.dwNumValues; x++)
							{
								GUID guid;
								LPBYTE pb = col.pADsValues[x].OctetString.lpValue;
								WCHAR wszGUID[MAX_PATH];

								// Convert the octet string into a GUID.
								guid.Data1 = *((long*)pb);
								pb += sizeof(guid.Data1);
								guid.Data2 = *((short*)pb);
								pb += sizeof(guid.Data2);
								guid.Data3 = *((short*)pb);
								pb += sizeof(guid.Data3);
								CopyMemory(&guid.Data4, pb, sizeof(guid.Data4));

								// Convert the GUID into a string.
								StringFromGUID2(guid, wszGUID, MAX_PATH);
								wprintf(wszGUID);

								if((x + 1) < col.dwNumValues)
								{
									wprintf(L","); 
							}
								OutputDebugStringW(wszGUID);
								OutputDebugStringW(L"\n");

								{
									DWORD a;

									for(a = 0, *wszGUID = 0; a < col.pADsValues[x].OctetString.dwLength; a++)
									{
										wsprintfW(wszGUID + lstrlenW(wszGUID), L"%02X", *(LPBYTE)(col.pADsValues[x].OctetString.lpValue + a));
								}

									OutputDebugStringW(wszGUID);
									OutputDebugStringW(L"\n");
							}
						}
							wprintf(L"\n"); 
							break;

				}

					pSearch->FreeColumn(&col);
			}
		}

			wprintf(L"\n");
	}

		// Close the search handle to clean up.
		pSearch->CloseSearchHandle(hSearch);
}

cleanup:

	if(pwszSearch)
	{
		delete pwszSearch;
}

	if(pSearch)
	{
		pSearch->Release();
}

	return hr;
}