Accessing Microsoft’s ADSI–part 1

The overall aim of this project was to be able to get the groups that an NT user belongs to using ADSI (Active Directory Services Interface) and make this available in Java. This first article describes how to get ADSI information using VBScript, JScript and then C++. It shows how the different languages make it easier or harder. In the second article, we show how to wrap up the C++ function in a DLL that’s accessible using JNI.

Accessing ADSI from VBScript and JScript

ADSI is a useful technology that provides easy access to users, groups, domains, etc. For more information on ADSI, have a look at the MSDN site. ADSI is available on Windows 2000 as well as Windows XP and doesn’t require domains or Active Directory. You can test out whether this will work on your machines by creating this small VBScript and then simply double clicking on it:

rem listGroups.vbs
rem change the text below to have your computer and username
set user=GetObject("WinNT://DOLPHIN/denish")
for each group in user.groups
  msgbox group.name
next

The key is the GetObject
function. This allows you to bind to directory service objects without you needing to provide any extra authentication. GetObject can be used to connect to ADSI (by specifying the WinNT: protocol) or LDAP (using LDAP:). GetObject is available to any language that supports Automation, so that includes VBScript, VB, JScript, C#, Delphi and other .NET languages. The sort of object that you get back will depend on what you query. You can query for users, groups, machines etc. In our case we want to find out which
groups our user is in. Since when we were testing we didn’t have a domain, we used the machine name followed by the username. More normally, you would use the domain name followed by the username.

Given that JScript supports automation, it should be just as easy to write the code in JScript. However, VBScript is able to simply iterate through the groups using for each. In JScript we have to create an Enumerator first. We can initialise the Enumerator in the constructor like this: var e = new Enumerator(user.Groups());. There are then methods such as atEnd() and moveNext() that allow you to move through the enumeration.

Here is an ASP page that you can test using IIS which does slightly more than the script above (we used ASP because it was much easier to find bugs and output more complex results during development):

<%@ language = JScript %>
<h1>User information</h1>
<%
var user=GetObject("WinNT://DOLPHIN/denish");
if (user != null) {
%>
<table>
<tr><td>Class:</td><td><%=user.Class%></td></tr>
<tr><td>Name:</td><td><%=user.Name%></td></tr>
<tr><td>Full Name:</td><td><%=user.FullName%></td></tr>
<tr><td>Description:</td><td><%=user.Description%></td></tr>
<tr><td>Last Login:</td><td><%=user.LastLogin%></td></tr>
<tr><td>Password Expiration date:</td><td><%=user.PasswordExpirationDate%></td></tr>
<tr><td>Account Disabled:</td><td><%=user.AccountDisabled%></td></tr>
<tr><td valign="top">Groups:</td><td>
<%
    for (var e = new Enumerator(user.Groups()); ! e.atEnd(); e.moveNext())
    {
        var oGroup = e.item();
        Response.write(oGroup.Name+"<br>n");
    }
%></td></tr>
</table>
<%
} else {
    Response.write("User is null");
}
%>
</body>
</html>

Writing an ADSI test program in C++

GetObject is only available to languages supporting automation. Since we want to use JNI eventually, we’re really tied to using C++, and a fairly basic version of that so that the DLL is nice and portable with as few externak dependencies as possible. So, for C++ we have to use ADsGetObject.
This function is quite a bit more “basic” than GetObject. It takes the query string (which must be an array of WideChars, so we have to convert our string to WideChars first), then the ID of the type interface that we want back (in this case IID_IADsUser) then the address of a pointer to the object we want to get back as it will allocate the space and update our pointer. Finally it returns an HRESULT which we can examine to see if the call has been successful or not.

Since we want to eventually wrap this as a JNI DLL, we’re going to write this code to be more general than our examples above, so we won’t hard-code in the username, instead we’ll pass it in as a parameter to a function
void getGroupsFor(const char *user). This will then prepend the “WinNT://”, convert the string to a wide string, call ADsGetObject, check the result and assuming everything’s OK, get the groups and (for now) simply print them out to stdout. The function is shown below:

void getGroupsFor(const char *user)
{
    // First we have to make up the string that we're going to pass to ADsGetObject
    char adsPath[80];
    strcat(adsPath, "WinNT://");
    strcat(adsPath, user);
    fprintf(stdout, "adsPath is '%s'n", adsPath);

    // Call CoInitialize to initialise COM
    fprintf(stdout, "Starting up, calling CoInitialisen");
    CoInitialize(NULL);

    // Convert the adsPath to wide chars so that we can pass it to ADsGetObject
    int nLen = strlen(adsPath) + 1;
    WCHAR* widePath = new WCHAR [nLen];
    mbstowcs(widePath, adsPath, nLen);

    // Now set up the required variables and call ADSGetObject
    fprintf(stdout, "Now calling ADsGetObjectn");
    IADsUser *pUser;
    HRESULT hr = ADsGetObject(widePath,
                IID_IADsUser,
                (void**) &pUser );

    // If ADsGetObject can't return the user, we can stop now
    if(FAILED(hr)) {
        fprintf(stderr, "Got an error getting the user: %x", hr);
        return;
    }

    // Now let's get the groups
    IADsMembers *pGroups;
    hr = pUser->Groups(&pGroups);
    if(FAILED(hr)) {
        fprintf(stderr, "Got an error getting the groups: %x", hr);
        return;
    }
    // Don't need the user any more
    pUser->Release();

    // In order to iterate through the groups we need to get an enumerator
    // We then don't need the groups object itself any more so we can release it
    IUnknown *pUnk;
    hr = pGroups->get__NewEnum(&pUnk);
    if (FAILED(hr)) {
        fprintf(stderr, "Got an error getting the enumeration: %x", hr);
        return;
    }
    pGroups->Release();

    // From IUnknown interface, we can query to get the EnumVARIANT interface
    IEnumVARIANT *pEnum;
    hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
    if (FAILED(hr)) {
        fprintf(stderr, "Got an error getting the enumeration variant: %x", hr);
        return;
    }
    // Once we've got the interface we want, we can release the original one
    pUnk->Release();

    // Finally we can go round and enumerate the groups
    BSTR bstr;
    VARIANT var;
    IADs *pADs;
    ULONG lFetch;
    IDispatch *pDisp;

    // Initialise the variant
    VariantInit(&var);
    // Go and get the first group, hr will be S_OK if we get one
    hr = pEnum->Next(1, &var, &lFetch);
    while(hr == S_OK)
    {
        if (lFetch == 1)
        {
            // Now we need to get the IADs interface, so we use QueryInterface again
            pDisp = V_DISPATCH(&var);
            pDisp->QueryInterface(IID_IADs, (void**)&pADs);
            // Get the name of the group so that we can display it
            pADs->get_Name(&bstr);
            fprintf(stdout, "Group: %Sn",bstr);
            // Release the string because we don't need it any more
            SysFreeString(bstr);
            pADs->Release();
        }
        VariantClear(&var);
        // Now go and get the next one in the group
        hr = pEnum->Next(1, &var, &lFetch);
    };
    // We don't need the enumerator any more
    pEnum->Release();

    // We're done, so uninitialise the COM interface
    CoUninitialize();
}

The function starts off ordinarily enough by concatenating the string and then widening it then calling the ADsGetObject, from there, we can easily get the groups. Unfortunately, as was the case in JScript, once we’ve got the groups, we need to get an enumerator. We can do this by calling the romantically named
get__NewEnum(&pUnk)
(in fact this is method being called implicitly in both JScript and VBScript). Unfortunately, get__NewEnum only takes the address of an IUnknown pointer (the most general of interface types), so we then have to use QueryInterface
to get the interface we want.

QueryInterface is the basic workhorse of COM and is equivalent to dynamic casting in other languages (and in fact is how languages that support automation actually do the dynamic casting). So, using QueryInterface we can as our IUnknown pointer for an IEnumVARIANT so that we can iterate through the groups.

Our enumerator has a Next method which allows us to get one or more members at a time, but since we don’t know how many there are, it’s easier to just get one at a time. The Next method also takes the address of a VARIANT. So, once we’ve got that, we need to call V_DISPATCH to get the IDispatch interface. Now we can use QueryInterface again to get the IADs interface. This is the most general of the ADSI interfaces and it has the basic get_Name that we need in order to be able to print out the name of the group.

Now that we have written the function, it’s easy to call this from a main() so that we can test it out. The complete program is downloadable here. For more information, see the
resources section at the end of this article.

If you now run the program you should get something like the following:

c:cvsutility projectsjava_adsisrccpp>getusergroups.exe DOLPHIN/denish
adsPath is 'WinNT://DOLPHIN/denish'
Starting up, calling CoInitialise
Now calling ADsGetObject
Group: Administrators
Group: Users

One disappointing observation is that there’s a noticable pause between printing out “Now calling ADsGetObject” and printing out the group names. Further investigation shows it is the actual call to ADsGetObject which takes the time. Taking 2 seconds each time we want to verify which groups a user is in probably isn’t going to be acceptable, on the other hand, caching the groups isn’t a good idea since we’d need to know when they were changed. Still, resolving this issue is outside the scope of this article. In part 2 we’ll modify the function so as to be called from Java and wrap the whole thing in a JNI DLL.

Resources

One thought on “Accessing Microsoft’s ADSI–part 1”

Leave a Reply

Your email address will not be published. Required fields are marked *