Active Directory Shadow Group Script – will let you spend less time on updating group memberships

Introduction
If you are just looking for a free shadow group script, either click here for a nice simple one or go to the bottom of this post for the full AD administrated script.

I was looking into Shadow Groups, inspired by a customer migrating from Novell to Active Directory. Apparently in Novell you can use Organizational Units as security groups, and by just moving a user to another OU when they change departments, they will automatically update their security permissions given by their department OU placement.

So what is so great about shadow groups you might ask. Simply put if you have OU’s for departments, where you place users depending on department membership, shadow groups, will shadow the members of the OU in the security group, I assume that is where the name shadow group comes from. This allows you to setup security permissions for a group that is linked to an organizational unit. So when you move user A from department sales, into department accounting, the user A will automatically be removed from the sales security group and added to the accounting security group, effectively updating user A’s permissions automatically. Saves time for large organizations, now a user moving OU does not need to have his groups manually updated.

The first hit on google was a blog post by John Policelli (MVP) explaining shadow groups is not a new type of group in Active Directory, it is rather a concept, when you automatically update the members of a security group from the objects placed in an Organizational Unit. Also he points out that this automatic synchronization is not an existing feature in Windows Server, we need to add it our self. The example he uses with dsquery, dsget and dsmod, works if you manually set it in a script for each OU/Group, I was looking for something easier to manage, that preferably did not require editing of the script that needed to run. I strongly believe scripts that can be maintained from Active Directory will always have a longer life time, since less updates and potential errors happen in the script.

After some more searching I found an article by Jakob H. Heidelberg (MVP and fellow Dane) this one also had a good explanation about what Shadow Groups are and also a download link to a simple VBScript to populate a group with the users in an Organizational Unit. If You are looking for a script to feed the OU and Group and then update the group from the users of the OU, that script will do you just fine and I would recommend you take a look at the article and script he wrote, as it is simpler and less prone to errors by being simple.

My idea of a Shadow Group Script

Using this as inspiration I sat down and wrote a couple of lines about what I would like from the ideal Shadow Group script, what I got was something like this:

  • After initial setup of script and scheduling, no editing should be necesary to add/edit/remove shadow groups.
  • Create shadow groups automatically from information in a organizational unit
  • Update multiple shadow groups with information from one organizational unit (i.e. one with computers, one with users, one with all ou and sub ou’s users, etc.)
  • High level of customization of shadow group names and content
  • Logging of all editing by script to logfiles
  • Security safe with the ability to avoid a user to change security groups where user should not be able to

From that I have created a VBScript that can be scheduled to run, preferably in the security context of a service account, with only permissions to create and edit groups within the area of Active Directory where deemed safe. The script is free for anyone to use, edit, improve, under the creative commons license, so basicly all I ask is that credit is given if used or changes are made.

Please feel free to comment how you used the script, any improvements or additions you make are also welcomed. Find a bug? Please let me know. Having trouble getting it to work? Feel free to write a comment with your questions.

How to setup and install the script.

  • Create a service account with only the permissions you trust the script with. Preferably only able to create and edit security groups within the OU’s needed.
  • Edit the sections of the script with “EDIT THIS”.
  • On a server with access to a domain controller, create a sheduled task to run every x minutes, i.e. 15 minutes, that runs “cscript.exe shadowgroups.vbs”.

How to use the script to create shadow groups.

Option #1 – Shadow group created from Organizational Unit

  • Edit the description field of the Organizational Unit with the parameters to create and maintain a connected shadow group

Option #2 – Shadow group connected to an Organizational Unit

  • Create a Security Group – dont forget any required word in the group name i.e. “SHADOWGROUP”/”SG” etc.
  • Edit the description field of the security group with the parameters to update its members from an Organizational Unit

This allows multiple groups to update from the same target.

Parameters

The parameters to use in the description field of a security group / Organizational Unit, must be written comma seperated in the following format:
ValueName=Value,ValueName2=Value2,etc.

ValueName = Values – Description

Scope = OneLevel – Default value, only gets members from the target OU
Scope = SubTree – Gets members from targeted OU and all sub OU’s

OU = OUa/OUb/OUc – The OU path to the target OU, the example before is from a full path like this LDAP://ou=OUa,ou=OUb,ou=OUc,dc=my,dc=domain,dc=local
Notice only “real” OU’s can be used, no builtin OU’s, also do not add the domain.

GroupOU = OUa/OUb/OUc – The path to the OU where the shadow groups is to be placed, same format as OU.

GroupName = SHADOWGROUP Department Sales – The full custom name of the shadow group (if not set defaults to standard prefix + OU name).

GroupPrefix = SHADOWGROUP Department – The prefix to use for the group name + the source OU name. Usefull for using the same description for multiple sub OU’s.

OBJ = CUG – A combination of letters designating what objects to include in the shadow group. C = Computers, U = Users, G = Groups.

# = Text – Text written here is ignored, usefull for descriptions for humans and other uses.

Required parameters and defaults

It is not required to use all the parameters each time you setup a shadow group, but you must start the description field with SHADOWGROUP.

The following defaults will be used unless specifically set to something else when using an OU as source:

  • Source OU will be the OU with the description set to SHADOWGROUP
  • Group name will be standard prefix (from script config) + OU name.
  • Group location will be within the source OU.
  • Scope will be OneLevel.
  • Objects will be users only.

When using a group as the shadow group initiator, you must set source OU, other than that the following defaults will be used:

  • Source OU must be set manually or group processing will be aborted.
  • Group name will be source group.
  • Group location will be source group location.
  • Scope will be OneLevel
  • Objects will be users only.

The actual script

Download the full Active Directory Shadow Group Script from here [shadowgroups.vbs.txt]. Also sourcecode visible here (sorry the blog automatically changes some stuff including links).

'*************************************************************************************************
' Name: Active Directory Shadow Groups Script
' File: ShadowGroups.vbs
' Description: Creates and maintains security groups based on OU members
'
' Created by Sole Viktor - sole@sole.dk - www.sole.dk
' http://creativecommons.org/licenses/by-sa/3.0/
'
' On Error Resume Next ' uncomment this to see errors that werent supposed to happen
Dim LogFileName
'
' Set constants - EDIT THIS!
'
Const str_Domain = "dc=my,dc=domain,dc=local"
Const str_BaseOU = "ou=Departments," ' limit where to look for ShadowGroup OU/Groups
Const str_DefaultGroupPrefix = "Prefix SHADOWGROUP" ' default prefix to use for shadow groups
Const str_Default_ObjectCategory = "(objectCategory=user)" ' default objects to include in a shadow group
Const str_Default_Scope = "OneLevel" ' OneLevel current ou, SubTree recursive ou search
Const const_DefaultGroupDescription = "Autogenerated shadowgroup - do not edit members manually" ' default description for auto created shadow groups
LogFileName = "\\fileserver.my.domain.local\logshare\logfolder\logfile-" & Date & ".log" ' daily log file
Const boolDebug = False ' Logs all OU and Group members, increases run time considerably
'
' SECURITY SETTINGS - EDIT THIS! (ensures script can not do anything bad)
'
' Impportant if user running script has access to more than shadowgroups, ensure you set below to restrict access!
'

' Enter word that must be present in shadowgroup name
Const sec_GroupName_MustContain = "SHADOWGROUP" ' avoids pointing a shadowgroup destination to an existing security group, thereby controlling its members, i.e. Domain Admins

' Only allow shadowgroup destination in or below source OU
Const sec_GroupOU_equal_targetOU = True

''
'' Aditional information
''
'' To use this script, run it with a scheduled process,
'' it will use Description fields in OU and Group objects to automatically create Shadow Groups
'' Be carefull to avoid duplicate group names, i.e. in auto named groups, with sub OU's having same names
''
'' Notice hardcoded limit on 1000 objects (dictionary and ldap searches for shadowgroup OU/Group)
'' Notice do not run this script in a loop, it is likely to memory leak. Restart the script instead.
'' Notice special characters used in Active Directory objects, may interfere with the scripts ability to successfully run.
''
'' Shadow Groups can be created by adding parameters to the description field of an existing security group
'' or by using the description field of an OU. The description field must begin with SHADOWGROUP,
'' additional parameters may be given comma seperated after the SHADOWGROUP word, with name=value.
'' Parameter = Values
''
'' "Scope" = "OneLevel" (default), only gets members of the targeted OU
''           "SubTree" gets targeted OU and all sub OU's members
''
'' "OU" = "OU/OU/OU" the OU Path to target, a full dn of LDAP://ou=A,ou=B,dc=mydomain,dc=local,
'' must be written as "A/B", notice only real OU's may be in path, no builtin OU's, also no domain.
''
'' "GroupOU" = "OU/OU/OU" the path to the OU where the shadow group is to be placed, same format as "OU"
''
'' "GroupName" = "SHADOWGROUP Department Sales" the full custom name for the shadow group
''
'' "GroupPrefix" = "SHADOWGROUP Department" group prefix, will be appended with the source OU name
''
'' "OBJ" = "CUG" A combination of letters designating what objects to include in the Shadow Group
'' C = Computers, U = Users, G = Groups
''
'' "#" = "Description text" this parameter is ignored, but can be used to add description text to the OU/Group for other uses.
''
'' SCRIPT START - DO NOT EDIT BELOW THIS LINE ----------------------------------------------------
'' -----------------------------------------------------------------------------------------------

' Script constants, dont edit this
Const ScriptVersion = "0.2"
Const ADS_GROUP_TYPE_GLOBAL_GROUP = &h2
Const ADS_GROUP_TYPE_SECURITY_ENABLED = &h80000000
Const ADS_PROPERTY_APPEND = 3
Const ADS_PROPERTY_DELETE = 4

' setting time of script start for logging purposes
Dim startTime, endTime
startTime = Timer

' Set vars used for counting what changes where made and logging
Dim count_GroupCreated, count_GroupProcessed, count_GroupAborted, count_MemberAdded, count_MemberRemoved, str_LogText
count_GroupCreated = 0
count_GroupProcessed = 0
count_GroupAborted = 0
count_MemberAdded = 0
count_MemberRemoved = 0

WriteLog "Script started - version " & ScriptVersion & " - " & Date & " " & Time

'Setting global dictionaries for ou and group objects
Dim dictOUObjects, dictGroupObjects
Set dictOUObjects = CreateObject("Scripting.Dictionary")
dictOUObjects.CompareMode = TextMode
Set dictGroupObjects = CreateObject("Scripting.Dictionary")
dictGroupObjects.CompareMode = TextMode

' Find shadow groups to create/update
' Find OU's with shadow group enabled
Start_Update_OU Find_OU_ShadowGroup()
WriteLog "Done getting ShadowGroup OU's"

' Find Group's that are shadow group enabled
Start_Update_Group Find_Group_ShadowGroup()
WriteLog "Done getting ShadowGroup Groups"

'' Script finished
WriteLog ""
WriteLog "Groups processed:          " & vbTab & count_GroupProcessed
WriteLog "Groups created:            " & vbTab & count_GroupCreated
WriteLog "Groups aborted:            " & vbTab & count_GroupAborted
WriteLog "Members added to group:    " & vbTab & count_MemberAdded
WriteLog "Members removed from group:" & vbTab & count_MemberRemoved

WriteLog "Script finished - version " & ScriptVersion & " - " & Date & " " & Time
WriteLogFile
WScript.Quit

''' MAIN FUNCTIONS

Function Start_Do_Parameters (Parameters, OUName, OUPath, GroupName, GroupCN)
    Dim arr_Parameters, str_Parameters, str_Scope, str_ObjectCategory
    str_ObjectCategory = str_Default_ObjectCategory
    str_Scope = str_Default_Scope
    str_GroupPrefix = str_DefaultGroupPrefix
    ' Go thru parameters
    arr_Parameters = Split(Parameters,",")
    For Each str_Parameter In arr_Parameters
        arr_tmp_parm = Split(str_Parameter, "=")
        Select Case UCase(arr_tmp_parm(0))
            Case "SHADOWGROUP"
            'Ignore
            Case "#"
            'Ignore
            Case "SCOPE"
            ' Check if we should search subtree of ou
            If UCase(arr_tmp_parm(1)) = "SUBTREE" Or UCase(arr_tmp_parm(1)) = "SUB" Or UCase(arr_tmp_parm(1)) = "1" Then
                str_Scope = "SubTree"
            End If
            Case "OU"
            ' Check if we have a OU path
            OUPath = "LDAP://ou=" & Replace(arr_tmp_parm(1), "/", ",ou=") & "," & str_Domain
            arr_tmp_ouname = Split(arr_tmp_parm(1), "/")
            OUName = arr_tmp_ouname(0)
            Case "GROUPOU"
            ' Check if we have a OU path
            GroupPath = "LDAP://ou=" & Replace(arr_tmp_parm(1), "/", ",ou=") & "," & str_Domain
            Case "GROUPNAME"
            ' Check if we have a OU path
            GroupName = arr_tmp_parm(1)
            Case "GROUPPREFIX"
            ' Check if we have a OU path
            str_GroupPrefix = arr_tmp_parm(1)
            Case "OBJ"
            str_ObjectCategory = ""
            If InStr(arr_tmp_parm(1), "U") Then str_ObjectCategory = str_ObjectCategory & "(objectCategory=User)"
            If InStr(arr_tmp_parm(1), "C") Then str_ObjectCategory = str_ObjectCategory & "(objectCategory=Computer)"
            If InStr(arr_tmp_parm(1), "G") Then str_ObjectCategory = str_ObjectCategory & "(objectCategory=Group)"
            If Len(arr_tmp_parm(1)) > 1 Then str_ObjectCategory = "(|" & str_ObjectCategory & ")"
            Case Else
            WriteLog "Unknown parameter: " & str_Parameter
        End Select
    Next
    ' Figure out defaults
    If GroupName = "" Then
        GroupName = str_GroupPrefix & " " & OUName
    End If
    If GroupPath = "" Then
        GroupPath = OUPath
    End If
    If GroupCN = "" Then
        GroupCN = "LDAP://CN=" & GroupName & "," & Right(GroupPath,Len(GroupPath)-7)
    End If

    WriteLog "GroupName: " & GroupName
    '    WriteLog "GroupPath: " & GroupPath
    WriteLog "GroupCN: " & GroupCN
    '    WriteLog "OUName: " & OUName
    WriteLog "OUPath: " & OUPath
    WriteLog "Scope: " & str_Scope
    WriteLog "ObjectCategory: " & str_ObjectCategory

    ''' SECURITY CHECK IF GROUPNAME CONTAINS
    If InStr(GroupName, sec_GroupName_MustContain) = 0 Or InStr(GroupCN, sec_GroupName_MustContain) = 0 Then
        ' Group name does not contain our security word
        WriteLog "XXX - This group does not contain the security word, this group will not be updated!"
        WriteLog "XXX - This security word must be in all Shadow Group names: " & sec_GroupName_MustContain
        count_GroupAborted = count_GroupAborted + 1
        Exit Function
    End If

    ''' SECURITY CHECK IF GroupOU is same as target OU
    If sec_GroupOU_equal_targetOU Then
        ' This means they must be same, otherwise we ignore
        If InStr(GroupCN, Right(OUPath, Len(OUPath)-7)) = 0 Then
            ' target OU is not part of group ou
            WriteLog "XXX - This group is not placed below the target OU, this group will not be updated!"
            count_GroupAborted = count_GroupAborted + 1
            Exit Function
        End If
    End If

    '' Start the updating of the defined shadow group
    Start_Update_ShadowGroup OUPath, GroupCN, str_Scope, str_ObjectCategory
End Function

Function Start_Update_ShadowGroup (ByVal dnOU, dnGroup, str_Scope, strObjectCategory)
    count_GroupProcessed = count_GroupProcessed + 1
    dictGroupObjects.RemoveAll
    dictOUObjects.RemoveAll
    WriteLog "Updating group: " & dnGroup
    Find_OU_Objects dnOU, strObjectCategory, str_Scope, dnGroup
    Find_Group_Objects dnGroup
    Add_Group_Objects dnGroup
    Remove_Group_Objects dnGroup
End Function

Function Remove_Group_Objects (ByVal dnGroup)
    For Each removeObject In dictGroupObjects.Items
        If Not dictOUObjects.Exists(removeObject) Then
            ' object is not in OU lets remove it
            Set objGroup = GetObject(dnGroup)
            objGroup.PutEx ADS_PROPERTY_DELETE,"member",Array(removeObject)
            objGroup.SetInfo
            WriteLog vbTab & " - removing object from group: " & removeObject
            count_MemberRemoved = count_MemberRemoved + 1
            Set objGroup = Nothing
        End If
    Next
End Function

Function Add_Group_Objects (ByVal dnGroup)
    For Each addObject In dictOUObjects.Items
        If Not dictGroupObjects.Exists(addObject) Then
            ' object is not in group lets add it
            Set objGroup = GetObject(dnGroup)
            objGroup.PutEx ADS_PROPERTY_APPEND,"member",Array(addObject)
            objGroup.SetInfo
            WriteLog vbTab & " + adding object to group: " & addObject
            count_MemberAdded = count_MemberAdded + 1
            Set objGroup = Nothing
        End If
    Next
End Function

Function Find_OU_Objects (ByVal dnOU, strObjectCategory, str_Scope, dnGroup)
    Set objConnection = CreateObject("ADODB.Connection")
    Set objCommand =   CreateObject("ADODB.Command")
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
    Set objCommand.ActiveConnection = objConnection
    objCommand.Properties("Page Size") = 1000
    objCommand.CommandText = _
    "<"_
    & dnOU &_
    ">;"_
    & strObjectCategory &_
    ";AdsPath;"_
    & str_Scope
    Set objRecordSet = objCommand.Execute
    If objRecordSet.EOF = False Then objRecordSet.MoveFirst
    Do Until objRecordSet.EOF
        tmp_object = Right(objRecordSet.Fields("AdsPath").Value, Len(objRecordSet.Fields("AdsPath").Value) -7)
        ' we dont want the actual shadow group as a member
        If objRecordSet.Fields("AdsPath").Value <> dnGroup Then
            ' we do not like special chars in names, add more if you find them
            tmp_object = Replace(tmp_object, "\/", "/")
            ' add object to dictionary
            dictOUObjects.Add tmp_object, tmp_object
            If boolDebug Then WriteLog vbTab & "OU member " & tmp_object
        End If
        objRecordSet.MoveNext
    Loop
    Set objCommand = Nothing
    Set objConnection = Nothing
End Function

Function Find_Group_Objects (Byval dnGroup)
    On Error Resume Next ' In case group doesnt exist
    Dim member, arrMemberOf

    Set objGroup = GetObject(dnGroup)
    If Err.Number = -2147016656 Then
        ' Group does not exist
        Err.Clear
        '  Create new Group
        arr_tmp_ou = Split ( dnGroup, ",", 2 )
        arr_tmp_cn = Split ( arr_tmp_ou(0), "//", 2)
        arr_tmp_name = Split ( arr_tmp_cn(1), "=", 2)
        WriteLog "Creating group " & arr_tmp_name(1) & " / " & arr_tmp_cn(1) & " in ou " & arr_tmp_ou(1) & " err: " & Err.Number
        Set objOU = GetObject("LDAP://" & arr_tmp_ou(1))
        Set objGroup = objOU.Create("Group", arr_tmp_cn(1))
        objGroup.Put "sAMAccountName", arr_tmp_name(1)
        objGroup.Put "description", Array(const_DefaultGroupDescription)
        objGroup.Put "groupType", ADS_GROUP_TYPE_GLOBAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED
        Err.Clear
        objGroup.SetInfo
        If Err.Number <> 0 Then
            WriteLog "Problem creating group, possibly allready exists or sam account name used: " & arr_tmp_name(1)
            Err.Clear
        Else
            WriteLog "Created group successfully"
            count_GroupCreated = count_GroupCreated + 1
        End If
    End If
    objGroup.GetInfo
    arrMemberOf = objGroup.GetEx("member")
    If Err.Number = -2147463155 Then
        ' Group has no members
        Err.Clear
        Exit Function
    End If
    For Each member In arrMemberOf
        'Add the user to a dictionary object
        dictGroupObjects.Add member, member
        If boolDebug Then WriteLog vbTab & "Group Member " & member
    Next
    Set objGroup = Nothing
End Function

Function WriteLog (strLog)
    currentTime = Timer - startTime
    If currentTime < 0.1 Then currentTime = "0,0"
    If Len(currentTime) > 5 Then currentTime = Left(currentTime, 5)
    currentTime = currentTime & "s"
    While Len(currentTime) < 6
        currentTime = currentTime & " "
    Wend
    WScript.Echo currentTime & vbTab & strLog
    str_LogText = str_LogText & currentTime & vbTab & strLog & vbCrLf
End Function

Function WriteLogFile ()
    WriteLog "Appending log to file: " & LogFileName
	Set fso = CreateObject("Scripting.FileSystemObject") ' used for file operations
    Set theLog = fso.OpenTextFile(LogFileName, 8, True)
    theLog.WriteLine(str_LogText)
    theLog.Close
    Set theLog = Nothing
    Set fso = Nothing
    str_LogText = ""
End Function

Function Start_Update_OU(arr_OU)
    Dim i
    i = 0
    While arr_OU(i,0) <> ""
        Err.Clear
        WriteLog "SG Object " & arr_OU(i,2)
        WriteLog "SG Parameter " & arr_OU(i,1)
        'Start_Do_Parameters Parameters, OUName, OUpath, GroupName, GroupOU
        Start_Do_Parameters arr_OU(i,1), arr_OU(i,0), arr_OU(i,2), "", ""
        If Err.Number <> 0 Then
            WriteLog "ErrNumber:" & Err.Number
            WriteLog "ErrDescription:" & Err.Description
            WriteLog "ErrSource:" & Err.Source
        End If
        WriteLog ""
        i = i + 1
    Wend
End Function

Function Start_Update_Group(arr_Group)
    Dim i
    i = 0
    While arr_Group(i,0) <> ""
        Err.Clear
        WriteLog "SG Object " & arr_Group(i,2)
        WriteLog "SG Parameter " & arr_Group(i,1)
        'Start_Do_Parameters Parameters, OUName, OUpath, GroupName, GroupOU
        Start_Do_Parameters arr_Group(i,1), "", "", arr_Group(i,0), arr_Group(i,2)
        If Err.Number <> 0 Then
            WriteLog "ErrNumber:" & Err.Number
            WriteLog "ErrDescription:" & Err.Description
            WriteLog "ErrSource:" & Err.Source
        End If
        WriteLog ""
        i = i + 1
    Wend
End Function

' Find OU's with Shadow Group properties
Function Find_OU_ShadowGroup ()
    Dim arr_tmp_object(1000,2)
    On Error Resume Next
    WriteLog "Getting OU's with shadowgroup in description"
    Set objConnection = CreateObject("ADODB.Connection")
    Set objCommand =   CreateObject("ADODB.Command")
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
    Set objCommand.ActiveConnection = objConnection
    objCommand.Properties("Page Size") = 1000
    objCommand.CommandText = _
    "<LDAP://" & str_BaseOU & str_Domain &_
    ">;(&(objectCategory=OrganizationalUnit)(description=SHADOWGROUP*));Name,AdsPath,description;Subtree"
    Set objRecordSet = objCommand.Execute
    objRecordSet.MoveFirst
    count_obj = 0
    Do Until objRecordSet.EOF
        arr_tmp_object(count_obj,0) = objRecordSet.Fields("Name").Value
        arrDescription = objRecordSet.Fields("description").Value
        arr_tmp_object(count_obj,1) = arrDescription(0)
        arr_tmp_object(count_obj,2) = objRecordSet.Fields("AdsPath").Value
        count_obj = count_obj + 1
        objRecordSet.MoveNext
    Loop
    Find_OU_ShadowGroup = arr_tmp_object
    Set objCommand = Nothing
    Set objConnection = Nothing
End Function

' Find Group's with Shadow Group properties
Function Find_Group_ShadowGroup ()
    Dim arr_tmp_object(1000,2)
    On Error Resume Next
    WriteLog "Getting Groups with shadowgroup in description"
    Set objConnection = CreateObject("ADODB.Connection")
    Set objCommand =   CreateObject("ADODB.Command")
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
    Set objCommand.ActiveConnection = objConnection
    objCommand.Properties("Page Size") = 1000
    objCommand.CommandText = _
    "<LDAP://" & str_BaseOU & str_Domain &_
    ">;(&(objectCategory=Group)(description=SHADOWGROUP*));Name,AdsPath,description;Subtree"
    Set objRecordSet = objCommand.Execute
    objRecordSet.MoveFirst
    count_obj = 0
    Do Until objRecordSet.EOF
        arr_tmp_object(count_obj,0) = objRecordSet.Fields("Name").Value
        arrDescription = objRecordSet.Fields("description").Value
        arr_tmp_object(count_obj,1) = arrDescription(0)
        arr_tmp_object(count_obj,2) = objRecordSet.Fields("AdsPath").Value
        count_obj = count_obj + 1
        objRecordSet.MoveNext
    Loop
    Find_Group_ShadowGroup = arr_tmp_object
    Set objCommand = Nothing
    Set objConnection = Nothing
End Function

The fine print

Feel free to comment, make suggestions for improvements optimizations, use for commercial purposes allowed, etc. just requires you give credit in the script used.The script is made available under the creative commons license. Any questions, need some more information, help with using it, etc. leave a comment or e-mail me.

My hope is that the script will be put to good use. But all use in a production environment is at own risk and liability.

22 Responses to “Active Directory Shadow Group Script – will let you spend less time on updating group memberships”

  • Sole:

    Thanks 🙂
    I actually created another version that can also use other fields than the comments to set a ShadowGroup parameter. Available on request if needed.

  • Morten:

    add line 76-77
    Dim Shell
    Set Shell = CreateObject(“Wscript.Shell”)

    replace line
    WScript.Echo currentTime & vbTab & strLog
    with
    Shell.Popup currentTime & vbTab & strLog,1,,64

    then you will have a 1 sec popup instead echo when running booldebug = true

  • Jason:

    Excellent script – thanks for putting the work into creating this!

    I’m curious why you chose to include the function at lines 192-201. We have a very logically named OU structure with a few nested levels to apply GPO effectively, and so I’ve created a separate OU to hold all of the Shadow Groups. Because I know exactly which OU is referenced by looking at the group name, this gives me the best visibility of all Shadow Groups at once. Without commenting 192-201, I was unable to operate in this fashion.

    Thanks again!

  • Michael:

    Hey

    Super nice script 🙂

    Is it possible to add the parent OU name to the Shadow group name?

    For example:

    Company -> Dep1 -> Users (Prefix Shadowgroup Users – Dep1)
    Company -> Dep2 -> Users (Prefix Shadowgroup Users – Dep2)

    Michael

  • Loil:

    Superb 😉

    How to get the full OU-name as name in the group name?

    Loil

  • Sole:

    If you look at line 51-52
    ‘ Only allow shadowgroup destination in or below source OU
    Const sec_GroupOU_equal_targetOU = True

    You just have to set that last line to false, this will allow the functionality you are using with an OU somewhere outside the path of the Shadow Group OU.

    The reason behind it. It ensures that the script will NOT edit any group that is not at or below the point with the Shadow Group Setting. Since the script is most likely running with full permissions and someone might have delegated rights to a OU at some lower level. I did not want them to be able to create a shadow group setting that would make changes in a group the “script owner” has permission too, but the delegated user does not, i.e. the Administrators group.

  • Sole:

    Not out of the box, but nothing preventing you from adding it.

  • Sole:

    I am unsure what you mean, but I assume you mean the full OU path.
    In my own experience I always hit the groupname length limit so this didn’t work for me.
    Thats why I didnt use it, but nothing prevents you from just taking the full path and using it for the name, however it does not do that out of the box.

  • Phil:

    Great Script! Works great!

    The only issue I ran into was with this line:

    LogFileName = “\\fileserver.my.domain.local\logshare\logfolder\logfile-” & Date & “.log” ‘ daily log file

    Cannot create a file name with the “/” character. My date format is 6/16/2011. Changed the script to write to a file called results.txt. This works well because there is a timestamp on the file itself and also in the log.

    LogFileName = “\\fileserver.my.domain.local\logshare\logfolder\results.txt”‘ daily log file

  • Phil:

    Update to my earlier post. I was able to produce a date named logfile by adding/editing the following.

    dtmThisDay = Day(Date)
    dtmThisMonth = Month(Date)
    dtmThisYear = Year(Date)

    LogFileName = “d:\scripts\logfile-” & dtmThisMonth & “_” & dtmThisDay & “_” & dtmThisYear & “.log” ‘ daily log file

  • Venkat:

    Hello,

    I am looking for script or automation that will automatically update the Users office field with OU description. We have many Sub-OU’s depends on the location and like OU Newyork(Admin, computers, Secrutiy group, distribution group, Users).

    In the Users OU, we want to update the office field with Description present in OU NewYork. Whenever someone add or move user in this OU. The office field should update automatically.

    I will thankful to you.

    Best Wishes,
    Venkat

  • Great script! Since I’m not big into scripts it wasn’t 100% intuitive but I still got it working quickly. A few changes I made:
    The date thing that Phil mentioned. I just used the “WeekdayName(Weekday(Now))” command instead.
    Also changed the second security check “GroupOU is same as target OU”
    case sensitivity was a problem so I modified the script to use:
    “If InStr(ucase(GroupCN), ucase(Right(OUPath, Len(OUPath)-7))) = 0 Then”

    The only question I have is about using the script on a DC vs. a member server. Does anyone have input on where it’s best to create a scheduled task for this to run?

  • Sole:

    Thanks for sharing your fixes 🙂

  • Sole:

    Hi Venkat,

    You would probally need to program a new script to do that specific task, most of the code is present in this script so it would be an easy task for someone with a little knowledge of VB Scripting to reuse some of the code for your purpose.

  • swensc:

    I used this script to create a Shadowgroup and initially populate the group. Now I created a few test users and ran the script again to see if those new users get added to the group and they don’t. I did notice in the comments about a hardcoded 1000 limit on the dictionary and LDAP searches. I have about 20,000 users in my OU. How can I increased the 1000 limit?

  • Sole:

    Hi Swensc,
    I would recommend not having 20.000 users in one OU, you should have the same issue when using other LDAP tools like ADUC (Active Directory Users and Computers MMC console). The limit of results from LDAP searches of 1000 is not from the scripts end, but LDAP searches in MS. I am sorry but I do not have any easy fixes for this issue. You would somehow need to make the script able to work with the users in portions, to split up the very large amount of results.

  • Antoine Dijoux:

    Hi,

    Thanks for this well-commented script. It worked really well. Here is a little feedback.
    – The fact that it is needed to put the word “shadowgroup” in each UO we want to process wasn’t obvious for me. When I found it out, I looked for a way to delete this obligation, but it looked a bit tricky and dangerous for the script.
    – I didn’t know if the comma was intended in the value ‘Const str_BaseOU = “ou=Departments,’ (it looks like it is)
    – I experienced great performances on a very low configuration (one processor and 512Mb Ram). When the script execution is scheduled, it takes less than 2s for 700 operations (add/delete members/groups).

    Oh, and thanks to Phil for his date troubleshooting (for I use french date format).

  • Sole:

    I am glad the script was usefull and that you took the time to give feedback others can use 🙂
    -Sole

  • Sole:

    Just a quick note, I do not remember it as required that the OU’s name includes the ShadowGroup Word, but it was required for the Group names, to ensure we didn’t mess with any admin/security sensitive Groups.

    so to avoid a scenario where someone has rights to create OU’s and use the shadowgroup functions, they could make an OU pointing to the domain admins Group, updating its members… that is why the Word shadowgroup should be in the Group name.

  • Steve:

    I have the same problem as Phil with the line “LogFileName = “\\fileserver.my.domain.local\logshare\logfolder\logfile-” & Date & “.log” ‘ daily log file”

    Logfiles are not created. I want to edit/update the script with the the solution phil also suggest. But do i have to add the lines to the script.Where do i place them?? Or do i have to replace some lines??
    Thanx

  • Steve:

    Don’t mind my last post. Got it working..

Leave a Reply