GroupMembers
Enumerating and searching groups in Active Directory
Introduction
Problem: The Customer calls you to check / to document which users can access his server share. Normally, I take a simple template LDAP query but this is error-prone and not very handy.
This little tool helps you to determine all members of a domain group in Active Directory; inclusive to members of nested groups. In addition, the tool offers a data export function through Excel-Automation. Make sure your computer is a member of a domain.
Using the code
The source code of GroupMembers shows you how you can enumerate and search the entire Forest for objects in Active Directory. The application is quite simple (without extra threads, etc.).
GroupMembers does two main steps, described below.
Part 1: Searching objects
To find objects in an Active Directory domain tree you need
to bind to a global catalog server. Therefore, we use the function
ADsOpenObject
with an LDAP path like GC:\\contoso.com. After this we can use the
IDirectorySearch
interface to search our objects.
HRESULT FindMultipleADObjects(LPWSTR pszSearchBase,
LPWSTR pszFilter, CPtrArray* arIADsList)
{
HRESULT hr = S_OK;
wchar_t pszADsPath[MAX_PATH];
IDirectorySearch* pDSSearch = NULL;
IADs* ppObj = NULL;
ADS_SEARCHPREF_INFO arSearchPrefs[3];
ADS_SEARCH_COLUMN col;ADS_SEARCH_HANDLE hSearch = NULL;
LPWSTR pszAttribute[1] = {L"ADsPath" };
// we need only one attribute
if (NULL == pszSearchBase || NULL == pszFilter ||
NULL == arIADsList)
{
return (E_INVALIDARG);
}
// binds to our base ADSI object
hr = ADsOpenObject(pszSearchBase, NULL, NULL,
ADS_SECURE_AUTHENTICATION, IID_IDirectorySearch,
(void**) &pDSSearch);
if (FAILED(hr))
return hr;
// set/use the page size -> don't stress the server
arSearchPrefs[0].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
arSearchPrefs[0].vValue.dwType =
ADSTYPE_INTEGER;
arSearchPrefs[0].vValue.Integer = 100;
// we will search also child trees
arSearchPrefs[1].dwSearchPref =
ADS_SEARCHPREF_SEARCH_SCOPE;
arSearchPrefs[1].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs[1].vValue.Integer =
ADS_SCOPE_SUBTREE;
//set a time limit for big domains
arSearchPrefs[2].dwSearchPref =
ADS_SEARCHPREF_TIME_LIMIT;
arSearchPrefs[2].vValue.dwType =
ADSTYPE_INTEGER;arSearchPrefs[2].vValue.Integer = 120;
// set the search
configurationhr = pDSSearch->SetSearchPreference(arSearchPrefs,3);
if (FAILED(hr))
{
if (pDSSearch)pDSSearch->Release();
return (hr);
}
// now start the search
hr = pDSSearch->ExecuteSearch(pszFilter,pszAttribute, (UINT) 1,
&hSearch);
if (SUCCEEDED(hr))
{
while (SUCCEEDED(hr =pDSSearch->GetNextRow(hSearch)))
{
if (S_OK == hr)
{
// extract our one attribute we need
hr = pDSSearch->GetColumn(hSearch, pszAttribute[0],
&col);
if (SUCCEEDED(hr))
{
wcsncpy(pszADsPath, col.pADsValues->CaseIgnoreString,
MAX_PATH);
pszADsPath[MAX_PATH - 1] = 0;
// open the found object
hr = ADsOpenObject(pszADsPath, NULL, NULL,
ADS_SECURE_AUTHENTICATION,IID_IADs,(void**)&ppObj);
if (SUCCEEDED(hr))
arIADsList->Add(ppObj);
// and add it to our ptr array
// !!! the caller must release the object !!!
pDSSearch->FreeColumn(&col);
}
}
else
break;
}
pDSSearch->CloseSearchHandle(hSearch);
// free the search handle
}
if (pDSSearch)
pDSSearch->Release();
// release the search object
return (hr);
}
This function takes 3 parameters: pszSearchBase
defines the
LDAP path like GC:\\contoso.com, pszFilter
defines the
search filter like
(objectClass=user), and the last parameter (arIADsList
) is a
pointer array that
stores the IADs
objects from our search results. Important
note: the returned IADs
objects must be released by the caller.
Part 2: Enumerating group members
To query object details or enumerating members we need to rebind to ADSI. Why? The global catalog server we have bound to does not have all the information we need. This information is only available on normal domain controllers.
Therefore, I use a little helper class (CADGroupList
). This
class
stores the different LDAP path names of the IADs
object for us
to rebind to
ADSI. Now, after we have found our group name, we can enumerate the group
members. We call the function below recursively.
The first parameter
is an IADsMembers
interface which we have extracted from the
group itself. This
interface is needed for enumerating the members. The second parameter is
again
a little helper class (CADGroupMemberList) which stores the group members
with
a few member details, including the group name to which the member belongs.
HRESULT EnumGroupMembers(IADsMembers* pGrpMembers,
CADGroupMemberList* pMemberList, CString csGroupName)
{
HRESULT hr = E_FAIL;
IADs* pADs = NULL;
IUnknown* pUnk = NULL;
IEnumVARIANT* pEnum = NULL;
IADsGroup* pGroup = NULL;
IADsMembers* pMembers = NULL;
VARIANT var;
ULONG lFetch = 0;
BSTR bstr;
hr = pGrpMembers->get__NewEnum(&pUnk);
if (FAILED(hr))
{
goto
Cleanup;
}
hr = pUnk->QueryInterface(IID_IEnumVARIANT, (void**) &pEnum);
if (FAILED(hr))
{
goto
Cleanup;
}
VariantInit(&var);
hr = pEnum->Next(1, &var, &lFetch);
if (hr == S_FALSE)
{
goto
Cleanup;
}
while (hr == S_OK)
{
if (lFetch == 1)
{
hr = V_DISPATCH(&var)->QueryInterface(IID_IADs,
(void**) &pADs);
if (FAILED(hr))
break;
if (ADsIsGroup(pADs))
{
hr = pADs->QueryInterface(IID_IADsGroup,
(void**) &pGroup);
if (FAILED(hr))
break;
pGroup->Members(&pMembers);
pGroup->get_Name(&bstr); // get the group name
// recursive enum call -> get nested members
hr = EnumGroupMembers(pMembers, pMemberList, bstr);
SysFreeString(bstr);
if (FAILED(hr))
break;
if (pMembers) { pMembers->Release(); pMembers = NULL; }
if (pGroup)
{
pGroup->Release();
pGroup = NULL;
}
}
else
if (ADsIsUser(pADs)) // this is a user object
{
// add to the member list
pADs->get_Name(&bstr); // inclusive group name
CADGroupMemberProperty GP;
GP.m_ADGroupMemberName = bstr;
GP.m_ADGroupName = csGroupName;
pMemberList->AddGroupMember(&GP);
SysFreeString(bstr);
}
if (pADs)
{
pADs->Release();
pADs = NULL;
}
}
VariantClear(&var);
hr = pEnum->Next(1, &var, &lFetch);
};
Cleanup:
if (pADs) pADs->Release();
if (pUnk) pUnk->Release();
if (pEnum) pEnum->Release();
if (pGroup) pGroup->Release();
if (pMembers) pMembers->Release();
VariantClear(&var);
return hr;
}
Conclusion
That's it. OK, it's a minimalist approach. But I hope you find this information / Tool helpful. You are free to use this code in our own projects. Comments are welcome!
History
- Version 0.5
Initial release on Code Project