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
- Download the VBScript version in a ZIP file so that it doesn’t execute or worry your virus protection. Don’t forget to edit the script to change the username and domain or machine name.
- Download the ASP version. Again in a zip, also you’ll need to have IIS configured if you want to use this version (and you’ll have to edit the file to change the username etc.).
- Download the C++ test program source.
- Instructions on using the (free) Borland C++ compiler to compile the C++ test program