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. The following examples intend to demonstrate the basic to the complex.

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 here.

ConnectionProvider://Server/DistinguishedName
LDAP://somedomain.example/CN=Chris Dent,OU=Somewhere,DC=somedomain,DC=example

ADSPath using WinNT

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

ConnectionProvider://Server/Object Name
WinNT://somedomain.example/Chris

Using LDAP and the IsMember method

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

Set objADSysInfo = CreateObject("ADSystemInfo")
' strUserDN will look like CN=Chris Dent,OU=Somewhere,DC=somedomain,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://somedomain.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=somedomain,DC=example
CN=Admins,OU=Users,OU=London,DC=somedomain,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, x)
  ' 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)
    ' WScript.Echo Space(x) & objGroup.Get("name")
    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
          y = x + 2
          booIsMember = RecursiveIsMember(strGroup, arrTemp, y)
          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
' 0 is used to debug, debugging echoes the nested group structure as a tree
If RecursiveIsMember("Some Group", "", 0) = 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 & ";distinguishedName,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

Related posts:

  1. Changing the Primary Group with PowerShell Exactly as the title says, an example of how to...
  2. Listing Trusts A script to enumerate trust information from an Active Directory...

Related posts brought to you by Yet Another Related Posts Plugin.

4 Responses to this post.

  1. Posted by chris on 21.10.08 at 2:49 pm

    Thank you so much, you’ve saved my a** :)

  2. Posted by Alexis on 21.10.08 at 2:49 pm

    Very good article : for those interested in nested groups membership, I also recommend Richard Mueller’s website “Hilltop Lab” ( http://www.rlmueller.net )

  3. Posted by Christopher on 21.10.08 at 2:49 pm

    Lovely…The LDAP_MATCHING_RULE_IN_CHAIN section was exactly what I was hoping to find. Thank you very much for this post.

  4. Posted by James Bower on 21.10.08 at 2:49 pm

    Great blog, reading it through RSS feed as well

Respond to this post