PowerShell Onboarding Scripts in a Hybrid AD-Azure Environment

Title: PowerShell Onboarding Scripts in a Hybrid AD-Azure Environment
Date: 2024-10-27
Tags: IT Support, Automation, Active Directory, PowerShell, Azure AD, Onboarding

I. Introduction

1.1 Context & Purpose
  • Manual onboarding for new hires (creating AD accounts, assigning licenses, setting up home drives) was error-prone and took ~45 minutes per user.
  • Developed a PowerShell script to automate 90% of the process, ensuring consistency and freeing up L2/L3 time for more complex tasks.
1.2 What This Covers
  • The logic and structure of the main automation script.
  • Integration with on-prem Active Directory and Azure AD Connect.
  • Error handling and logging for support team visibility.

II. Setup & Environment

2.1 Network & Tools Overview
  • Domain Controller: Windows Server 2019 (On-prem)
  • Sync Tool: Azure AD Connect (v2.x)
  • Client: Administrative workstation with RSAT and AzureAD module.
  • Script Location: \\fileserver\IT\Scripts\Automation\Onboarding
    Pasted image 20260104202623.png
2.2 Prerequisites / Preparations
  • Pre-populated CSV template from HR (newhires_YYYYMMDD.csv) in a secure share.
  • Pre-defined AD security groups (Staff-Office, Staff-Remote, Department-*).
  • Available Microsoft 365 licenses in the tenant.

III. Execution & Findings

3.1 Steps Taken
  1. Script Trigger: Manual run from secure IT workstation after HR notification.
    .\Start-NewHireOnboarding.ps1 -CsvPath "\\hrserver\secure\newhires_20241027.csv"
    
  2. Core Script Logic (Abridged):
    # 1. Import CSV and validate data
    $NewHires = Import-Csv $CsvPath
    foreach ($User in $NewHires) {
        # 2. Generate username (first.last) and check for duplicates
        $SamAccountName = "$($User.FirstName).$($User.LastName)".ToLower()
        
        # 3. Create AD User Account
        $Password = ConvertTo-SecureString -String (New-ComplexPassword) -AsPlainText -Force
        New-ADUser -Name "$($User.FirstName) $($User.LastName)" `
                   -SamAccountName $SamAccountName `
                   -UserPrincipalName "$SamAccountName@corp.example.com" `
                   -AccountPassword $Password `
                   -Enabled $true `
                   -Path "OU=Users,DC=corp,DC=example,DC=com" `
                   -OtherAttributes @{title=$User.JobTitle; department=$User.Department}
        
        # 4. Add to Security Groups based on Department/Location
        Add-ADGroupMember -Identity "Department-$($User.Department)" -Members $SamAccountName
        Add-ADGroupMember -Identity "Location-$($User.Location)" -Members $SamAccountName
        
        # 5. Create Home Drive and set permissions
        $HomeDrivePath = "\\fileserver\homes\$SamAccountName"
        New-Item -Path $HomeDrivePath -ItemType Directory
        icacls $HomeDrivePath /grant "$($env:USERDOMAIN)\$SamAccountName`:(OI)(CI)F"
        
        # 6. Wait for AD Connect sync, then assign M365 license
        Start-Sleep -Seconds 180
        Connect-AzureAD
        $License = Get-AzureADSubscribedSku | Where-Object {$_.SkuPartNumber -eq "ENTERPRISEPACK"}
        Set-AzureADUserLicense -ObjectId "$SamAccountName@corp.example.com" `
                               -AssignedLicenses @{SkuId = $License.SkuId}
        
        # 7. Log all actions
        Write-Log -Message "Created user: $SamAccountName" -Level INFO
    }
    
3.2 Challenges & Fixes
  • Challenge: Azure AD Connect sync delay caused "user not found" errors when trying to assign licenses immediately.
    # ERROR: Set-AzureADUserLicense : Error occurred while executing SetUserLicenses
    # Code: Request_ResourceNotFound
    
  • Fix: Added a Start-Sleep -Seconds 180 wait and implemented a retry loop that checks Azure AD for user existence before proceeding.
    $RetryCount = 0
    do {
        Start-Sleep -Seconds 30
        $AzureUser = Get-AzureADUser -Filter "userPrincipalName eq '$UPN'"
        $RetryCount++
    } while ($null -eq $AzureUser -and $RetryCount -lt 10)
    
  • Challenge: HR CSV sometimes had missing department data.
  • Fix: Added validation logic that sends an alert to the IT ticket system and skips that user, rather than failing the entire batch.
    if ([string]::IsNullOrWhiteSpace($User.Department)) {
        Write-Log -Message "Missing department for $($User.Email). Creating ticket." -Level WARN
        New-ServiceNowTicket -ShortDescription "Onboarding Data Issue" -Description "User missing department"
        continue
    }
    

IV. Observations & Insights

  • Time Saved: Reduced manual work from 45 to ~5 minutes (just verification). Batch of 10 users now takes 20 minutes instead of 7.5 hours.
  • Error Reduction: Zero typos in usernames or group assignments since implementation.
  • Unexpected Benefit: The detailed log file (Onboarding_20241027.log) became invaluable for auditing and proving compliance during internal audits.

V. Considerations & Next Steps

  1. Integrate with HR System: Next phase is to call the HRIS API directly instead of using CSV files.
  2. Self-Service Portal: Build a web front-end where managers can trigger onboarding for contractors with pre-approved templates.
  3. Offboarding Script: Build the counterpart script that disables accounts, archives data, and reclaims licenses with the same rigor.

VI. Conclusion

  • Automation in IT support isn't about replacing people; it's about eliminating repetitive, low-value tasks that are prone to human error.
  • This script paid for itself in time savings within the first month of use and significantly improved the new hire experience (everything was ready at 9 AM on day one).
  • The key to success was robust error handling and comprehensive logging—the script needed to fail gracefully and tell us exactly why.