HaloPSA: The Missing Manual Series – Volume II
Introduction
Back in May I dropped a hot piece of PowerShell in your laps with Part I, which covered some advanced Azure automation techniques with HaloPSA to create on-premises AD user accounts directly from Halo.
Today I’m back to cover Azure AD user creation automation. This is surprisingly a bit trickier than on-premises user creation because you need to also manage authentication against a few different services like MSOL and ExchangeOnline instead of just piping a script down to a server in your AD environment.
HaloPSA gives you some kickass automation tools out of the box. The way the platform is designed to usually give you more than one way to achieve an objective is really demonstrative of next generation software design. This is especially apparent when you get to the Azure Automation Runbook and Webhook integrations. I’ll be honest when I say I’m not a huge fan of how they’re implementing the baked-in New Azure User functionality, but the fact that I can work around it easily with multiple different methods is why this platform outvalues any other PSA available. I’ve touched on the lack of advanced documentation for HaloPSA in previous posts, but since that time Tim has gone on the record that they are actively working on that problem. +1 to Tim and the Halo crew, and now I kind of feel like a dick. Sorry, Tim. I hope all the folks at Halo see these articles for what they really are: a love letter to an amazing product.
The Ask
- Automated or one-click provisioning of users in Azure AD from HaloPSA
- License the user**
- Ability to add users to groups
- Ability to manipulate ExchangeOnline properties
Prerequisites
- Read Part I and completely understand what is going on. If you don’t completely understand what is going on, you may or may not have a bad time.
- An Azure Subscription in the destination tenant
- Create or already have an Azure Automation Account
- Your Automation Account needs to be set up with an Azure Run As Account
- An Azure Key Vault (make sure you grant your Automation Run As Account service principal access to read this vault)
- My Halo_ServiceDesk Powershell API module uploaded as a PowerShell 5.1 Module to your Automation Account.
- The ExchangeOnlineManagement, AZ.Accounts, AZ.Automation, AZ.Keyvault and AzureADPreview PowerShell 5.1 modules need to be added to your Automation Account from the gallery.
This actually sounds more complicated than the last one. Can you ELI5?
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.
Halo_ServiceDesk module – A Powershell module I’ve written to wrap basic interactions with the Halo API for easy pipeline commands in automation tasks.
Why the asterisk for the license assignment?
Listen, every bad onboarding script in the world attempts to pull product IDs from MSonline and assign a license manually. That is an exceptionally over complicated and error prone approach and I don’t condone that.
You need to be using group based assignment of licenses in Azure AD. If you don’t have it configured, do that now. It is monumentally simpler for both your workflow and automation to simply assign a license to a group in Azure AD and then let automation assign new users to those groups.
“bUt i hAvE mY rEaSoNs!!!11!” or “wE’vE aLwAyS dOnE iT tHiS wAy!!”
No. Stop it. We don’t do that here. If you want to suffer and be miserable go read any three threads on r/sysadmin instead.
A Note about NCE
Microsoft’s New Commerce Experience changes the way licensing and commits work for your clients, leaving you on the hook as the MSP if things go south.
I highly recommend you do not automate the purchase of licenses or creation of users without a check step or internal approval process. There was a time we could increment and decrement license counts to compensate for errors with no strings attached. Those times are gone. You need to have a well structured license approval and purchase process.
This tutorial assumes there is a license purchase/provision step before the script is triggered. Personally, I trigger the automation with an approval in HaloPSA. This way someone is putting eyes on the onboarding request and only purchasing a license if necessary. Feel free to work that out any way you like, just know that purchasing a license is not part of this script and never will be.
Step 1: Give your Azure Run As Account the proper permissions
The great thing about Azure Run As accounts is that they can only be invoked from an automation account and they are service principals. This makes them much less susceptible to compromise or abuse. To do what we need for user creation we need to give the Run As account the ability to work with services inside Azure.
The Easy Way: Go to Azure AD -> Roles -> Global Administrator -> Add your Azure Run As Account (the object ID can be found in the Automation Account)
The Better Way: Add roles to your Run As Account as you need them. For this tutorial you could get away with only granting “User Administrator” and “Exchange Administrator” roles to your Run As Account.
If both of those options are too spicy for you, you could refactor this script to use the Secure Application Model and granular API permissions. That’s outside the scope of this tutorial but totally possible. For simplicity of demonstration I’m going to use roles.
Step 2: Upload and configure your runbook
<#
.NOTES
===========================================================================
Created on: 7/2/2022 13:38
Created by: The MSP Automator
Organization: MSPAutomator
Filename: New-AZADUser.ps1
===========================================================================
.DESCRIPTION
Creates a new Azure AD user.
#>
param ([Parameter (Mandatory = $false)]
[object]$WebHookData
)
$data = ConvertFrom-Json -InputObject $WebHookData.RequestBody
$FirstName = $data[0].Content.firstname
$LastName = $data[0].Content.lastname
$Zoom = $data[0].Content.Zoom
$Slack = $data[0].Content.Slack
$UserType = $data[0].Content.UserType
$TicketID = $data[0].Content.TicketID
Import-Module Halo_ServiceDesk
Import-Module Az.Accounts
Import-Module Az.Automation
Import-Module Az.KeyVault
Import-Module ExchangeOnlineManagement
Import-Module AzureADPreview
$VaultName = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
$HaloPSAClientIDName = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
$HaloPSASecretName = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
$HaloUrl = "https://YOURDOMAIN.halopsa.com"
$RawUser = 'user@yourdomain.com'
function New-HaloPSAConnection
{
Write-Host "Got the AZ context info, retrieving keyvault data..."
try
{
$ClientID = Get-AzKeyVaultSecret -vaultname $VaultName -Name $HaloPSAClientIDName -AsPlainText -ea Stop
Write-Output "Successfully retrieved Client ID."
}
catch
{
Write-Error $_.Exception.Message
}
try
{
$ClientSecret = Get-AzKeyVaultSecret -vaultname $VaultName -Name $HaloPSASecretName -AsPlainText -ea stop
Write-Output "Successfully retrieved Client Secret."
}
catch
{
Write-Error $_.Exception.Message
}
try
{
Connect-HaloPSA -ClientID $ClientID -ClientSecret $ClientSecret -HaloUrl $HaloUrl -RawUser $RawUser -ea stop
}
catch
{
Write-Error $_.Exception.Message
}
}
function Get-RandomPassword
{
$rand = Get-Random -Minimum 100 -Maximum 999
$substr1 = $FirstName.Substring(0, 1)
$substr1 = $substr1.ToUpper()
$substr2 = $LastName.Substring(0, 1)
$substr2 = $substr2.ToLower()
$randPass = $substr1 + $substr2 + 'c0nsu!t' + $rand
return $randPass
}
$displayname = $firstname + " " + $lastname
$password = Get-RandomPassword
$Note = "Users first password set to: $password"
$PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile
$PasswordProfile.password = "\<$password\>"
$PasswordProfile.ForceChangePasswordNextLogin = $true
$userName = $FirstName + "." + $LastName
$upn = $userName + '@YOURDOMAIN.COM'
$tenantName = "DOMAIN.onmicrosoft.com"
$RunAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection"
#Get the Azure context info
try
{
Connect-AzAccount `
-ServicePrincipal `
-Tenant $RunAsConnection.TenantId `
-ApplicationId $RunAsConnection.ApplicationId `
-CertificateThumbprint $RunAsConnection.CertificateThumbprint | Write-Verbose
Set-AzContext -Subscription $RunAsConnection.SubscriptionID | Write-Verbose
}
catch
{
Write-Error $_.Exception.Message
}
# Initialize connection to HaloPSA
try
{
New-HaloPSAConnection
}
catch
{
Write-Error $_.Exception.Message
}
# Connect AzureAd
Connect-AzureAD -TenantId $RunAsConnection.TenantId -ApplicationId $RunAsConnection.ApplicationID -CertificateThumbprint $RunAsConnection.CertificateThumbprint
# Connect to ExchangeOnline
Connect-ExchangeOnline -CertificateThumbprint $RunAsConnection.CertificateThumbprint -AppId $RunAsConnection.ApplicationID -Organization $tenantName
#Create User
try
{
New-AzureADUser -DisplayName $displayname -UserPrincipalName $upn -GivenName $firstname -Surname $lastname -PasswordProfile $PasswordProfile -MailNickname $userName -AccountEnabled $true
Create-HaloPrivateNote -TicketID $TicketID -Note $Note
}
catch
{
Write-Output "New user request failed"
}
Write-Host "User add successful, trying for group adds next..."
sleep 300 #Wait for provisioning
# Get user ObjectID
$userObjID = Get-AzureAdUser -ObjectId "$upn"
if ($UserType -match 'Employee')
{
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #Business Premium Group (assigns all licenses)
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #All Company
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #Zoom Enabled
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #Slack Enabled
Create-HaloPrivateNote -TicketID $TicketID -Note "SharePoint and SSO Groups have been added successfully"
}
else
{
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #Business Basic Group (assigns licenses)
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #Contractors
if ($Zoom -match 'true')
{
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #Zoom Enabled
}
if ($Slack -match 'true')
{
Add-AzureADGroupMember -ObjectId 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -RefObjectId $userObjID.ObjectID #Slack Enabled
}
Create-HaloPrivateNote -TicketID $TicketID -Note "SharePoint and SSO Groups have been added successfully"
}
sleep 1200 #wait 20 minutes for license and mailbox to populate
try
{
if ($UserType -match 'Employee')
{
Add-DistributionGroupMember -Identity 'Employees@YOURDOMAIN.COM' -Member $upn -BypassSecurityGroupManagerCheck #Employees
}
Add-DistributionGroupMember -Identity 'ALL@YOURDOMAIN.COM' -Member $upn -BypassSecurityGroupManagerCheck #Notion All
Add-MailboxFolderPermission -Identity "'$upn':\Calendar" -User 'YOURUSER@YOURDOMAIN.COM' -AccessRights Editor -SharingPermissionFlags none
Create-HaloPrivateNote -TicketID $TicketID -Note "Distribution Groups and Calendar Permissions have been set successfully."
}
catch
{
Write-Host $_.Exception.Message
Create-HaloPrivateNote -TicketID $TicketID -Note "Something went wrong with provisioning Distribution Group Memberships or Calendar Permissions. Please verify manually."
}
Read through this script carefully and input the following fields:
- $VaultName – Set this to the name of your Azure Key Vault
- $HaloPSAClientIDName – This is the name of the ClientID secret you created in the key vault earlier.
- $HaloPSASecretName – This is the name of the ClientSecret secret you created in the key vault earlier.
- $HaloUrl – The domain your agents use to access HaloPSA.
- $RawUser – 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.
- $UserType – Demonstrates how to specify different procedures based on user type. Your client may have contractors and employees who have different needs, for instance.
- $Zoom and $Slack – Demonstrate how to provision access to SaaS apps automatically by passing a true/false from HaloPSA checkbox field.
- Line 103 – Needs to be updated to reflect your desired UPN.
- Line 105 – The onmicrosoft domain for the tenant.
- Line 109 – The OU path where you want the user created.
- Line 160-182 – Demonstration/example of how to add to Azure AD groups based on $UserType. In this example each group does something like assign a license or provision access to a SaaS app.
- Line 184 – This script takes about 25 minutes to finish running. 20 minutes of that is spent sleeping to ensure the mailbox is provisioned for the user after they are assigned to a group that provides a license.
- Line 186-203: Demonstration of adding calendar permissions or distribution group memberships programmatically.
Step 3: Configure your webhook and test
Send your request from HaloPSA to your new webhook and crack a beer. We did it, Reddit.
Conclusion? Already?
That’s all folks. If you need help setting up webhooks or the HaloPSA side, take a gander at Part I (I told you it was important) as it lays down the foundations of the whole automation.
Thanks for the read! Please let me know in the comments what you’d like to see covered next.