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" };
if (NULL == pszSearchBase || NULL == pszFilter ||
NULL == arIADsList)
{
return (E_INVALIDARG);
}
hr = ADsOpenObject(pszSearchBase, NULL, NULL,
ADS_SECURE_AUTHENTICATION, IID_IDirectorySearch,
(void**) &pDSSearch);
if (FAILED(hr))
return hr;
arSearchPrefs[0].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
arSearchPrefs[0].vValue.dwType =
ADSTYPE_INTEGER;
arSearchPrefs[0].vValue.Integer = 100;
arSearchPrefs[1].dwSearchPref =
ADS_SEARCHPREF_SEARCH_SCOPE;
arSearchPrefs[1].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs[1].vValue.Integer =
ADS_SCOPE_SUBTREE;
arSearchPrefs[2].dwSearchPref =
ADS_SEARCHPREF_TIME_LIMIT;
arSearchPrefs[2].vValue.dwType =
ADSTYPE_INTEGER;arSearchPrefs[2].vValue.Integer = 120;
configurationhr = pDSSearch->SetSearchPreference(arSearchPrefs,3);
if (FAILED(hr))
{
if (pDSSearch)pDSSearch->Release();
return (hr);
}
hr = pDSSearch->ExecuteSearch(pszFilter,pszAttribute, (UINT) 1,
&hSearch);
if (SUCCEEDED(hr))
{
while (SUCCEEDED(hr =pDSSearch->GetNextRow(hSearch)))
{
if (S_OK == hr)
{
hr = pDSSearch->GetColumn(hSearch, pszAttribute[0],
&col);
if (SUCCEEDED(hr))
{
wcsncpy(pszADsPath, col.pADsValues->CaseIgnoreString,
MAX_PATH);
pszADsPath[MAX_PATH - 1] = 0;
hr = ADsOpenObject(pszADsPath, NULL, NULL,
ADS_SECURE_AUTHENTICATION,IID_IADs,(void**)&ppObj);
if (SUCCEEDED(hr))
arIADsList->Add(ppObj);
pDSSearch->FreeColumn(&col);
}
}
else
break;
}
pDSSearch->CloseSearchHandle(hSearch);
}
if (pDSSearch)
pDSSearch->Release();
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);
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))
{
pADs->get_Name(&bstr); 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
Initial release on Code Project
Ralph started programming in Turbo Pascal, later in Delphi. After this, he began learning C++, which is his favorite language up to now.
He is interested in almost everything that has to do with computing, his special interests are security and networking.