Many administrative tasks and logon scripts require that you check if a user is a member
of group. If you are not concerned about membership due to nested groups, or membership
in the "Primary Group", there are several ways to check for direct membership
in a group. However, some of these methods have drawbacks you should be aware of.
The easiest method is to bind to the group object and use the IsMember method of the
group object. You pass the ADsPath of the user (or other prospective member) to the method.
IsMember returns True if the corresponding object is a direct member of the group, False
otherwise. In a logon script, if the client is Windows 2000 or above, you can retrieve the
Distinguished Name of the current user from the ADSystemInfo object and append the
"LDAP://" moniker to construct the AdsPath. For example:
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
strAdsPath = "LDAP://" & strUserDN
Set objGroup = GetObject("LDAP://TestGroup,ou=Sales,dc=MyDomain,dc=com")
If (objGroup.IsMember(strAdsPath) = True) Then
Wscript.Echo "Current user is a member of the group."
Else
Wscript.Echo "Current user is not a member of the group."
End If
Other methods use the memberOf attribute of the user object. This multi-valued attribute is a collection of the Distinguished Names of all groups the user is a direct member of (except the "Primary Group" of the user). However, any code that deals with the memberOf attribute must account for the three possible situations. The memberOf attribute may have no Distinguished Names, one Distinguished Name, or more than one. For example, the following code raises an error on the "For Each" statement if the memberOf attribute has either no Distinguished Names or only one:
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUserDN)
arrGroups = objUser.memberOf
For Each strGroup In arrGroups ' <==== Can Raise an Error
Wscript.Echo "Member of group " & strGroup
Next
If memberOf has no Distinguished Names, then arrGroups in the above example will be Empty. If memberOf has one Distinguished Name, memberOf is data type "String". Otherwise, member is data type "Variant()". Since the "For Each" statement expects an array, an error is raised unless memberOf is "Variant()". A good solution is to check for the three situations. For example:
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUserDN)
arrGroups = objUser.memberOf
If IsEmpty(arrGroups) Then
Wscript.Echo "Member of no groups"
ElseIf (TypeName(arrGroups) = "String") Then
Wscript.Echo "Member of group " & arrGroups
Else
For Each strGroup In arrGroups
Wscript.Echo "Member of group " & strGroup
Next
End If
The same errors are raised if you use the Get method of the user object to retrieve the memberOf attribute. The situation improves a bit if you use the GetEx method of the user object. However, an error is still raised when the memberOf attribute is Empty. For example:
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUserDN)
arrGroups = objUser.GetEx("memberOf") ' <==== Can Raise an Error
For Each strGroup In arrGroups
Wscript.Echo "Member of group " & strGroup
Next
The GetEx method returns an array with data type "Variant()" when memberOf has one Distinguished Name. It is an array with one element. However, the GetEx method raises an error if the memberOf attribute has no Distinguished Names. The error indicates that the Active Directory property cannot be found in the cache. If you use the GetEx method, the only solution is to trap the possible error. For example:
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUserDN)
On Error Resume Next
arrGroups = objUser.GetEx("memberOf")
If (Err.Number <> 0) Then
On Error GoTo 0
Wscript.Echo "Member of no groups"
Else
On Error GoTo 0
For Each strGroup In arrGroups
Wscript.Echo "Member of group " & strGroup
Next
End If
A method often suggested to check group membership involves converting the memberOf attribute into a string of group Distinguished Names. For example:
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUserDN)
arrGroups = objUser.memberOf
strGroups = LCase(Join(arrGroups)) ' <==== Can Raise an Error
If (InStr(strGroups, "mygroup") > 0) Then
Wscript.Echo "Member of group MyGroup"
End If
The Join function produces a string of Distinguished Names separated by spaces. However, the above will raise a "Type Mismatch" error on the Join function unless the memberOf attribute has at least two Distinguished Names. This is because the data type of arrGroups in the above example is not "Variant()" unless there are at least two Distinguished Names in the collection, and the Join function requires an array. Using the Get method of the user object yields the same results. Again, the GetEx method returns a "Variant()" if the memberOf attribute has one or more than one Distinguished Names. However, the GetEx method still raises the "Active Directory property cannot be found in the cache" error if memberOf is Empty. The only workaround is to trap the error. For example:
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUserDN)
On Error Resume Next
arrGroups = objUser.GetEx("memberOf")
If (Err.Number <> 0) Then
On Error GoTo 0
Wscript.Echo "Member of no groups"
Else
On Error GoTo 0
strGroups = LCase(Join(arrGroups))
If (InStr(strGroups, "mygroup") > 0) Then
Wscript.Echo "Member of group MyGroup"
End If
End If
If you use code similar to above, be careful how you search for group names with the InStr
function. In the example above we check if a group with Common Name "MyGroup" is
in strGroups. Remember, however, that the Common Name of a group does not have to be unique
in the domain, only in the container or OU. It is possible to have more than one group in
the domain with the same Common Name. Also, it is possible that the Common Name you search
for is a string found in the Distinguished Name of other groups. For example, if you check
for membership in a group called "Test", the InStr function will return a positive
number if the user is a member of any group in any OU's that contain the string "Test".
The best procedure is to check for the full Distinguished Name of the group.
Many examples in the forums, and even scripts online, deal with the
complications outlined here by using "On Error Resume Next" for the entire program.
I would never recommend this solution. If you use "On Error Resume Next", it should
be used for the one statement expected to possibly raise an error. Then normal error handling
should be restored with "On Error GoTo 0". This was done in the last example above.
Otherwise, even minor typographical errors can go unnoticed and are almost impossible to
troubleshoot. I feel this is even more important in logon scripts. A logon script can be run
by many users over a period of many years. If any unexpected problems arise, you want to know
about it. If you program the script to ignore all errors, you may get fewer calls for help, but
problems will be nearly impossible to recognize, much less fix.
The exact same issues described here apply to the member attribute of group objects. The same
techniques can be used with this attribute.
Another potential problem can arise with programs that reveal nested group membership. Often,
a recursive routine is used. This is a powerful technique that accommodates any level of group
nesting. For example, the following subroutine reveals nested membership in a group:
Set objMyGroup = GetObject("LDAP://cn=TestGroup,ou=Sales,dc=MyDomain,dc=com")
Call EnumMembers(objMyGroup)
Sub EnumMembers(objGroup)
' Recursive subroutine to enumerate members of a group.
For Each objMember In objGroup.Members
Wscript.Echo "Member: " & objMember.sAMAccountName _
& " (" & objMember.Class) & ")"
If (LCase(objMember.Class) = "group") Then
' Enumerate nested groups.
Call EnumMembers(objMember)
End If
Next
End Sub
Unfortunately, this program will get caught in an infinite loop if the group nesting is circular. For example, the group "TestGroup" above might have a nested group member called "School", which might in turn have a nested group member called "Grade8", which in turn could have the group "TestGroup" as a nested member. A well written program should account for this possibility. The best way to avoid the infinite loop is to keep track of the groups with a dictionary object. The subroutine is recursively called only if the group has not yet been processed by the program. For example:
' Setup dictionary object.
Set objGroupList = CreateObject("Scripting.Dictionary")
' Make group name comparisons case insensitive.
objGroupList.CompareMode = vbTextCompare
Set objMyGroup = GetObject("LDAP://cn=TestGroup,ou=Sales,dc=MyDomain,dc=com")
' Add the NetBIOS name of the group to the dictionary object.
' NetBIOS names, unlike Common Names, must be unique in the domain.
objGroupList.Add objMyGroup.sAMAccountName, True
Call EnumMembers(objMyGroup)
Sub EnumMembers(objGroup)
' Recursive subroutine to enumerate members of a group.
' The dictionary object objGroupList should have global scope.
For Each objMember In objGroup.Members
Wscript.Echo "Member: " & objMember.sAMAccountName _
& " (" & objMember.Class & ")"
If (LCase(objMember.Class) = "group") Then
' Check if this group has been encountered before.
If (objGroupList.Exists(objMember.sAMAccountName) = False) Then
' Add this group to the dictionary object, so we avoid
' an infinite loop if the group nesting is circular.
objGroupList.Add objMember.sAMAccountName, True
' Enumerate nested groups with a recursive call to this sub.
Call EnumMembers(objMember)
End If
End If
Next
End Sub
Further comments should be made about the IsMember method of group objects. The safest procedure is to bind to both the group and user objects, then use the ADsPath property method of the member object (user, computer, or group) in the IsMember method. This ensures that the objects actually exist. However, it also ensures that you have the correct ADsPath. This is not as important if you are using the LDAP provider, since the Distinguished Name is known. However, the value of the ADsPath that will be recognized by the IsMember method is not easy to determine when you use the WinNT provider (which you must if you are dealing with local objects). Also, when you bind to the objects, do not use "." to indicate the local computer. The bind operation will work, but the IsMember method never works. Finally, the ADsPath passed to the IsMember method should never include the object class, such as ",user". This is demonstrated by the following VBScript program
Option Explicit
Dim objGroup, objUser, objNetwork, strComputer, strDomain, strADsPath
Set objNetwork = CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName
strDomain = objNetwork.UserDomain
Wscript.Echo "Computer: " & strComputer
Wscript.Echo "Domain: " & strDomain
If (strComputer = strDomain) Then
strDomain = "Workgroup"
End If
Set objGroup = GetObject("WinNT://./Administrators,group")
Wscript.Echo "objGroup.ADsPath: " & objGroup.ADsPath
Set objUser = GetObject("WinNT://./Administrator,user")
Wscript.Echo "objUser.ADsPath: " & objUser.ADsPath
strADsPath = "WinNT://./Administrator,user"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
strADsPath = "objUser.ADsPath"
Wscript.Echo "Using: objUser.ADsPath: " & objGroup.IsMember(objUser.ADsPath)
strADsPath = "WinNT://" & strDomain & "/./Administrator"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
strADsPath = "WinNT://" & strDomain & "/./Administrator,user"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
Wscript.Echo "--"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators,group")
Wscript.Echo "objGroup.ADsPath: " & objGroup.ADsPath
Set objUser = GetObject("WinNT://" & strComputer & "/Administrator,user")
Wscript.Echo "objUser.ADsPath: " & objUser.ADsPath
strADsPath = "WinNT://" & strComputer & "/Administrator,user"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
strADsPath = "WinNT://" & strComputer & "/Administrator"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
strADsPath = "objUser.ADsPath"
Wscript.Echo "Using: objUser.ADsPath: " & objGroup.IsMember(objUser.ADsPath)
strADsPath = "WinNT://" & strDomain & "/" & strComputer & "/Administrator,user"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
strADsPath = "WinNT://" & strDomain & "/" & strComputer & "/Administrator"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
strADsPath = "WinNT://" & strDomain & "/" & strComputer & "/Administrator,user"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
strADsPath = "WinNT://" & strDomain & "/" & strComputer & "/Administrator"
Wscript.Echo "Using: " & strADsPath & ": " & objGroup.IsMember(strADsPath)
You can run the program above on a workstation not joined to a domain, a computer authenticated to a domain, and computer joined to a domain but not authenticated to it. The IsMember method only works in all cases when the program uses the ADsPath method of the user object, and the user and group objects are bound without using "." to represent the local computer.