Discussion:
Cleaning up nested groups in Active directory
(too old to reply)
Stefan van der Pur
2007-03-22 15:39:49 UTC
Permalink
Hi,

I currently work at a big Dutch organisation and we have the following
problem:
We at system engineering create our groups in AD, however at the
servicedesk people can add members to groups we create. As long as
they add users it is no problem, but they also add groups to groups
(aka nesting). At first we did not notice this, but now we have people
getting rights they should not have, because they are members of the
group. We know that de servicedesk made those mistakes, but it seems
to be our problem for cleaning up their mess. We have over 10.000
groups. I need a script that does the following:

1. It should query AD and give only the groups containing groups as a
result

eg. GroupA contains members GroupB and GroupC

2. It should read the membership of GroupB and GroupC and add the
members of GroupB and GroupC to GroupA and then it should remove
GroupB and GroupC as members of GroupA

3. It should process all groups in 1 OU.

This way we remove all of our groupnesting. In our environment nested
groups are not allowed. I need the script to be reusable in case it
happens again.

I currently have a bit of the script that displays all groups
containing groups as members, but it still needs to filtered better:

Option Explicit

Dim adoConnection, adoCommand, objRootDSE, strDNSDomain, strQuery
Dim adoRecordset, strDN, objGroup

Set adoConnection = CreateObject("ADODB.Connection")
Set adoCommand = CreateObject("ADODB.Command")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
Set adoCommand.ActiveConnection = adoConnection

Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

strQuery = "<LDAP://ou=Groups, ou=South, dc=test, dc=Local>;
(objectClass=group);distinguishedName;subtree"
adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False

Set adoRecordset = adoCommand.Execute
If (adoRecordset.EOF = True) Then
Wscript.Echo "No groups found"
adoRecordset.Close
adoConnection.Close
Set objRootDSE = Nothing
Set adoConnection = Nothing
Set adoCommand = Nothing
Set adoRecordset = Nothing
Wscript.Quit
End If

Do Until adoRecordset.EOF
strDN = adoRecordset.Fields("distinguishedName")
Set objGroup = GetObject("LDAP://" & strDN)
Wscript.Echo objGroup.sAMAccountName '& " (" &
GetType(objGroup.groupType) & ")"
Call GetMembers(objGroup)
adoRecordset.MoveNext
Loop
adoRecordset.Close

adoConnection.Close
Set objRootDSE = Nothing
Set objGroup = Nothing
Set adoConnection = Nothing
Set adoCommand = Nothing
Set adoRecordset = Nothing

Function GetType(intType)
If ((intType And &h01) <> 0) Then
GetType = "Built-in"
ElseIf ((intType And &h02) <> 0) Then
GetType = "Global"
ElseIf ((intType And &h04) <> 0) Then
GetType = "Local"
ElseIf ((intType And &h08) <> 0) Then
GetType = "Universal"
End If
If ((intType And &h80000000) <> 0) Then
GetType = GetType & "/Security"
Else
GetType = GetType & "/Distribution"
End If
End Function

Sub GetMembers(objADObject)
Dim objMember, strType
For Each objMember In objADObject.Members
If (UCase(Left(objMember.objectCategory, 8)) = "CN=GROUP")
Then
strType = "Group"
WScript.Echo " Geneste Groep: " &
objMember.sAMAccountName '& " (" & strType & ")"
End If
Next
Set objMember = Nothing
End Sub



Any input Is very welcome. I am not much of a scripter myself.
Richard Mueller [MVP]
2007-03-22 19:10:48 UTC
Permalink
Interesting problem. First, I don't see any way to query directly for groups
that have members that are groups. I see no better method than binding to
each group, then binding to each member in the group to check if the member
is a group, etc. This is a lot of binding, but seems unavoidable. With so
many groups, the task can take awhile.

Next, group nesting can be tricky. For example if I have this situation:

Group Members
------- ---------
Parish School (group)
BFranklin (user)

School TestUser (user)
Grade8 (group)

Grade8 JSmith (user)

If I consider the groups in this order and replace each member that is a
group by it's direct user members, the result is:

Group Members
------- ---------
Parish TestUser (user)
BFranklin (user)

School TestUser (user)
JSmith (user)

Grade8 JSmith (user)

However, this is not equivalent. In the original situation, user JSmith has
all permissions assigned to group Parish (due to nesting). This is not
reflected in the second situation. The program needs to work through the
group nesting to fix this. The result should be:

Group Members
------- ---------
Parish TestUser (user)
JSmith (user)
BFranklin (user)

School TestUser (user)
JSmith (user)

Grade8 JSmith (user)

However, if in the original situation we made group Parish a member of
Grade8, we would have circular nested groups. This can happen (especially if
people are adding groups to groups willy nilly), but can cause an infinite
loop if you don't account for it.

My solution, which worked in my testing, follows. I have commented out the
statements that actually modify group membership, so the script can be run
to see what it will do. In my case (admittedly convoluted to demonstrate the
complications), the script made some users members of groups repeatedly.
However, if the script is run with the Add and Remove statements this will
not happen, because the script first checks if a user is already a member
before adding them to the group.

Finally, you should only run this on selected OU's, as you suggest.
Otherwise, you might remove "Domain Users" from group "Users", and remove
"Domain Guests" from "Guests". Also, this program is blind to "primary"
group membership. If someone has made "Domain Users" a member of a group,
this will be removed, but the members of "Domain Users" will not be added to
the group. You may want to document what happens when this runs so you can
track possible problems that arise. Watch for line wrapping.
===================
Option Explicit

Dim adoCommand, adoConnection
Dim strBase, strFilter, strAttributes, strQuery, adoRecordset
Dim strDN, objGroup

' Use ADO to search Active Directory.
Set adoCommand = CreateObject("ADODB.Command")
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = adoConnection

' Search selected OU.
strBase = "<LDAP://ou=Groups,ou=South,dc=test,dc=Local>"

' Search for all groups in selected OU.
strFilter = "(objectCategory=group)"

' Comma delimited list of attribute values to retrieve.
strAttributes = "distinguishedName"

' Construct the LDAP query.
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"

' Run the query.
adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False
Set adoRecordset = adoCommand.Execute

' Enumerate the resulting recordset.
Do Until adoRecordset.EOF
' Retrieve values.
strDN = adoRecordset.Fields("distinguishedName").Value
' Bind to the group.
Set objGroup = GetObject("LDAP://" & strDN)
' Evaluate membership.
Call RemoveNestedGroups(objGroup)
adoRecordset.MoveNext
Loop

' Clean up.
adoRecordset.Close
adoConnection.Close

Function GetClass(objMember)
' Function to determine class of object.
Dim strClass

strClass = UCase(Left(objMember.objectCategory, 9))
Select Case strClass
Case "CN=PERSON"
GetClass = "PERSON"
Case "CN=GROUP,"
GetClass = "GROUP"
Case Else
GetClass = "OTHER"
End Select
End Function

Sub RemoveNestedGroups(objGroup)
' Subroutine to evaluate members of a group
' and remove nested groups.

Dim objMember, strClass

' Enumerate direct members.
For Each objMember In objGroup.Members
' Check if any direct members are nested groups.
strClass = GetClass(objMember)
If (strClass = "GROUP") Then
' Enumerate members of this nested group and make
' users members of the parent group.
Call GetMembers(objMember, objGroup)
' Remove nested group.
Wscript.Echo "Remove group " & objMember.sAMAccountName _
& " from group " & objGroup.sAMAccountName
' objGroup.Remove(objMember.AdsPath)
End If
Next

End Sub

Sub GetMembers(objGroup, objParent)
' Subroutine to determine nested user members of a group and make
' them members of the parent group.
Dim objNestedMember, strNestedClass

' Exit if circular nested group detected.
If (objGroup.sAMAccountName = objParent.sAMAccountName) Then
Wscript.Echo "Circular nested groups"
Exit Sub
End If

For Each objNestedMember In objGroup.Members
' Check if any members are users.
strNestedClass = GetClass(objNestedMember)
If (strNestedClass = "PERSON") Then
If (objParent.IsMember(objNestedMember.AdsPath) = False) Then
Wscript.Echo "Add User " & objNestedMember.sAMAccountName _
& " to group " & objParent.sAMAccountName
' objParent.Add(objNestedMember.AdsPath)
End If
End If
' Check if any members are groups.
If (strNestedClass = "GROUP") Then
' Enumerate nested members of this group and make them
' members of the parent group.
Wscript.Echo "Call GetMembers(" & objNestedMember.sAMAccountName
_
& ", " & objParent.sAMAccountName & ")"
Call GetMembers(objNestedMember, objParent)
End If
Next
End Sub
--
Richard Mueller
Microsoft MVP Scripting and ADSI
Hilltop Lab - http://www.rlmueller.net
--
Post by Stefan van der Pur
Hi,
I currently work at a big Dutch organisation and we have the following
We at system engineering create our groups in AD, however at the
servicedesk people can add members to groups we create. As long as
they add users it is no problem, but they also add groups to groups
(aka nesting). At first we did not notice this, but now we have people
getting rights they should not have, because they are members of the
group. We know that de servicedesk made those mistakes, but it seems
to be our problem for cleaning up their mess. We have over 10.000
1. It should query AD and give only the groups containing groups as a
result
eg. GroupA contains members GroupB and GroupC
2. It should read the membership of GroupB and GroupC and add the
members of GroupB and GroupC to GroupA and then it should remove
GroupB and GroupC as members of GroupA
3. It should process all groups in 1 OU.
This way we remove all of our groupnesting. In our environment nested
groups are not allowed. I need the script to be reusable in case it
happens again.
I currently have a bit of the script that displays all groups
Option Explicit
Dim adoConnection, adoCommand, objRootDSE, strDNSDomain, strQuery
Dim adoRecordset, strDN, objGroup
Set adoConnection = CreateObject("ADODB.Connection")
Set adoCommand = CreateObject("ADODB.Command")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
Set adoCommand.ActiveConnection = adoConnection
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")
strQuery = "<LDAP://ou=Groups, ou=South, dc=test, dc=Local>;
(objectClass=group);distinguishedName;subtree"
adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False
Set adoRecordset = adoCommand.Execute
If (adoRecordset.EOF = True) Then
Wscript.Echo "No groups found"
adoRecordset.Close
adoConnection.Close
Set objRootDSE = Nothing
Set adoConnection = Nothing
Set adoCommand = Nothing
Set adoRecordset = Nothing
Wscript.Quit
End If
Do Until adoRecordset.EOF
strDN = adoRecordset.Fields("distinguishedName")
Set objGroup = GetObject("LDAP://" & strDN)
Wscript.Echo objGroup.sAMAccountName '& " (" &
GetType(objGroup.groupType) & ")"
Call GetMembers(objGroup)
adoRecordset.MoveNext
Loop
adoRecordset.Close
adoConnection.Close
Set objRootDSE = Nothing
Set objGroup = Nothing
Set adoConnection = Nothing
Set adoCommand = Nothing
Set adoRecordset = Nothing
Function GetType(intType)
If ((intType And &h01) <> 0) Then
GetType = "Built-in"
ElseIf ((intType And &h02) <> 0) Then
GetType = "Global"
ElseIf ((intType And &h04) <> 0) Then
GetType = "Local"
ElseIf ((intType And &h08) <> 0) Then
GetType = "Universal"
End If
If ((intType And &h80000000) <> 0) Then
GetType = GetType & "/Security"
Else
GetType = GetType & "/Distribution"
End If
End Function
Sub GetMembers(objADObject)
Dim objMember, strType
For Each objMember In objADObject.Members
If (UCase(Left(objMember.objectCategory, 8)) = "CN=GROUP")
Then
strType = "Group"
WScript.Echo " Geneste Groep: " &
objMember.sAMAccountName '& " (" & strType & ")"
End If
Next
Set objMember = Nothing
End Sub
Any input Is very welcome. I am not much of a scripter myself.
Stefan van der Pur
2007-03-22 22:12:49 UTC
Permalink
Thank you so much for you input, I will test this script as soon as I
can. I have very little experience in scripting. Also thanks for the
explanation in the script so I can look at what eacht step does, very
usefull!

Maybe this is a good script for the next resource kit ;)

Thanks a bunch!

I will let you know if this is working for our situation!


Stefan van der Put

Loading...