MSP Automator

Technical and Operational Content for MSPs and IT – No ads, no sponsors, no bullshit.

HaloPSA – AD User Creation with Azure Automation

HaloPSA: The Missing Manual Series – Volume I

Introduction

You know what I love? Automation. You know what I hate? Shitty documentation. I’m a huge fan of HaloPSA. I’m not a huge fan of HaloPSA’s (lack of) advanced documentation. At my day job we recently made the switch and I’m learning to cope with the newfound power. To be fair, once you get the feel for how Halo works it becomes pretty easy to bend it to your will. Getting to that point is a bit of an experience.

My pain is your gain, I’m here to fill in the blanks and give you an introduction to the less documented capabilities of HaloPSA. This is the first in a series of articles where I’ll take you under the hood to build some really cool shit.

Automating onboarding is one of the most essential tasks you can tackle at an MSP. Manually provisioning user accounts doesn’t scale well and as your clients grow, you can’t afford to have provisioning mistakes. Likewise, you can’t expect technicians to remember each individual step or adhere to documentation. This is a job for a cold, calculating, computer.

HaloPSA gives you some kickass automation tools out of the box. Their Integrator app could be run to make AD accounts but it’s bulky and not scalable. Additionally, if you were to use the “Create Azure User” functionality currently available in HaloPSA beta, you’d have to capture way too many fields in your onboarding ticket to explain easily to clients. Luckily with some Azure magic and creativity we can make a full on-premise hybrid user account with just two fields: FirstName and LastName. Buckle up, this will be a doozy.

The Ask

  1. Automated or one-click provisioning of users in on-premise AD from HaloPSA.
  2. Ability to set up local profile folder permissions
  3. Ability to add to groups
  4. Ability to return the data to the onboarding ticket in HaloPSA

Prerequisites

  1. An Azure Subscription in the destination tenant
  2. Create or already have an Azure Automation Account
  3. Your Automation Account needs to be set up with an Azure Run As Account
  4. An Azure Hybrid Worker (I prefer using the Azure ARC-Enabled Server method)
  5. An Azure Key Vault (make sure you grant your Automation Run As Account service principal access to read this vault)
  6. My Halo_ServiceDesk Powershell API module installed on the hybrid worker

This all sounds really complicated, can you ELI5?

Azure Arc – A free agent based install that allows your on-premise infrastructure to both be managed in Azure *and* behave as though it resides in Azure. Why is this a big deal to us? Because the Azure Arc agent also has the Hybrid Worker Agent built in, we can create runbooks that execute code both on-premises and in Azure from inside the same script.

Azure Automation Accounts – Allow you to run Powershell or Python scripts on a schedule or via a trigger like a webhook. Azure Automation also allows you to securely store credentials and run those scripts in the cloud, or on-premises, or both without paying for a whole VM to run them at your whim. Automation accounts bring the full strength of Powershell to the cloud with great flexibility.

Azure Run As Account – Allows your Automation Account to have its own managed app identity in Azure AD. This is useful for assigning it permissions to perform tasks while observing the principal of least privilege.

Azure Key Vault – A place to securely store and retrieve credentials in Azure. We will use this to store our HaloPSA secrets and retrieve them securely on the fly.

Hybrid Worker – A server or VM that resides inside the perimeter of your on-premises network and runs the Azure Arc agent.

Halo_ServiceDesk module – A Powershell module I’ve written to wrap basic interactions with the Halo API for easy pipeline commands in automation tasks.

Step 1: Configure your Halo API Endpoint

First, you’re going to want to navigate to Settings -> Integrations -> HaloPSA API -> Applications -> New Application. If you’re using my Halo_ServiceDesk module you’ll select “Username & Password” and note the ClientID you’re given.

On the Permissions tab, we’ll want to select “read:tickets” and “edit:tickets”

Lets upload those credentials to the Azure Key Vault now, too. In your Key Vault, add a Secret and use the Manual option. We need to add the ClientID and the Password of the user we’ll be writing back as should be added as the ClientSecret.

Step 2: Prepare Hybrid Worker

Assuming you’ve already installed the Arc or Hybrid Worker agent and have a Hybrid Worker Group set up (see tutorial in prerequisites if you don’t), we need to prep your Hybrid Worker to be able to run commands both locally and in Azure.

The following script should be imported as a Powershell 5.1 runbook in your new automation account:

<#
.SYNOPSIS
Exports the Run As certificate from an Azure Automation account to a hybrid worker in that account.

.DESCRIPTION
This runbook exports the Run As certificate from an Azure Automation account to a hybrid worker in that account. Run this runbook on the hybrid worker where you want the certificate installed. This allows the use of the AzureRunAsConnection to authenticate to Azure and manage Azure resources from runbooks running on the hybrid worker.

.NOTES
LASTEDIT: 5/21/2022 15:46
#>

#Check to make sure the Hybrid worker has nuget installed
If (-not (Get-InstalledModule PowerShellGet -ErrorAction silentlycontinue))
{
	Try
	{
		Install-PackageProvider NuGet -Force | Out-Null
		Set-PSRepository PSGallery -InstallationPolicy Trusted
		Install-Module PowerShellGet -Force
	}
	Catch
	{
		Write-Host "There was an error installing NuGet or PowerShellGet."
		Exit 1
	}
}
#check for Halo_ServiceDesk module
If (-not (Get-InstalledModule Halo_ServiceDesk -ErrorAction silentlycontinue))
{
	Try
	{
		Write-Host "Starting Halo_ServiceDesk install"
		New-Item -ItemType Directory -Name "Halo_ServiceDesk" -Path "$env:ProgramFiles\PowerShell\Modules"
		
		$Url1 = 'https://github.com/mspautomator/Halo_ServiceDesk/blob/main/Halo_ServiceDesk.psm1'
		$Url2 = 'https://github.com/mspautomator/Halo_ServiceDesk/blob/main/Halo_ServiceDesk.psd1'
		$File1 = "$env:ProgramFiles\PowerShell\Modules\Halo_ServiceDesk" + $(Split-Path -Path $Url1 -Leaf)
		$File2 = "$env:ProgramFiles\PowerShell\Modules\Halo_ServiceDesk" + $(Split-Path -Path $Url2 -Leaf)
		Invoke-WebRequest -Uri $Url1 -OutFile $File1 -ErrorAction Stop
		Invoke-WebRequest -Uri $Url2 -OutFile $File2 -ErrorAction Stop
	}
	Catch
	{
		Write-Host "There was an error installing Halo_ServiceDesk."
		Exit 1
	}
}
#check for AZ Keyvault module
If (-not (Get-InstalledModule Az.KeyVault -ErrorAction silentlycontinue))
{
	Try
	{
		Write-Host "Starting az keyvault module install"
		Install-Module Az.KeyVault -Force 
	}
	Catch
	{
		Write-Host "There was an error installing Az keyvault."
		Exit 1
	}
}
#check for AZ Accounts module
If (-not (Get-InstalledModule Az.Accounts -ErrorAction silentlycontinue))
{
	Try
	{
		Write-Host "Starting az accounts module install"
		Install-Module Az.Accounts -Force 
	}
	Catch
	{
		Write-Host "There was an error installing Az accounts."
		Exit 1
	}
}
#check for AZ Automation module
If (-not (Get-InstalledModule Az.Automation -ErrorAction silentlycontinue))
{
	Try
	{
		Write-Host "Starting az module install"
		Install-Module Az.Automation -Force 
	}
	Catch
	{
		Write-Host "There was an error installing Az automation."
		Exit 1
	}
}

Import-Module Az.Accounts
Import-Module Az.Automation
Import-Module Az.KeyVault

# Generate the password used for this certificate
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue | Out-Null
$Password = [System.Web.Security.Membership]::GeneratePassword(25, 10)

# Stop on errors
$ErrorActionPreference = 'stop'

# Get the management certificate that will be used to make calls into Azure Service Management resources
$RunAsCert = Get-AutomationCertificate -Name "AzureRunAsCertificate"

# location to store temporary certificate in the Automation service host
$CertPath = Join-Path $env:temp  "AzureRunAsCertificate.pfx"

# Save the certificate
$Cert = $RunAsCert.Export("pfx",$Password)
Set-Content -Value $Cert -Path $CertPath -Force -Encoding Byte | Write-Verbose

Write-Output ("Importing certificate into $env:computername local machine root store from " + $CertPath)
$SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force
Import-PfxCertificate -FilePath $CertPath -CertStoreLocation Cert:\LocalMachine\My -Password $SecurePassword | Write-Verbose

Remove-Item -Path $CertPath -ErrorAction SilentlyContinue | Out-Null

# Test to see if authentication to Azure Resource Manager is working
$RunAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection"

Connect-AzAccount `
    -ServicePrincipal `
    -Tenant $RunAsConnection.TenantId `
    -ApplicationId $RunAsConnection.ApplicationId `
    -CertificateThumbprint $RunAsConnection.CertificateThumbprint | Write-Verbose

Set-AzContext -Subscription $RunAsConnection.SubscriptionID | Write-Verbose

# List automation accounts to confirm that Azure Resource Manager calls are working
Get-AzAutomationAccount | Select-Object AutomationAccountName

After you import this runbook, you need to run it against your Hybrid Worker Group. This will do the following:

  1. Installs the Halo_ServiceDesk and required Az modules necessary for the onboarding script to work
  2. Add the Azure Run As account certificate to the machine certificate store so the Hybrid Worker can authenticate against Azure without interaction. This allows your Hybrid Worker to run commands like Connect-AzAccount and perform tasks as the automation account.

Step 3: Upload and Configure the Onboarding Runbook

Now for the sauce. The following script should be adapted for your use:

<#	
	.NOTES
	===========================================================================
	 Created on:   	5/7/2022 19:00
	 Created by:   	The MSP Automator
	 Organization: 	MSPAutomator.com
	 Filename:     	New-ADUser.ps1
	===========================================================================
	.DESCRIPTION
		Powershell 5.1 runbook to receive a webhook from HaloPSA and create an on-premise user in AD via hybrid worker
#>

param ([Parameter (Mandatory = $false)]
	[object]$WebHookData
)


function New-HaloPSAConnection
{
	
	$RunAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection"
	
	Connect-AzAccount `
					  -ServicePrincipal `
					  -Tenant $RunAsConnection.TenantId `
					  -ApplicationId $RunAsConnection.ApplicationId `
					  -CertificateThumbprint $RunAsConnection.CertificateThumbprint | Write-Verbose
	
	Set-AzContext -Subscription $RunAsConnection.SubscriptionID | Write-Verbose
	
	Write-Host "Got the AZ context info, retrieving keyvault data..."
	
	$ClientID = Get-AzKeyVaultSecret -vaultname $VaultName -Name $HaloPSAClientIDName -AsPlainText
	Write-Output "Successfully retrieved Client ID."
	
	$ClientSecret = Get-AzKeyVaultSecret -vaultname $VaultName -Name $HaloPSASecretName -AsPlainText
	
	Write-Output "Successfully retrieved Client Secret."
	
	try
	{
		Connect-HaloPSA -ClientID $ClientID -ClientSecret $ClientSecret -HaloUrl $HaloUrl -RawUser $HaloUser
	}
	catch
	{
		Write-Error $_.Exception.Message
	}
	
	
}

$Global:VaultName = "XXXXXXXXXXXXXX"
$Global:HaloPSAClientIDName = "XXXXXXXXXXXXXXXXXXXX"
$Global:HaloPSASecretName = "XXXXXXXXXXXXXXXXXXX"
$Global:HaloUrl = "https://YOURDOMAIN.halopsa.com or https://SUBDOMAIN.YOURDOMAIN.COM"
$Global:HaloUser = "Your HaloPSA Display Name"

Import-Module Halo_ServiceDesk
Import-Module Az.Accounts
Import-Module Az.Automation
Import-Module Az.KeyVault
	
$data = ConvertFrom-Json -InputObject $WebHookData.RequestBody
	
try
{
	New-HaloPSAConnection
}
catch
{
	Write-Error $_.Exception.Message
}
		
$FirstName = $data[0].Content.firstname
$LastName = $data[0].Content.lastname
$TicketID = $data[0].Content.TicketID
		
$userName = $FirstName.Substring(0, 1) + $LastName
		
$mail = $userName + '@YOURDOMAIN.com'
		
$profileFolder = "\\yourdomain\yourshare\" + "$userName"

$rand = Get-Random -Minimum 1000 -Maximum 9999

$substr1 = $FirstName.Substring(0, 1)
$substr1 = $substr1.ToUpper()

$substr2 = $LastName.Substring(0, 1)
$substr2 = $substr2.ToLower()

$Password = $substr1 + $substr2 + 'auT0M4te!' + $rand
		
$Note = "Users first password set to: $password"
		
$secPw = ConvertTo-SecureString -String $password -AsPlainText -Force
		
$NewUserParameters = @{
	GivenName = $FirstName
	Surname   = $LastName
	Name	  = "$FirstName" + " " + "$LastName"
	DisplayName = "$FirstName" + " " + "$LastName"
	AccountPassword = $secPw
	EMail	  = $mail
	PasswordNeverExpires = $true
	SAMaccountName = $userName
	UserPrincipalName = $mail
	Enabled   = $true
	Path	  = "OU=XXXXXX,OU=XXXXXX,DC=YOURDOMAIN,DC=com"
}
		
Write-Host "Parameters loaded, attempting to create user"
		
Create-HaloPrivateNote -TicketID $TicketID -Note $Note #WRITE THE PASSWORD BACK TO HALO AS A PRIVATE NOTE

#START PROCESSING AD TASKS
try
{
	New-AdUser @NewUserParameters
}
catch
{
	Write-Error $_.Exception.Message
}

try
{
	#EXAMPLE GROUPS
	Add-AdGroupMember -Identity 'Business Premium' -Members $userName
	Add-AdGroupMember -Identity 'AVD Enabled' -Members $userName
}
catch
{
	Write-Error $_.Exception.Message
}

#START PROCESSING FILESHARE TASKS
try
{
	
	New-Item -Path $profileFolder -type directory -Force
	Write-Host "Profile folder creation successful"
}
catch
{
	Write-Error $_.Exception.Message
}

Write-Host "Setting profile folder permissions..."

try
{
	
	$acl = Get-Acl -Path $profileFolder
	$accessrule = New-Object System.Security.AccessControl.FileSystemAccessRule ("YOURDOMAIN\$userName", "FullControl", "ContainerInherit, ObjectInherit", "InheritOnly", "Allow")
	$acl.SetAccessRule($accessrule)
	$acl | Set-Acl $profileFolder
	
	Write-Host "Profile permissions set successfully"
}
catch
{
	Write-Error $_.Exception.Message
}

Take care to ensure you catch every variable that needs to be defined in this file. You can remove the functions that you don’t want to use, or add your own.

  1. $Global:VaultName – Set this to the name of your Azure Key Vault
  2. $Global:HaloPSAClientIDName – This is the name of the ClientID secret you created in the key vault earlier.
  3. $Global:HaloPSASecretName – This is the name of the ClientSecret secret you created in the key vault earlier.
  4. $Global:HaloUrl – The domain your agents use to access HaloPSA.
  5. $Global:HaloUser – The DISPLAY NAME of the Agent you want to write back as (corresponds to the password in ClientSecret). This is not a user@domain.com format but rather a “John Smith” format.
  6. Line 80 – Needs to be updated to reflect your desired UPN.
  7. Line 82 – If you’re creating a user profile folder, set this to your share root for the profiles.
  8. Line 109 – The OU path where you want the user created.
  9. Line 129 and 130 – Demonstration/example of how to add to on-premises groups. I recommend you use a group that syncs forward via Azure AD Connect to automatically license your users. Creating a group, waiting for it to sync to Azure AD, then correlating a license to that group will result in a user being automatically licensed once they sync from on-premises.
  10. Line 141 – Demonstration of creating a directory
  11. Line 154-157: Demonstration of adding custom permissions to folders. Note the YOURDOMAIN needs to be updated to your domain prefix if you intend to use this functionality.

Step 4: Create the Webhook in Azure

Next we need to establish a way to send data back and forth. Let’s start with Azure, where we need to add a webhook to our new Runbook:

Go to your automation account -> your onboarding runbook -> webhooks and create new.

Note the URL as we will need this later.
Be sure to select “Run on Hybrid Worker” and select your Hybrid Worker group.

Now for the HaloPSA side. You can use either an Azure Automation or a Webhook integration in HaloPSA. You can find these in Settings -> Integrations -> Azure Automations.

Be sure to build your table on the bottom to match the image

You’ll want to paste the Azure webhook URL from the last step here and add three attributes to the request attributes: CFFirstName, CFLastName, and ID. These will be the items HaloPSA sends to Azure from your onboarding request ticket.

Testing the Automation

Once we’re done we can hit “Send Webhook” and put in a TicketID from an onboarding request to test. If you’ve got everything in order you should receive a 202 response and see a payload like this:

A correctly formed request to our webhook in Azure

And if we check Azure we should see the following output

Successful runbook execution, the unapproved verbs warning is normal

And if we check HaloPSA we see:

Private note being posted back to HaloPSA with the temporary password the script generated

Last but not least, let’s see what happened in AD:

Conclusion

This concludes part I of the series. In part II we will architect a runbook to create AzureAD users and manipulate them with things like the ExchangeOnlineManagement and AzureAD modules. We’ll also cover some more advanced workflow and automation triggers inside HaloPSA itself.

Comments (

)