Active Directory, VbScript & testing group membership

When it comes to testing for group membership in Active Directory with VbScript there are a lot of different options.

WinNT vs LDAP

Not only does the structure of each group have to be considered but there are two separate providers to work with. To an extent these are inter-changeable, the examples below prefer to use LDAP. The advantages of LDAP become clear when performing more complex actions such as testing or returning nested membership.

The WinNT provider can still be useful as it is less complex, even if it does not grant access to a full set of attributes in AD.

Primary Groups

The Primary Group for an account is not listed in the memberOf attribute within AD and therefore not returned using LDAP. It is linked by the primaryGroupID attribute which matches the primaryGroupToken attribute on the group itself.

The WinNT provider on the other hand will list the Primary Group for an account using the MemberOf method.

None of the examples below explicitly check primary group membership.

ADSystemInfo

The ADSystemInfo interface is extremely useful when writing logon scripts, one of the most common reasons for checking group membership. It is documented on the MSDN area of Microsoft’s website as IADsADSystemInfo.

For the examples below the UserName or ComputerName properties are the most useful. The properties contain the distinguished names for the current user and current computer respectively.

IsMember method

Available in both the WinNT and LDAP provider the IsMember method can be called on a group object to test whether the ADSPath passed in belongs to that group.

ADSPath using LDAP

The ADSPath when using LDAP is written as follows and documented by Microsoft on MSDN.

ConnectionProvider://Server/DistinguishedName

For example:

LDAP://domain.example/CN=Chris Dent,OU=Somewhere,DC=domain,DC=example

ADSPath using WinNT

The ADSPath when using WinNT is written as follows and documented by Microsoft here.

ConnectionProvider://Server/ObjectName

For example:

WinNT://domain.example/Chris

Using LDAP and the IsMember method

strGroupDN = "CN=Domain Admins,CN=Users,DC=domain,DC=example"
Set objGroup = GetObject("LDAP://" & strGroupDN)

Set objADSysInfo = CreateObject("ADSystemInfo")
' strUserDN will look like CN=Chris Dent,OU=Somewhere,DC=domain,DC=example
strUserDN = objADSysInfo.UserName
If objGroup.IsMember("LDAP://" & strUserDN) Then
    ' The user is in the group so we can do things
End If

Using WinNT and the IsMember method

strGroupADSPath = "WinNT://domain.example/Domain Admins"
Set objGroup = GetObject(strGroupADSPath)

strUserADSPath = "WinNT://somedomain.example/Chris"
If objGroup.IsMember(strUserADSPath) Then
    ' The user is in the group so we can do things
End If

Group concatenation

Perhaps the simplest way of getting a list of groups from a user is to connect to the user and use the Join method to concatenate all of the groups into a single string.

This example makes use of GetEx with the LDAP provider, the method ensures that the list of groups is returned as an Array. If the user is only a member of a single group and Get is used the value returned would be a string which will break the script when Join is used.

The WinNT interface is a little more complex than the LDAP interface in this case.

The InStr function used to test for a group is okay, but can be a bit too accommodating. For instance, imagine these groups were returned with the LDAP interface:

CN=Domain Admins,CN=Users,DC=domain,DC=example
CN=Admins,OU=Users,OU=London,DC=domain,DC=example

In this case, if group membership of Admins were tested as follows:

If InStr(1, strGroups, "Admins", VbTextCompare) > 0 Then

It would match both Domain Admins and Admins. It is possible to work around this issue by including “CN=” and the trailing comma in the group name, effectively providing an explicit start and end to the group name.

The main advantages of this approach are speed and simplicity.

Using LDAP to retrieve groups as a string

Set objADSysInfo = CreateObject("ADSystemInfo")
Set objUser = GetObject("LDAP://" & objADSysInfo.Username)
strGroups = Join(objUser.GetEx("memberOf"))

If InStr(1, strGroups, "Domain Admins", VbTextCompare) > 0 Then
    ' The user is in the group so we can do things
End If

Using WinNT to retrieve groups as a string

Set objShell = CreateObject("WScript.Shell")
strDomain = objShell.ExpandEnvironmentStrings("%USERDOMAIN%") strUser = objShell.ExpandEnvironmentStrings("%USERNAME%") Set objUser = GetObject("WinNT://" & strDomain & "/" & strUser) For Each objGroup in objUser.Groups strGroups = strGroups & objGroup.Name Next If InStr(1, strGroups, "Domain Admins", VbTextCompare) > 0 Then ' The user is in the group so we can do things End If

Looping through MemberOf

This function starts with a group name, then checks to see if the current user belongs to the group name. In the previous methods a full Distinguished Name must be specified, or care must be taken with the group name. This function does not require a lot of care, but it is a lot of work if a lot of testing is being done.

Function IsMember(strGroup)
    ' Returns true if the user is a member of strGroup
    
    Dim strGroupDN
    Dim objUser, objGroup
    Dim booIsMember
    
    Set objADSystemInfo = CreateObject("ADSystemInfo")
    Set objUser = GetObject("LDAP://" & objADSystemInfo.UserName)
    
    On Error Resume Next
    booIsMember = False
    ' GetEx is used here. This always returns an Array for the queried
    ' attribute, even if the array only has one element.
    For Each strGroupDN In objUser.GetEx("memberOf")
        Err.Clear
        Set objGroup = GetObject("LDAP://" & strGroupDN)
        
        If Err.Number = 0 Then
            If LCase(objGroup.Get("name")) = LCase(strGroup) Then
                booIsMember = True
                Exit For
            End If
        End If
        
        Set objGroup = Nothing
    Next
    
    On Error Goto 0
    
    IsMember = booIsMember
End Function

' Example
If IsMember("Domain Admins") = True Then
     ' The user is in the group so we can do things
End If

Looping through MemberOf with recursion

This recursive function allows testing of membership in a nested chain.

A more concise version of this function is available from Microsoft in the “Hey, Scripting Guy!” posting.

This version is intended to stand on alone and drift down through nested groups for the current user with a minimal amount of input. It returns True or False depending on whether a group with a matching name was found in the chain.

Note that this can get caught in an infinite loop if it encounters circular group membership.

Function RecursiveIsMember(strGroup, arrGroups)
    ' Goes through Nested Groups until either booIsMember is True or there are
    ' no more groups to check
    
    Dim objADSystemInfo, objUser, objGroup
    Dim strGroupDN
    Dim arrTemp
    Dim booIsMember
    
    booIsMember = False
    On Error Resume Next
    If Not IsArray(arrGroups) Then
        Set objADSystemInfo = CreateObject("ADSystemInfo")
        Set objUser = GetObject("LDAP://" & objADSystemInfo.UserName)
        
        arrGroups = objUser.GetEx("memberOf")
    End If
    
    For Each strGroupDN in arrGroups
        Err.Clear
        
        Set objGroup = GetObject("LDAP://" & strGroupDN)
        If Err.Number = 0 Then
            If LCase(objGroup.Get("name")) = LCase(strGroup) Then
                booIsMember = True
                Exit For
            Else
                Err.Clear
                arrTemp = objGroup.GetEx("memberOf")
                If Err.Number = 0 Then
                    booIsMember = RecursiveIsMember(strGroup, arrTemp)
                    If booIsMember = True Then
                        Exit For
                    End If
                End If
            End If
        End If
        Set objGroup = Nothing
    Next

    On Error Goto 0

    RecursiveIsMember = booIsMember
End Function

' Calling the function:
' The group name, a blank value for the groups array and 0
If RecursiveIsMember("Some Group", "") = True Then
    ' The user is in the group so we can do things
End If

Using an LDAP Search to check "member"

This uses an LDAP search to find all the groups a user belongs to.

Function IsMember(strGroup)
    Dim objADSysInfo, objConnection, objRootDSE, objRecordSet
    Dim strUserDN, strFilter
    Dim booIsMember
    
    booIsMember = False 
    Set objADSysInfo = CreateObject("ADSystemInfo")
    strUserDN = objADSysInfo.UserName
    strFilter = "(member=" & strUserDN & ")"

    Set objConnection = CreateObject("ADODB.Connection") 
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"

    Set objRootDSE = GetObject("LDAP://RootDSE")
    Set objRecordSet = objConnection.Execute( _
      "<LDAP://" & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";name;subtree")
        
    While Not objRecordSet.EOF
        If LCase(objRecordSet.Fields("name").Value) = LCase(strGroup) Then
            booIsMember = True
        End If
        objRecordSet.MoveNext
    WEnd
    
    IsMember = booIsMember
End Function

If IsMember("Domain Admins") = True Then
    ' The user is in the group so we can do things
End If

Using LDAP_MATCHING_RULE_IN_CHAIN to check “member”

With Windows 2003 SP1 Microsoft added an Object Identifier (OID) that allows searching through nested group structures with a single LDAP search. This is a powerful option and can significantly simplify tasks involving nested membership.

All it needs is a tiny modification to the filter used in the search example above.

Function IsMember(strGroup)
    Dim objADSysInfo, objConnection, objRootDSE, objRecordSet
    Dim strUserDN, strFilter
    Dim booIsMember
    
    booIsMember = False
    Set objADSysInfo = CreateObject("ADSystemInfo")
    strUserDN = objADSysInfo.UserName
    strFilter = "(member:1.2.840.113556.1.4.1941:=" & strUserDN & ")"
    
    Set objConnection = CreateObject("ADODB.Connection") 
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
    
    Set objRootDSE = GetObject("LDAP://RootDSE")
    Set objRecordSet = objConnection.Execute( _
      "<LDAP://" & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";name;subtree")

    While Not objRecordSet.EOF
        If LCase(objRecordSet.Fields("name").Value) = LCase(strGroup) Then
            booIsMember = True
        End If
        
        objRecordSet.MoveNext
    WEnd
    
    IsMember = booIsMember
End Function

If IsMember("Domain Admins") = True Then
    ' The user is in the group so we can do things
End If

Returning all groups a user belongs to

Checking groups one by one like the examples above is hard work. It would be much better if we got a list of them once then held onto that for as long as a script needs it. This modification of the function above does just that, it returns a dictionary object containing all of the users groups. It can return all nested groups as well by making a small change to the filter.

Function GetAllGroups
    Dim objADSysInfo, objConnection, objRootDSE, objRecordSet, objGroups
    Dim strUserDN, strFilter
    
    Set objADSysInfo = CreateObject("ADSystemInfo")
    strUserDN = objADSysInfo.UserName
    
    strFilter = "(member=" & strUserDN & ")"
    ' Alternative search filter to test nested groups
    ' strFilter = "(member:1.2.840.113556.1.4.1941:=" & strUserDN & ")"
    
    Set objConnection = CreateObject("ADODB.Connection")
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
    
    Set objRootDSE = GetObject("LDAP://RootDSE")
    Set objRecordSet = objConnection.Execute( _
      "<LDAP://" & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";name;subtree")
    
    Set objGroups = CreateObject("Scripting.Dictionary")
    objGroups.CompareMode = VbTextCompare
    
    While Not objRecordSet.EOF strGroup = objRecordSet.Fields("name").Value
        If Not objGroups.Exists(strGroup) Then
            objGroups.Add UCase(strGroup), ""
        End If
        
        objRecordSet.MoveNext
    WEnd
    
    Set GetAllGroups = objGroups
End Function

Finally, the function can be used as follows.

Set objUsersGroups = GetAllGroups
' Using .Exists (case-insensitive)
If objUsersGroups.Exists("some group") Then 
    ' Do stuff
End If

' Using a loop and select case (case-sensitive)
For Each strGroup in objUsersGroups
    Select Case strGroup
        Case "DOMAIN ADMINS"
            ' Do stuff
        Case "LONDON USERS"
            ' Do stuff
    End Select
Next