Phased Deployment of Azure Conditional Access Multi-factor Authentication (MFA) using PowerShell

I recently had a client that needed to deploy Azure MFA, controlled by a conditional access policy, to around 30,000 users. Now, obviously, from a technical point of view this is quite straightforward, however because of the large number of users, and the fact that user communications and support needed to be managed, I wanted this to be done in a controlled way. I didn’t want to overload the helpdesk, or cause business problems for the users, due to unexpected issues.

I therefore wrote 3 scripts that would allow us to easily manage the deployment and ensure that things progressed in a tightly controlled way.

The process that was to be followed was this.

  1. Create the Conditional Access policy and assign to an MFA Deployment group.
  2. Identify all the affected users and gather their User Principal Names into multiple batches. Create files with, perhaps 500 users in each file. This ensures that users are enabled for MFA in small, manageable batches.
  3. Communications are sent to the user to tell them to register their additional authentication methods, together with the URL to the correct web page (aka.ms/MFASetup *). The users will automatically be prompted to register additional authentication methods when they first login, once they are enabled for MFA, however many users tend to ignore this if they can, so its better to try and get them to register before they need to. This saves issues building up with the helpdesk if they encounter problems after waiting until the last minute to register additional authentication methods.
  4. Run the CheckVerificationStatus.ps1 script. This script reads the UPNs from the Input File and lists each UPN together with a list of their registered Strong Authentication Methods. If nothing is listed for a user then they have not registered additional authentication methods for MFA. Communications could then be sent to those users to request that they register.
  5. Run the EnableMFA.ps1 script that takes the UPN names from a file, ensures that sufficient EMS licenses exist, provisions the user with an EMS license, then adds the user to the MFA deployment group.
  6. If irresolvable problems occur, run the DisableMFA.ps1 script which will back-out MFA for all users in the batch, or create a list of users to be backed out and run DisableMFA using that file.

* The aka.ms/MFASetup URL will send each user to the Additional Security Verification page for their own ID. If they haven’t registered additional security verification previously they will be prompted to configure it. If they have configured additional security verification previously they will be shown the page below.

The input files that provide the UPN list are simply TXT files that list each UPN on it’s own line. Try to ensure that there are no blank lines at the end of the file, otherwise you will see the following error in the PowerShell console for each blank line. It doesn’t affect how the scripts work though. The output file is simply a log of what the script has done.

Get-MsolUser : Cannot bind argument to parameter 'UserPrincipalName' because it is an empty string.
At line:10 char:52
+     $CurrentUser = Get-MsolUser -UserPrincipalName $UPN
+                                                    ~~~~
 
    + CategoryInfo          : InvalidData: (:) [Get-MsolUser], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.Online.Administration.Automation.GetUser

CheckVerificationStatus.ps1

Import-Module -Name Msonline
Connect-MsolService

$InputFile = "C:\Data\UpnList.txt" # List of UPNs
$OutputFile = "C:\Data\Output.txt"
 # Output file

# Read in list of users who will be enabled for MFA
$UPNs = Get-Content -Path $InputFile

ForEach ($UPN in $UPNs)
    {
    $CurrentUser = Get-MsolUser -UserPrincipalName $UPN
      
    If ($CurrentUser.StrongAuthenticationmethods -eq "")
        {
        $NewLine = $UPN + "`t" + "nul" >> $OutputFile
        }
    Else
        {
        $NewLine = $UPN + "`t" + $CurrentUser.StrongAuthenticationmethods.MethodType >> $OutputFile
        }
    }

After prompting for Azure credentials this script takes input from the $InputFile and outputs tab separated data to the $OutputFile. This file can be easily opened in Excel for data manipulation. An example of the output is shown below, showing the Strong Authentication Methods registered for each user. Users with no methods listed have not registered any additional security methods

graham.higginson@xxxxxx.com	OneWaySMS TwoWayVoiceMobile
joe.bloggs@xxxxxx.com	PhoneAppOTP PhoneAppNotification OneWaySMS TwoWayVoiceMobile
jim.booth@xxxxxx.com	
scott.devlin@xxxxxx.com	
andy.knowles@xxxxxx.com	
simon.jay@xxxxxx.com	
john.bonham@xxxxxx.com	

EnableMFA.ps1

Import-Module -Name MsolService
Connect-MsolService

$InputFile = "C:\Data\UpnList.txt"
 # List of UPNs
$OutputFile = "C:\Data\Output.txt"
 # Output File
$License = "xxxxx:EMS"
 # Name of license
$GroupID = "4adcacb1-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
 # GUID of the MFA Deployment group


# Read in list of users who will be enabled for MFA
$UPNs = Get-Content -Path $InputFile


# Identify how many available EMS licenses exist
$Licenses = Get-AzureADSubscribedSku | where {$_.SkuPartNumber -eq 'EMS'}
$PrepaidLicenses = $Licenses.PrePaidUnits.Enabled
$ConsumedLicenses = $Licenses.ConsumedUnits
$AvailableLicenses = $PrepaidLicenses - $ConsumedLicenses

If ($UPNs.count -lt $AvailableLicenses)
    {
     Write-Warning "Sufficient licenses are available"
     Write-Warning "Script continuing..."
    }
Else
    {
     Write-Warning "Insufficient licenses are available"
     Write-Warning "Script stopping!!"
     Break
    }

# Add EMS license to user and add to MFA group
ForEach ($UPN in $UPNs)
    {
    $CurrentUser = Get-MsolUser -UserPrincipalName $UPN
    If ($CurrentUser.Licenses.AccountSkuId -Contains $License)
        {
        Write-Host $UPN "already has an EMS license assigned" >>$OutputFile
        }
    Else
        {
        Set-MsolUserLicense -UserPrincipalName $UPN -AddLicenses $License
        If ($Error[0])
            {
            Write-Host "Unable to add license to" $UPN >>$OutputFile
            $Error.Clear
            }
        Else
            {
            Write-Host "Successfully added license to" $UPN >>$OutputFile
            }
        }

    Add-MsolGroupMember -GroupObjectId $GroupID -GroupMemberType User -GroupMemberObjectId $CurrentUser.ObjectId
    If ($Error[0])
        {
        Write-Host "Unable to add" $UPN "to the MFA group" >>$OutputFile
        $Error.Clear
        }
        Else
            {
            Write-Host "Successfully added" $UPN "to MFA group" >>$OutputFile
            }
    }

After prompting for Azure credentials this script reads in the list of user UPNs and checks that enough licenses are available for the number of users to be enabled for MFA. If there aren’t enough licenses the script will exit. Otherwise the script will assign and EMS license to each user, unless they already have a license assigned. The script then adds each user to the specified MFA Deployment group.

DisableMFA.ps1

Import-Module -Name MsolService
Connect-MsolService


$InputFile = "C:\Data\UpnList.txt"
 # List of UPNs
$OutputFile = "C:\Data\Output.txt"
 # Output File
 
$License = "xxxxx:EMS"
 # Name of license
$GroupID = "4adcacb1-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
 # GUID of the MFA Deployment group

# Read in list of users who will be enabled for MFA
$UPNs = Get-Content -Path $InputFile


ForEach ($UPN in $UPNs)
    {
    $CurrentUser = Get-MsolUser -UserPrincipalName $UPN
    Remove-MsolGroupMember -GroupObjectId $GroupID -GroupMemberType User -GroupMemberObjectId $CurrentUser.ObjectId
    If ($Error[0])
        {
        Write-Host "Unable to remove " $UPN " from MFA group" >>$OutputFile
        $Error.Clear
        }
    Else
        {
        Write-Host "Successfully removed " $UPN " from MFA group" >>$OutputFile
        }
    
    Set-MsolUserLicense -UserPrincipalName $UPN -RemoveLicenses $License
    
    If ($Error[0])
        {
        Write-Host "Unable to remove license from " $UPN >>$OutputFile
        $Error.Clear
        }
    Else
        {
        Write-Host "Successfully removed license from " $UPN >>$OutputFile
        }
    $NewLine = $UPN + "`t" + "Removed from Group" >> $OutputFile
    }

After prompting for Azure credentials this script reads in the list of UPNs and then attempts to remove the user from the MFA Deployment group. It then attempts to remove the EMS license from the user.

*** You may want to comment out the license removal if many of your users already have EMS licenses assigned prior to rolling out MFA. ***

This solution was designed for a very particular use case, and may not be a perfect fit for every requirement, but I hope it provides food for thought. Feel free to take the bits that work for you, to produce your own solution.

Troubleshooting a Custom Script Extension as part of an ARM Template Deployment

I recently decided to test the use of a Custom Script Extension, as I wanted to deploy a VM with client software installed. I created a Powershell script that downloaded, extracted and installed the MSI tool Orca. If I could get this to deploy and run successfully it would give me a basis to install any software I needed, plus perform any other configuration I might need.

I added lots of error logging to the script so that I would have plenty of information when it came to troubleshooting. The script I used is located here.

Create ARM Template

I took an existing ARM template that I used to create a Windows 10 VM, renamed it and added the Custom Script Extension code. The template is here and the parameter file I used is here.

The Custom Script Extension code itself is shown here.

To deploy the template I used the following Powershell commands (after installing the Az module).

Connect-AzAccount
New-AzResourceGroupDeployment -Name TestDeployment -ResourceGroupName HiggCon -TemplateFile C:\Dev\VM\VMcse.template.json -TemplateParameterFile C:\Dev\VM\VM.parameters.json

And that is where the problems started…

There is a part of me that always hopes that something like this will work first time, but I have to be honest, it hardly ever does! So the error I got back is below.

The deployment failed

I looked back through my Custom Script Extension code (which I had got, and edited, from the Microsoft documentation), and found the following line that I hadn’t noticed.

"name": "virtualMachineName/config-app",

Hmm, that didn’t look right, so I changed it to this.

"name": "[Concat(parameters('virtualMachineName'),'/config-app')]",

Hurrah, the deployment was then successful…but.

Looking in Azure, I could see that the VM appeared to have been created successfully. Unfortunately, after logging into the VM, Orca wasn’t installed.

I looked in the Eventlog. There were no entries from my script so it looked like the script hadn’t run, but why?

First I looked in the C:\WindowsAzure\Logs\WaAppAgent.Log file. It looked like some Custom ScriptExtensions had run successfully, but they looked Azure specific, with no hint that my script had run. After some digging I managed to find the VMConfig.ps1 file, that was my script, in C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.10.8\Downloads\0. So that showed that the script had been copied down correctly. I tried running the script manually…and got the “Running Scripts is disabled” message. Doh!

I changed my command to execute to the following, deleted the VM and reran the deployment.

"commandToExecute": "powershell -ExecutionPolicy Unrestricted -File VMConfig.ps1"

Success! My Deployment worked AND my script worked. Orca was installed successfully. My script had also written successfully to the Event Viewer.

I looked in C:\WindowsAzure\Logs\WaAppAgent.Log and found these entries near the bottom.

Its not exactly clear, but I think it is related to my script as it wasn’t there previously. The middle line includes this “ExtensionName: ”’ seqNo ‘0’ reached terminal state”. My script was downloaded to a directory named 0, so my thinking is that they are related. The end of the last line stating “completed with status: RunToCompletion” sounds right too. Unfortunately it is difficult to know definitely.

Summary

So what have we learnt? Well first of all, I think that when writing a script to deploy using Custom Script Extension, it makes a lot of sense to include as much logging as possible, and to test if fully before trying to deploy, as it will be difficult to determine what is wrong otherwise.

The deployment can really be split into two sections, Deployment and Execution.

Deployment

It is easy to misconfigure the ARM template, as I found to my cost. I used Visual Studio Code to edit the template as it supports ARM templates and identifies error syntax. Make sure all errors are resolved before trying to deploy. The “New-AzResourceGroupDeployment” Powershell cmdlet will tell you if the deployment was successful or not. The error messages aren’t always easy to interpret though.

Execution

Once successfully deployed, if you don’t get the result you are expecting check the

C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.10.8\Downloads

directory for a subdirectory holding your script. That way you know that it is actually there. Then trying running the script and see what error messages you get.

I also looked in

C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.10.8\Status

and found a “0.status” file that gave me a little more information on my script, and a success status code.

In

C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.10.8\RuntimeSettings

I found a file named “0”, which contained, amongst other things, the URL the script was download from and the command to execute. So be aware if you are working in a secure environment, or are using a public repository.

Copying or migrating users between groups

A common task when implementing new systems is to copy or migrate users from one AD group to another. It’s often a requirement when deploying upgraded application versions too. I wrote a small utility that makes this task easier. Being GUI based means it can be used by less technical team members as well.

The PowerShell code for this utility is below.

#region ScriptForm Designer

#region Constructor

[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")

#endregion

#region Post-Constructor Custom Code

#endregion

#region Form Creation
#Warning: It is recommended that changes inside this region be handled using the ScriptForm Designer.
#When working with the ScriptForm designer this region and any changes within may be overwritten.
#~~< GroupMembershipMigrationToolForm >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$GroupMembershipMigrationToolForm = New-Object System.Windows.Forms.Form
$GroupMembershipMigrationToolForm.ClientSize = New-Object System.Drawing.Size(839, 479)
$GroupMembershipMigrationToolForm.Text = "Group Membership Migration Tool"
#~~< UpdateButton >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$UpdateButton = New-Object System.Windows.Forms.Button
$UpdateButton.Location = New-Object System.Drawing.Point(695, 422)
$UpdateButton.Size = New-Object System.Drawing.Size(75, 23)
$UpdateButton.TabIndex = 13
$UpdateButton.Text = "Update"
$UpdateButton.UseVisualStyleBackColor = $true
#~~< TextBox2 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$TextBox2 = New-Object System.Windows.Forms.TextBox
$TextBox2.Location = New-Object System.Drawing.Point(38, 422)
$TextBox2.Size = New-Object System.Drawing.Size(635, 20)
$TextBox2.TabIndex = 12
$TextBox2.Text = ""
#~~< Label1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Label1 = New-Object System.Windows.Forms.Label
$Label1.Location = New-Object System.Drawing.Point(38, 395)
$Label1.Size = New-Object System.Drawing.Size(100, 23)
$Label1.TabIndex = 11
$Label1.Text = "Label1"
$Label1.add_MouseClick({UpdateButtonMouseClick($Label1)})
#~~< MigrateButton >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$MigrateButton = New-Object System.Windows.Forms.Button
$MigrateButton.Location = New-Object System.Drawing.Point(695, 268)
$MigrateButton.Size = New-Object System.Drawing.Size(75, 23)
$MigrateButton.TabIndex = 10
$MigrateButton.Text = "Migrate"
$MigrateButton.UseVisualStyleBackColor = $true
$MigrateButton.add_MouseClick({MigrateButtonMouseClick($MigrateButton)})
#~~< ClearButton >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ClearButton = New-Object System.Windows.Forms.Button
$ClearButton.Location = New-Object System.Drawing.Point(583, 268)
$ClearButton.Size = New-Object System.Drawing.Size(75, 23)
$ClearButton.TabIndex = 9
$ClearButton.Text = "Clear"
$ClearButton.UseVisualStyleBackColor = $true
$ClearButton.add_MouseClick({ClearButtonMouseClick($ClearButton)})
#~~< CloseButton1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$CloseButton1 = New-Object System.Windows.Forms.Button
$CloseButton1.Location = New-Object System.Drawing.Point(469, 268)
$CloseButton1.Size = New-Object System.Drawing.Size(75, 23)
$CloseButton1.TabIndex = 8
$CloseButton1.Text = "Close"
$CloseButton1.UseVisualStyleBackColor = $true
$CloseButton1.add_MouseClick({CloseButton1MouseClick($CloseButton1)})
#~~< ResultsLabel >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ResultsLabel = New-Object System.Windows.Forms.Label
$ResultsLabel.Location = New-Object System.Drawing.Point(38, 119)
$ResultsLabel.Size = New-Object System.Drawing.Size(100, 15)
$ResultsLabel.TabIndex = 7
$ResultsLabel.Text = "Results"
$ResultsLabel.add_Click({Label1Click($ResultsLabel)})
#~~< TextBox1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$TextBox1 = New-Object System.Windows.Forms.TextBox
$TextBox1.Location = New-Object System.Drawing.Point(38, 146)
$TextBox1.Multiline = $true
$TextBox1.Size = New-Object System.Drawing.Size(347, 213)
$TextBox1.TabIndex = 6
$TextBox1.Text = ""
#~~< DeleteMembersCheckBox >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$DeleteMembersCheckBox = New-Object System.Windows.Forms.CheckBox
$DeleteMembersCheckBox.Location = New-Object System.Drawing.Point(450, 146)
$DeleteMembersCheckBox.Size = New-Object System.Drawing.Size(198, 33)
$DeleteMembersCheckBox.TabIndex = 5
$DeleteMembersCheckBox.Text = "Delete members from Source Group"
$DeleteMembersCheckBox.UseVisualStyleBackColor = $true
#~~< GroupMembershipMigrationToolLabel >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$GroupMembershipMigrationToolLabel = New-Object System.Windows.Forms.Label
$GroupMembershipMigrationToolLabel.Font = New-Object System.Drawing.Font("Tahoma", 12.0, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$GroupMembershipMigrationToolLabel.Location = New-Object System.Drawing.Point(258, 9)
$GroupMembershipMigrationToolLabel.Size = New-Object System.Drawing.Size(333, 23)
$GroupMembershipMigrationToolLabel.TabIndex = 4
$GroupMembershipMigrationToolLabel.Text = "Group Membership Migration Tool"
#~~< DestinationGroupLabel >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$DestinationGroupLabel = New-Object System.Windows.Forms.Label
$DestinationGroupLabel.Location = New-Object System.Drawing.Point(450, 54)
$DestinationGroupLabel.Size = New-Object System.Drawing.Size(141, 23)
$DestinationGroupLabel.TabIndex = 3
$DestinationGroupLabel.Text = "Destination Group"
$DestinationGroupLabel.add_Click({Label2Click($DestinationGroupLabel)})
#~~< SourceGroupLabel >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$SourceGroupLabel = New-Object System.Windows.Forms.Label
$SourceGroupLabel.Location = New-Object System.Drawing.Point(38, 54)
$SourceGroupLabel.Size = New-Object System.Drawing.Size(141, 23)
$SourceGroupLabel.TabIndex = 2
$SourceGroupLabel.Text = "Source Group"
#~~< ListBox2 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ListBox2 = New-Object System.Windows.Forms.ListBox
$ListBox2.FormattingEnabled = $true
$ListBox2.Location = New-Object System.Drawing.Point(450, 80)
$ListBox2.SelectedIndex = -1
$ListBox2.Size = New-Object System.Drawing.Size(347, 17)
$ListBox2.TabIndex = 1
#~~< ListBox1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ListBox1 = New-Object System.Windows.Forms.ListBox
$ListBox1.FormattingEnabled = $true
$ListBox1.Location = New-Object System.Drawing.Point(38, 80)
$ListBox1.SelectedIndex = -1
$ListBox1.Size = New-Object System.Drawing.Size(347, 17)
$ListBox1.TabIndex = 0
$GroupMembershipMigrationToolForm.Controls.Add($UpdateButton)
$GroupMembershipMigrationToolForm.Controls.Add($TextBox2)
$GroupMembershipMigrationToolForm.Controls.Add($Label1)
$GroupMembershipMigrationToolForm.Controls.Add($MigrateButton)
$GroupMembershipMigrationToolForm.Controls.Add($ClearButton)
$GroupMembershipMigrationToolForm.Controls.Add($CloseButton1)
$GroupMembershipMigrationToolForm.Controls.Add($ResultsLabel)
$GroupMembershipMigrationToolForm.Controls.Add($TextBox1)
$GroupMembershipMigrationToolForm.Controls.Add($DeleteMembersCheckBox)
$GroupMembershipMigrationToolForm.Controls.Add($GroupMembershipMigrationToolLabel)
$GroupMembershipMigrationToolForm.Controls.Add($DestinationGroupLabel)
$GroupMembershipMigrationToolForm.Controls.Add($SourceGroupLabel)
$GroupMembershipMigrationToolForm.Controls.Add($ListBox2)
$GroupMembershipMigrationToolForm.Controls.Add($ListBox1)

#endregion

#region Custom Code

#endregion

#region Event Loop

function Main{
     [System.Windows.Forms.Application]::EnableVisualStyles()
     [System.Windows.Forms.Application]::Run($GroupMembershipMigrationToolForm)
}

#endregion

#endregion

#region Event Handlers



function MigrateButtonMouseClick( $object ){

}

function ClearButtonMouseClick( $object ){

}

function CloseButton1MouseClick( $object ){

        

}


function UpdateButtonMouseClick( $object ){

    

    # **** Change the -searchbase entry below to point at the Active Directory OU containing Application Deployment Groups ****
    $Groups = get-adgroup -filter * -searchbase "OU="Application Deployment Groups", OU=Groups, OU=EUC, OU="Thames Water", DC=TWUTIL, DC=NET" | sort Name
    If ($SearchBase -ne "")
    {
    $Groups = get-adgroup -filter * -searchbase $SearchBase | sort Name
        if ($error[0] -gt "")
            {
                #$TextBox2.AppendText("Unable to get AD group list" + "`r`n")
                #$TextBox2.AppendText("Error message " + $Error[0] + "`r`n")
                $error.Clear()
            }
        else
            {
                #$TextBox2.AppendText("Successfully added " + $CompName + " to " + $SelectedGroup + "`r`n") 
                ForEach ($Group in $Groups)
                {
                    ListBox1.Items.Add($Group.Name)
                }
            }


Set-ItemProperty -path HKCU:\Software\GroupMembershipMigrationTool -Name SearchBase -Value $TextBox1.text
    $SearchBase= $TextBox1.Text 
    Try {$Groups = get-adgroup -filter * -searchbase $SearchBase | sort Name}
        Catch
            {
                #$TextBox2.AppendText("Unable to get AD group list" + "`r`n")
                #$TextBox2.AppendText("Error message " + $Error[0] + "`r`n")
                $error.Clear()
            }

        }
}



function Label1Click( $object ){

}

  


Main # This call must remain below all other event functions

#endregion

Add a User or Computer to multiple groups using PowerShell

During many deployment or transformation projects, no matter how organised, there is often a requirement to add Users or Computers to application deployment groups during the rollout phase. Often this has to be done at the last minute due to unexpected requirements (or users simply not having responded to requiests for information.

I wrote the following Powershell script to allow Users or Computers to be added to multiple groups. All the groups are expected to be in one OU, and this is set when the script is first run. It will then use that location when opened subsequently. Multiple groups can be selected.

This script requires that the Microsoft RSAT tools are installed on the machine where the script is run.

####################################################################################
# This application lists all available software groups and allows multiple groups to
# be added to a Computer or User
#
# Created by Graham Higginson 13/12/2017
# V1.0
#
####################################################################################

#region  ScriptForm  Designer

#region  Constructor


[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")

#endregion

#region Post-Constructor Custom Code

#endregion

#region Form Creation
#Warning: It is recommended that changes inside this region be handled using the ScriptForm Designer.
#When working with the ScriptForm designer this region and any changes within may be overwritten.
#~~< Form1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Form1 = New-Object System.Windows.Forms.Form
$Form1.ClientSize = New-Object System.Drawing.Size(904, 704)
#$Form.AutoScroll = $true
#~~< Label4 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Label4 = New-Object System.Windows.Forms.Label
$Label4.Location = New-Object System.Drawing.Point(24, 642)
$Label4.Size = New-Object System.Drawing.Size(604, 15)
$Label4.TabIndex = 10
$Label4.Text = "Enter AD group location here, e.g. OU=App Groups, OU=Groups, OU=Client, DC=Company, DC=Local "
#~~< TextBox3 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$TextBox3 = New-Object System.Windows.Forms.TextBox
$TextBox3.Location = New-Object System.Drawing.Point(24, 660)
$TextBox3.Size = New-Object System.Drawing.Size(540, 21)
$TextBox3.TabIndex = 9
$TextBox3.Text = ""
#~~< TextBox2 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$TextBox2 = New-Object System.Windows.Forms.TextBox
$TextBox2.Location = New-Object System.Drawing.Point(24, 475)
$TextBox2.Multiline = $true
$TextBox2.ScrollBars = "Vertical"
$TextBox2.WordWrap = $false
$TextBox2.Size = New-Object System.Drawing.Size(540, 141)
$TextBox2.TabIndex = 8
$TextBox2.Text = ""
#~~< Label3 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Label3 = New-Object System.Windows.Forms.Label
$Label3.Font = New-Object System.Drawing.Font("Tahoma", 12.0, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$Label3.Location = New-Object System.Drawing.Point(114, 479)
$Label3.Size = New-Object System.Drawing.Size(277, 23)
$Label3.TabIndex = 8
$Label3.Text = ""
$Label3.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
#~~< ListView1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ListView1 = New-Object System.Windows.Forms.ListView
$ListView1.Location = New-Object System.Drawing.Point(24, 81)
$ListView1.Size = New-Object System.Drawing.Size(540, 374)
$ListView1.TabIndex = 7
#$ListView1.CheckBoxes = $true
$ListView1.FullRowSelect = $true
$ListView1.Text = "ListView1"
$ListView1.UseCompatibleStateImageBehavior = $false
$ListView1.MultiSelect = $true
$ListView1.View = [System.Windows.Forms.View]::Details
#~~< ColumnHeader1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ColumnHeader1 = New-Object System.Windows.Forms.ColumnHeader
$ColumnHeader1.Text = "Group Name"
$ColumnHeader1.Width = 400
#~~< ColumnHeader2 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#$ColumnHeader2 = New-Object System.Windows.Forms.ColumnHeader
#$ColumnHeader2.Text = "Install State"
#$ColumnHeader2.Width = 100
$ListView1.Columns.AddRange([System.Windows.Forms.ColumnHeader[]](@($ColumnHeader1)))
#~~< Button4 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Button4 = New-Object System.Windows.Forms.Button
$Button4.Font = New-Object System.Drawing.Font("Tahoma", 12.0, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$Button4.Location = New-Object System.Drawing.Point(678, 408)
$Button4.Size = New-Object System.Drawing.Size(120, 47)
$Button4.TabIndex = 6
$Button4.Text = "Close"
$Button4.UseVisualStyleBackColor = $true
$Button4.add_MouseClick({Button4MouseClick($Button4)})
#~~< Button3 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Button3 = New-Object System.Windows.Forms.Button
$Button3.Font = New-Object System.Drawing.Font("Tahoma", 12.0, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$Button3.Location = New-Object System.Drawing.Point(678, 341)
$Button3.Size = New-Object System.Drawing.Size(120, 47)
$Button3.TabIndex = 5
$Button3.Text = "Clear"
$Button3.UseVisualStyleBackColor = $true
$Button3.add_MouseClick({Button3MouseClick($Button3)})
#~~< Button2 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Button2 = New-Object System.Windows.Forms.Button
$Button2.Font = New-Object System.Drawing.Font("Tahoma", 12.0, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$Button2.Location = New-Object System.Drawing.Point(678, 642)
$Button2.Size = New-Object System.Drawing.Size(120, 47)
$Button2.TabIndex = 4
$Button2.Text = "Update"
$Button2.UseVisualStyleBackColor = $true
$Button2.add_MouseClick({Button2MouseClick($Button2)})
#~~< Button1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Font = New-Object System.Drawing.Font("Tahoma", 12.0, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$Button1.Location = New-Object System.Drawing.Point(678, 196)
$Button1.Size = New-Object System.Drawing.Size(120, 47)
$Button1.TabIndex = 3
$Button1.Text = "Add"
$Button1.UseVisualStyleBackColor = $true
$Button1.add_MouseClick({Button1MouseClick($Button1)})
#~~< Label2 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Label2 = New-Object System.Windows.Forms.Label
$Label2.Font = New-Object System.Drawing.Font("Tahoma", 9.5, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$Label2.Location = New-Object System.Drawing.Point(663, 81)
$Label2.Size = New-Object System.Drawing.Size(201, 19)
$Label2.TabIndex = 2
$Label2.Text = "Enter computer or User name"
#~~< Label1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$Label1 = New-Object System.Windows.Forms.Label
$Label1.Font = New-Object System.Drawing.Font("Tahoma", 16.0, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, ([System.Byte](0)))
$Label1.Location = New-Object System.Drawing.Point(214, 9)
$Label1.Size = New-Object System.Drawing.Size(446, 32)
$Label1.TabIndex = 1
$Label1.Text = "Multi-group add Tool"
$Label1.add_Click({Label1Click($Label1)})
#~~< TextBox1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$TextBox1 = New-Object System.Windows.Forms.TextBox
$TextBox1.Location = New-Object System.Drawing.Point(663, 103)
$TextBox1.Size = New-Object System.Drawing.Size(135, 20)
$TextBox1.TabIndex = 0
$TextBox1.Text = ""
$Form1.Controls.Add($Label4)
$Form1.Controls.Add($TextBox3)
$Form1.Controls.Add($TextBox2)
$Form1.Controls.Add($Label3)
$Form1.Controls.Add($ListView1)
$Form1.Controls.Add($Button4)
$Form1.Controls.Add($Button3)
$Form1.Controls.Add($Button2)
$Form1.Controls.Add($Button1)
$Form1.Controls.Add($Label2)
$Form1.Controls.Add($Label1)
$Form1.Controls.Add($TextBox1)

#endregion

#region Custom Code

#endregion

#region Event Loop

function Main{
	[System.Windows.Forms.Application]::EnableVisualStyles()
    #$Form1.AcceptButton = $Button1
	
    $listView1.Items.Clear()
    
    foreach ($Group in $Groups)
       {
           $Line = New-Object System.Windows.Forms.ListViewItem($Group.Name)
           $ListView1.Items.Add($Line)
       }

    [System.Windows.Forms.Application]::Run($Form1)

}


#endregion

#endregion

#region Event Handlers

function Button1MouseClick( $object )
{
$CompName = "" 
$SelectedGroups = $listView1.SelectedItems.Text 
$Error.Clear()

Try {$Name = Get-ADComputer $TextBox1.Text}
    Catch {}

Try {if ($Error[0] -ne "") {$Name = Get-ADUser $TextBox1.Text -EA}}
    Catch {}

    


if ($Name -ne "")
   {

   foreach ($SelectedGroup in $SelectedGroups)
    {
        $Error.Clear()
        #Write-Host $SelectedGroup 
        $Members = Get-ADGroupMember -identity $SelectedGroup
        $Identity = get-ADGroup $SelectedGroup
        
        if ($Members.name -notcontains $TextBox1.Text -and $Members.SamAccountName -notcontains $TextBox1.Text)
            {        
        
            $Result = Add-ADGroupMember -identity $Identity.DistinguishedName -members $Name.DistinguishedName 
            if ($error[0] -gt "")
               {
                    $TextBox2.AppendText("Unable to add " + $TextBox1.Text + " to " + $SelectedGroup + "`r`n")
                    $TextBox2.AppendText("Error message " + $Error[0] + "`r`n")
                    $error.Clear()
               }
            else
               {
                    $TextBox2.AppendText("Successfully added " + $TextBox1.Text + " to " + $SelectedGroup + "`r`n")
               }
    }
    else
    {
    $TextBox2.AppendText($TextBox1.Text + " is already a member of " + $SelectedGroup + "`r`n")
    }

   }

   }
   else
   {
   $TextBox2.AppendText("Unable to find device " + $Name + "!!!`r`n")
   } 



}

function Button2MouseClick($object)
{
    Set-ItemProperty -path HKCU:\Software\MultiGroupAddTool -Name SearchBase -Value $TextBox3.text
    $SearchBase= $TextBox3.Text 
    Try {$Groups = get-adgroup -filter * -searchbase $SearchBase | sort Name}
        Catch
            {
                $TextBox2.AppendText("Unable to get AD group list" + "`r`n")
                $TextBox2.AppendText("Error message " + $Error[0] + "`r`n")
                $error.Clear()
            }
    
    $listView1.Items.Clear()
    
    foreach ($Group in $Groups)
       {
           $Line = New-Object System.Windows.Forms.ListViewItem($Group.Name)
           $ListView1.Items.Add($Line)
       }          


}

function Button3MouseClick($object)
{
    #$listView1.Items.Clear()
    $TextBox1.Clear()
    $Label3.Text = ""		
}

function Button4MouseClick($object)
{
   $Groups = ""
   $Form1.Dispose()  
}


function Label1Click( $object ){

}

    #$TextBox2.AppendText("Preparing AD group list..." + "`r`n")

        
    if ((Test-Path -Path HKCU:\Software\MultiGroupAddTool) -eq $false)
        {
        New-Item -path HKCU:\Software\MultiGroupAddTool
        New-ItemProperty -path HKCU:\Software\MultiGroupAddTool -Name SearchBase
        $SearchBase = ""
        }
    else
    {
                $Reg = Get-ItemProperty -path HKCU:\Software\MultiGroupAddTool -Name SearchBase
                $TextBox3.text = $Reg.SearchBase
                $SearchBase = $Reg.SearchBase
    }

    if ($SearchBase -eq "") 
        {
        [System.Windows.MessageBox]::Show("Please add AD group location")
         #Main
        }

    # **** Change the -searchbase entry below to point at the Active Directory OU containing Application Deployment Groups ****
    #$Groups = get-adgroup -filter * -searchbase "OU=Application Catalogue, OU=Software, OU=Groups, OU=Client, DC=Arqiva, DC=Local" | sort Name
    if ($SearchBase -ne "")
    {
    $Groups = get-adgroup -filter * -searchbase $SearchBase | sort Name
        if ($error[0] -gt "")
            {
                $TextBox2.AppendText("Unable to get AD group list" + "`r`n")
                $TextBox2.AppendText("Error message " + $Error[0] + "`r`n")
                $error.Clear()
            }
        else
            {
                #$TextBox2.AppendText("Successfully added " + $CompName + " to " + $SelectedGroup + "`r`n") 
            }
    }


Main #This call must remain below all other event functions

#endregion