Home Up Feedback

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 group nesting, 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")

For Each strGroup In arrGroups                  ' <==== Can Raise an Error

    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 to a string of group Distinguished Names. For example:

Set objSysInfo = CreateObject("ADSystemInfo")

strUserDN = objSysInfo.UserName

Set objUser = GetObject("LDAP://" & strUserDN)

arrGroups = objUser.memberOf

strGroup = 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

    strGroup = 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 strGroup. 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 newsgroups, and even scripts on Microsoft's Script Center, 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

 
Send mail to HilltopLab@RLMueller.Net with questions or comments about this web site.
Copyright © 2002-2007 Richard L. Mueller
Last modified: June 13, 2008