Click here to Skip to main content
Click here to Skip to main content

GroupMembers

, 21 Mar 2007
Rate this:
Please Sign up or sign in to vote.
Enumerating and searching groups in Active Directory

Screenshot - RPGroupMembers.jpg

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

perle1
Systems Engineer
Germany Germany
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.

Comments and Discussions

 
GeneralDon't find Groups that are Members of the group (WinNT:\\) PinmemberStanBurton12-Apr-11 6:25 
GeneralRe: Don't find Groups that are Members of the group (WinNT:\\) Pinmemberperle113-Apr-11 11:54 
GeneralRe: Don't find Groups that are Members of the group (WinNT:\\) PinmemberStanBurton13-Apr-11 13:24 
GeneralMy vote of 5 Pinmemberkiany18-Feb-11 20:00 
GeneralNo code found Pinmemberlaurayjp20-May-08 2:13 
GeneralExcellent example Pinmemberrobert.nack10-Aug-07 5:26 
QuestionIADsADSystemInfo Object PinmemberEmad Wahib14-Jun-07 5:32 
AnswerRe: IADsADSystemInfo Object Pinmemberperle114-Jun-07 10:02 
GeneralRe: IADsADSystemInfo Object PinmemberEmad Wahib14-Jun-07 11:38 
GeneralGet Email. PinmemberSenthu16-Apr-07 12:10 
GeneralRe: Get Email. Pinmemberperle116-Apr-07 22:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 21 Mar 2007
Article Copyright 2007 by perle1
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid