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:
- Changing the Primary Group with PowerShell Exactly as the title says, an example of how to...
- Listing Trusts A script to enumerate trust information from an Active Directory...
Related posts brought to you by Yet Another Related Posts Plugin.
Posted by chris on 21.10.08 at 2:49 pm
Thank you so much, you’ve saved my a** :)
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 )
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.
Posted by James Bower on 21.10.08 at 2:49 pm
Great blog, reading it through RSS feed as well