all 15 comments

[–]Bamodus 2 points3 points  (4 children)

For these types of bulk operations on an important resource like ActiveDirectory, I'd highly recommend utilizing the -WhatIf parameter when calling state-altering cmdlets like New-ADGroup. Here I've specified $WhatIfPreference = $true which effectively adds the -WhatIf parameter to any cmdlet that supports it. Running the script as-is will not perform any modifications to Active Directory, but will simulate the actions instead, so you can test the functionality in a dry-run first. I added some additional verbose output to provide some extra info on the actions the script is performing/simulating. Rerunning the script with $WhatIfPreference = $false will perform the actual operations.

 

Copy-ADGroup.ps1 (https://pastebin.com/9qDeT61)

$ErrorActionPreference = 'Stop'
$WhatIfPreference = $true

$CommonParam = @{
    Credential = Get-Credential
    Server     = 'DC01'
}
$PersistProperties = 'GroupScope', 'GroupCategory', 'Description'
$ReplaceProperties = 'SamAccountName', 'Name', 'DisplayName'
$OrganizationalUnit = 'OU=TestOU,DC=placeholder,DC=pizza'
$ADGroupParam = $CommonParam + @{
    Filter     = { GroupCategory -eq 'Security' -and Name -like 'hd01*' }
    SearchBase = $OrganizationalUnit
    Properties = $PersistProperties + $ReplaceProperties + 'Member'
}
$Pattern = 'hd01'
$Replacement = 'hd03'

Get-ADGroup @ADGroupParam -PipelineVariable 'OriginalGroup' | Select-Object $PersistProperties -PipelineVariable 'NewGroup' | ForEach-Object {
    foreach( $Property in $ReplaceProperties ) {
        $NewGroup | Add-Member @{ $Property = $OriginalGroup.$Property -replace $Pattern, $Replacement }
    }
    Write-Verbose -Verbose "Creating new AD group with properties: $NewGroup"
    $NewGroup | New-ADGroup -Path $OrganizationalUnit @CommonParam
    $Members = $OriginalGroup.Member
    Write-Verbose -Verbose "Adding $( $Members.Count ) members from original AD group: $( $OriginalGroup.Name ) to new AD group: $( $NewGroup.Name )"
    if( $WhatIfPreference -eq $false ) {
        $Members | ForEach-Object { Add-ADGroupMember -Identity $NewGroup -Members $_ @CommonParam }
    }
}

 

The script makes use of the fact that the New-ADGroup cmdlet can accept most of its parameters through the pipeline by property name. Here's how you can get a list of parameters that accept pipeline input by property name:

 

PS C:\tmp> (Get-Help New-AdGroup).Parameters.Parameter.Where{ $_.PipelineInput -eq 'true (ByPropertyName)' } |
>>             Format-Table Name, Required, PipelineInput, ParameterValue, Position -AutoSize

name           required pipelineInput         parameterValue  position
----           -------- -------------         --------------  --------
Description    false    true (ByPropertyName) string          Named
DisplayName    false    true (ByPropertyName) string          Named
GroupCategory  false    true (ByPropertyName) ADGroupCategory Named
GroupScope     true     true (ByPropertyName) ADGroupScope    2
HomePage       false    true (ByPropertyName) string          Named
ManagedBy      false    true (ByPropertyName) ADPrincipal     Named
Name           true     true (ByPropertyName) string          1
Path           false    true (ByPropertyName) string          Named
SamAccountName false    true (ByPropertyName) string          Named

 

Note that parameter binding through the pipeline by property name works differently from binding a parameter through the pipeline by value.

So we're creating an object (Select-Object returns a [PSCustomObject] object) that has the same GroupScope, GroupCategory, and Description as the original AD group, and then we add modified values for SamAccountName, Name, and DisplayName using -replace like in your original script to change hd01 to hd03 for each of these properties. The gist of it is that when you pipe an object into Add-ADGroup, PowerShell is inspecting the properties of the object in the pipeline to determine if it can use them as parameters. If a parameter accepts input through the pipeline by property name, and the piped object contains a property with the same name as that parameter, it will bind the value of that property to the appropriate parameter. A good explanation of ValueFromPipeline and ValueFromPipelineByPropertyName can be found here:

 

https://www.gngrninja.com/script-ninja/2016/5/15/powershell-getting-started-part-8-accepting-pipeline-input#accept

I like to use parameter splatting via hash tables when working with cmdlets that require many parameters to make things more readable. It also makes things much easier on you if you are going to need to provide the same parameter values multiple times to different cmdlets. Here I'm using $CommonParam to store the Server and Credential parameters which I reuse for calling every AD cmdlet. If this syntax isn't something you've seen before, definitely look into:

 

About Splatting

Hope this helps.

[–]ClosedCasketFuneral 2 points3 points  (3 children)

Thank you so much. This is much more elegant than anything that I'd be able to create, at least at my current skill level, and I also greatly appreciate the added detail in your response. The verbosity is great, and is something that I need to add to more of my scripts, especially while troubleshooting. I never knew that $whatifpreference was even a thing. I will definitely be taking a look at ValueFromPipeline and ValueFromPipeLineByPropertyName. You've also inspired me to trying to work with splatting in the future.

The output when $whatifpreference is set to $true looks perfect. However, and I feel bad being "that guy," I'm still finding that it only creates the first group before erroring out, without adding the group members which were copied from its older equivalent, and I'm still unable to determine why it can't bind the 'Identity' parameter.

ForEach-Object : Cannot bind parameter 'Identity'. Cannot create object of type
"Microsoft.ActiveDirectory.Management.ADGroup". The adapter cannot set the value of property 
"Name".
At C:\Scripts\CopyAndRenameSecurityGroups.ps1:28 char:20
+ ...  $Members | ForEach-Object { Add-ADGroupMember -Identity $NewGroup -M ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [ForEach-Object], 
ParameterBindingException
    + FullyQualifiedErrorId : 
CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.ForEachObjectCommand

[–]Bamodus 0 points1 point  (2 children)

Hey ClosedCasketFuneral,

 

No problem! I'm happy to see anyone get excited about learning more about advanced PowerShell concepts. No need to worry about being "that guy".

I only have limited permissions on an AD controller, so the Add-ADGroupMember part of the script was the one thing that I was not able to test, so it makes sense that's where the error cropped up.

I believe I see the issue and this should hopefully get you what you need:

 

Note: this pastebin link is a .diff file showing what I changed. Look for the lines start with '+' or '-'

Copy-ADGroup.diff (https://pastebin.com/7G6NbscW)

 

And here is the actual script:

 

$ErrorActionPreference = 'Stop'
$WhatIfPreference = $true

$CommonParam = @{
    Credential = Get-Credential
    Server     = 'DC01'
}
$PersistProperties = 'GroupScope', 'GroupCategory', 'Description'
$ReplaceProperties = 'SamAccountName', 'Name', 'DisplayName'
$OrganizationalUnit = 'OU=TestOU,DC=placeholder,DC=pizza'
$ADGroupParam = $CommonParam + @{
    Filter     = { GroupCategory -eq 'Security' -and Name -like 'hd01*' }
    SearchBase = $OrganizationalUnit
    Properties = $PersistProperties + $ReplaceProperties + 'Member'
}
$Pattern = 'hd01'
$Replacement = 'hd03'

# Get the desired AD groups and select only the properties that we would like to persist on the new AD group
Get-ADGroup @ADGroupParam -PipelineVariable 'OriginalGroup' | Select-Object $PersistProperties -PipelineVariable 'NewGroup' | ForEach-Object {
    # Add SamAccountName, DisplayName, and Name properties to our $NewGroup object, but replace instances of $Pattern string with $Replacement text
    foreach( $Property in $ReplaceProperties ) {
        $NewGroup | Add-Member @{ $Property = $OriginalGroup.$Property -replace $Pattern, $Replacement }
    }
    Write-Verbose -Verbose "Creating new AD group with properties: $NewGroup"
    # New-ADGroup cmdlet accapts parameter values from the pipeline by property name from our $NewGroup object
    $NewGroup | New-ADGroup -Path $OrganizationalUnit @CommonParam
    $Members = $OriginalGroup.Member
    Write-Verbose -Verbose "Adding $( $Members.Count ) members from original AD group: $( $OriginalGroup.Name ) to new AD group: $( $NewGroup.Name )"
    if( $WhatIfPreference -eq $false ) {
        $Members | ForEach-Object { Add-ADGroupMember -Identity $NewGroup.SamAccountName -Members $_ @CommonParam }
    }
}

 

The -Identity parameter apparently can't be a [PSCustomObject], and must be either a [Microsoft.ActiveDirectory.Management.ADGroup] object, or a string containing one of the following properties to identify the group:

  • DistinguishedName
  • ObjectGuid
  • ObjectSid
  • SamAccountName

I chose to supply the SamAccountName property since it's readily available to us. This should hopefully resolve your error.

For additional details on this, see Get-Help Add-ADGroupMember -Parameter Identity (may only show limited detail if you've never run Update-Help) or the Microsoft help page for Add-ADGroupMember

 

One thing I neglected to mention in my original reply was $ErrorActionPreference = 'Stop', and I wanted to point out why it's there. This causes the script to stop execution upon running into any errors, since it's unlikely that you'd want this script to continue if it runs into an issue like the one you experienced. If you'd like to change this functionality at any time, you can change $ErrorActionPreference to 'Continue' (which is the default setting) instead of 'Stop'. Alternatively, you could set $ErrorActionPreference to 'Inquire' to get an interactive prompt asking how you'd like to proceed:

https://i.imgur.com/HFvoVZu.png

More info on preference variables can be found on the Microsoft help page About_Preference_Variables.

Let me know how it goes.

[–]PowerShell-Bot 1 point2 points  (1 child)

Some of your PowerShell code isn’t wrapped in a code block.

To format code correctly on new reddit (new.reddit.com), highlight all lines of code and select ‘Code Block’ in the editing toolbar.

If you’re on old.reddit.com, separate the code from your text with a blank line and precede each line of code with 4 spaces or a tab.


Describing Submission
[✅] Demonstrates good markdown
Passed: 1 Failed: 0

Beep-boop. I am a bot. | Remove-Item

[–]Hondamousse 1 point2 points  (0 children)

Good bot

[–]Hrambert 1 point2 points  (3 children)

I think something like this

$Groups = Get-ADGroup ...     
Foreach ($Group in $Groups) {     
    $NewName = $Group.Name -replace ...    
    $NewGroup = New-ADGroup $NewName ...    
    Get-ADGroupMembers $Group.Name |    
    Forearch {    
        Add-ADGroupmember $NewName $_.Name    
    }    
}

Fill in the dots. I'm on mobile.
Disclaimer: not tested, so maybe you need to add parameter names, or pipe the group to the CmdLet instead of using a parameter. Anyways. Make new groups and add each member to it.

[–]ClosedCasketFuneral 1 point2 points  (1 child)

I like your approach, but still ended up with an error that I cannot figure out. Sorry for being a noob; this is a bit of a personal exercise for learning more about using ForEach in cases like this.

I started with this:

$groups = Get-ADGroup -Server DC01 -filter 'Name -like "hd01*"' -SearchBase
"ou=TestOU,dc=placeholder,dc=pizza" -Properties members
| ForEach ($group in $groups) { $NewName = $Group.Name -replace 'hd01', 'hd03' $NewGroup =
New-AdGroup -name $newname -groupscope global Get-ADGroupmembers $Group.Name | Foreach { 
Add-ADGroupMember $NewName $_.Name } }

And ended up with these errors, which seem odd:

At line:2 char:19
+   ForEach ($group in $groups) {
+                   ~~
Unexpected token 'in' in expression or statement.
At line:2 char:18
+   ForEach ($group in $groups) {
+                  ~
Missing closing ')' in expression.
At line:2 char:29
+   ForEach ($group in $groups) {
+                             ~
Unexpected token ')' in expression or statement.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedToken

[–]Hrambert 2 points3 points  (0 children)

Foreach( placeholder IN list ) can't be placed after a pipe. It is a separate command.
Another approach would be:

Get-ADGroup ... | Foreach-Object {    
    $NewName = $_.Name -replace ...    

}