group MemberOf Attribute

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.