hiddenUser_name

Untitled

Nov 5th, 2026
24
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <#
  2. This script will be setup on one or more domain controllers to be run periodically through task scheduler, although the times should be staggered by domain controller.
  3. Example:
  4. dc01 runs on the half hour
  5. dc02 runs on the hour
  6.  
  7. Things this script needs to do:
  8. determine if a new user exists in ou=rippling user temp,ou=user accounts
  9. if a user or users exist, iterate through them.
  10.  
  11. For each user:
  12. - look at the job title and office to determine if the user should be licensesd for M365 E3 or O365 f3. Add to appropriate AD groups
  13. - look at the department to assign default groups in AD
  14. - look at the department to assign default teams in Microsoft 365 (if any)
  15. - check for available licenses in Microsoft 365
  16. - move the user from ou=rippling user temp,ou=user accounts to appropriate ou based on country
  17. - email admins that a new user has been created plus any errors that occured.
  18. - Synchronize the changes between AD servers and then to the cloud
  19.  
  20. Requirements:
  21. - cert creds to connect to azure ad
  22. - any ad functions need to specify domain name so it works in testing from azure ad joined computer
  23. - activedirectory module
  24. - msgraph module
  25. #>
  26.  
  27. # Import required modules
  28. Import-Module ActiveDirectory
  29.  
  30. # Configuration
  31. $LogFile = "C:\temp\auto-usersetup-$(Get-Date -Format 'yyyyMMdd').log"
  32. $EmailTo = '[email protected]'
  33. $EmailFrom = '[email protected]'
  34. $SmtpServer = 'mail.contoso.local'
  35. $SmtpPort = 25
  36.  
  37. # Azure AD App Registration Configuration
  38. # Azure AD App Registration Configuration
  39. $TenantId = '<YOUR_TENANT_ID>'
  40. $AppId = '<YOUR_APP_REGISTRATION_CLIENT_ID>'
  41. $CertThumbprint = '<YOUR_CERT_THUMBPRINT>'
  42. $Organization = 'contoso.com'
  43.  
  44. # Initialize log collection for email
  45. $script:LogMessages = @()
  46. $script:CertWarning = $null
  47. $script:UsersProcessed = 0
  48. $script:UsersSuccessfullyProcessed = 0  # Only count users who completed all critical steps
  49. $script:UserPasswords = @()  # Store user/password pairs for summary email (only successfully processed users)
  50. $script:LmsWarnings = @()  # Track LMS API failures for email report
  51.  
  52. # Set variables
  53. $domain = "contoso.local"
  54. $ripplingTempOU = "ou=rippling user temp,ou=user accounts,ou=root-users,dc=contoso,dc=local"
  55.  
  56. try {
  57.     $passwordWriteServer = (Get-ADDomain -Server $domain -ErrorAction Stop).PDCEmulator
  58. }
  59. catch {
  60.     $passwordWriteServer = $domain
  61. }
  62.  
  63. # European country codes
  64. $EuropeanCountries = @('GB','DE','IT','FR','ES','NL','BE','AT','SE','DK','NO','FI','PL','CZ','IE','PT','CH','GR','HU','RO','BG','HR','SI','SK','LT','LV','EE','LU','MT','CY','IS','LI','MC','AD','SM','VA','ME','MK','AL','BA','RS','XK','UA','BY','MD')
  65.  
  66. # Define destination OUs based on country
  67. $DestinationOUs = @{
  68.     'US' = 'OU=US-CAN,OU=user accounts,OU=root-users,DC=contoso,DC=local'
  69.     'CA' = 'OU=US-CAN,OU=user accounts,OU=root-users,DC=contoso,DC=local'
  70.     'MX' = 'OU=US-CAN,OU=user accounts,OU=root-users,DC=contoso,DC=local'
  71.     'AU' = 'OU=Australia,OU=user accounts,OU=root-users,DC=contoso,DC=local'
  72.     'IN' = 'OU=India,OU=user accounts,OU=root-users,DC=contoso,DC=local'
  73. }
  74.  
  75. # Add European countries to UK-GB OU
  76. foreach ($euCountry in $EuropeanCountries) {
  77.     $DestinationOUs[$euCountry] = 'OU=UK-GB,OU=user accounts,OU=root-users,DC=contoso,DC=local'
  78. }
  79.  
  80. function New-TemporaryPassword {
  81.     param(
  82.         [int]$Length = 16
  83.     )
  84.  
  85.     if ($Length -lt 3) {
  86.         $Length = 3
  87.     }
  88.  
  89.     $upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'.ToCharArray()
  90.     $lower = 'abcdefghijkmnopqrstuvwxyz'.ToCharArray()
  91.     $digit = '23456789'.ToCharArray()
  92.  
  93.     # First character is uppercase. Remaining characters are lowercase/numbers.
  94.     $firstChar = ($upper | Get-Random)
  95.     $tailChars = @(
  96.         ($lower | Get-Random)
  97.         ($digit | Get-Random)
  98.     )
  99.  
  100.     $tailPool = $lower + $digit
  101.     for ($i = $tailChars.Count + 1; $i -lt $Length; $i++) {
  102.         $tailChars += ($tailPool | Get-Random)
  103.     }
  104.  
  105.     $shuffledTail = $tailChars | Sort-Object { Get-Random }
  106.     $passwordChars = @($firstChar) + $shuffledTail
  107.     return ($passwordChars -join '')
  108. }
  109.  
  110. # Define domain controllers for AD replication (currently unused - replication runs locally only)
  111. # $DomainControllers = @('dc01.contoso.local', 'dc02.contoso.local', 'dc03.contoso.local')
  112.  
  113. #set default error action to stop
  114. $ErrorActionPreference = "Stop"
  115.  
  116. # Logging function
  117. function Write-Log {
  118.     param(
  119.         [string]$Message,
  120.         [string]$Level = 'INFO'
  121.     )
  122.     $Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
  123.     $LogEntry = "[$Timestamp] [$Level] $Message"
  124.    
  125.     # Write to console
  126.     Write-Host $Message
  127.    
  128.     # Write to log file
  129.     Add-Content -Path $LogFile -Value $LogEntry
  130.    
  131.     # Add to email collection
  132.     $script:LogMessages += $LogEntry
  133. }
  134.  
  135. # Helper function to add user to AD group, suppressing "already a member" error
  136. function Add-ADGroupMemberSafe {
  137.     [CmdletBinding()]
  138.     param(
  139.         [Parameter(ValueFromPipeline=$true)]
  140.         $InputObject,
  141.  
  142.         [Parameter(Mandatory=$false)]
  143.         $Identity,
  144.        
  145.         [Parameter(Mandatory=$true)]
  146.         $Members,
  147.        
  148.         [Parameter(Mandatory=$true)]
  149.         [string]$Server
  150.     )
  151.    
  152.     process {
  153.         $targetIdentity = if ($PSBoundParameters.ContainsKey('Identity')) {
  154.             $Identity
  155.         }
  156.         else {
  157.             $InputObject
  158.         }
  159.  
  160.         if ($null -eq $targetIdentity -or ($targetIdentity -is [string] -and [string]::IsNullOrWhiteSpace($targetIdentity))) {
  161.             throw "Add-ADGroupMemberSafe requires -Identity or pipeline input."
  162.         }
  163.  
  164.         try {
  165.             Add-ADGroupMember -Identity $targetIdentity -Members $Members -Server $Server -ErrorAction Stop
  166.         }
  167.         catch {
  168.             # Check if error is "already a member" - if so, silently continue
  169.             if ($_.Exception.Message -like "*already a member of the group*" -or
  170.                 $_.Exception.Message -like "One or more added object references already exist for the following modified properties: 'members'") {
  171.                 # User is already a member, this is fine - do nothing
  172.             }
  173.             else {
  174.                 # This is a different error, re-throw it
  175.                 throw
  176.             }
  177.         }
  178.     }
  179. }
  180.  
  181. # Helper function to add user to Exchange Online distribution group, suppressing "already a member" error
  182. function Add-DistributionGroupMemberSafe {
  183.     param(
  184.         [Parameter(Mandatory=$true)]
  185.         [string]$Identity,
  186.  
  187.         [Parameter(Mandatory=$true)]
  188.         [string]$Member
  189.     )
  190.  
  191.     try {
  192.         Add-DistributionGroupMember -Identity $Identity -Member $Member -ErrorAction Stop
  193.     }
  194.     catch {
  195.         if ($_.Exception.Message -like "*already a member*") {
  196.             # User is already a member, this is fine - do nothing
  197.         }
  198.         else {
  199.             throw
  200.         }
  201.     }
  202. }
  203.  
  204. # Helper function to add user to Microsoft Graph group, suppressing "already exists" error
  205. function New-MgGroupMemberSafe {
  206.     param(
  207.         [Parameter(Mandatory=$true)]
  208.         [string]$DirectoryObjectId,
  209.        
  210.         [Parameter(Mandatory=$true)]
  211.         [string]$GroupId
  212.     )
  213.    
  214.     try {
  215.         New-MgGroupMember -DirectoryObjectId $DirectoryObjectId -GroupId $GroupId -ErrorAction Stop
  216.     }
  217.     catch {
  218.         # Check if error is "already exists"
  219.         if ($_.Exception.Message -like "*One or more added object references already exist*") {
  220.             # User is already a member, this is fine - do nothing
  221.         }
  222.         else {
  223.             # This is a different error, re-throw it
  224.             throw
  225.         }
  226.     }
  227. }
  228.  
  229. # Function to check certificate expiration
  230. function Test-CertificateExpiration {
  231.     param(
  232.         [string]$Thumbprint,
  233.         [int]$WarningDays = 30
  234.     )
  235.    
  236.     try {
  237.         $cert = Get-ChildItem -Path Cert:\LocalMachine\My\$Thumbprint -ErrorAction Stop
  238.        
  239.         $daysUntilExpiration = ($cert.NotAfter - (Get-Date)).Days
  240.        
  241.         Write-Log "Certificate expires on: $($cert.NotAfter.ToString('yyyy-MM-dd'))"
  242.         Write-Log "Days until expiration: $daysUntilExpiration"
  243.        
  244.         if ($daysUntilExpiration -le 0) {
  245.             $script:CertWarning = "CRITICAL: Certificate has EXPIRED on $($cert.NotAfter.ToString('yyyy-MM-dd'))!"
  246.             Write-Log $script:CertWarning -Level 'ERROR'
  247.         }
  248.         elseif ($daysUntilExpiration -le $WarningDays) {
  249.             $script:CertWarning = "WARNING: Certificate will expire in $daysUntilExpiration days on $($cert.NotAfter.ToString('yyyy-MM-dd'))!"
  250.             Write-Log $script:CertWarning -Level 'WARNING'
  251.         }
  252.        
  253.         return $cert
  254.     }
  255.     catch {
  256.         Write-Log "ERROR: Could not find certificate with thumbprint $Thumbprint" -Level 'ERROR'
  257.         throw
  258.     }
  259. }
  260.  
  261. Write-Log "=== Auto User Setup Script Started ==="
  262.  
  263. # Get users from rippling temp OU
  264. Write-Log "Searching for new users in Rippling temp OU..."
  265. $users = Get-ADUser -Filter * -SearchBase $ripplingTempOU -Server $domain -properties *
  266. Write-Log "Found $($users.Count) users to process"
  267.  
  268. if ($users.Count -eq 0) {
  269.     Write-Log "No users to process. Exiting."
  270.     exit 0
  271. }
  272.  
  273. # Check certificate expiration before connecting
  274. Write-Log "Checking certificate expiration..."
  275. $cert = Test-CertificateExpiration -Thumbprint $CertThumbprint -WarningDays 30
  276.  
  277. # Connect to Microsoft Graph
  278. Write-Log "Connecting to Microsoft Graph..."
  279. import-module microsoft.graph.users
  280. import-module microsoft.graph.groups
  281. import-module microsoft.graph.teams
  282. Import-Module Microsoft.Graph.Authentication
  283. Import-Module ExchangeOnlineManagement
  284.  
  285. try {
  286.     Connect-MgGraph -ClientId $AppId -TenantId $TenantId -CertificateThumbprint $CertThumbprint -NoWelcome
  287.     Write-Log "Successfully connected to Microsoft Graph"
  288. }
  289. catch {
  290.     Write-Log "ERROR connecting to Microsoft Graph: $_" -Level 'ERROR'
  291.     throw
  292. }
  293.  
  294. Write-Log "Connecting to Exchange Online..."
  295. $exchangeOnlineConnected = $false
  296. try {
  297.     Connect-ExchangeOnline -CertificateThumbprint $CertThumbprint -AppId $AppId -Organization $Organization -ShowBanner:$false -ErrorAction Stop
  298.     $exchangeOnlineConnected = $true
  299.     Write-Log "Successfully connected to Exchange Online"
  300. }
  301. catch {
  302.     Write-Log "WARNING: Failed to connect to Exchange Online: $($_.Exception.Message). Distribution group assignments will be skipped." -Level 'WARNING'
  303. }
  304.  
  305. #region LMS API
  306. # LMS API Configuration
  307. $lmsUrl = "https://lms.example.com/rest/v.01"
  308. $lmsKey = "<YOUR_LMS_CLIENT_ID>"
  309. $lmsAuth = "<YOUR_LMS_CLIENT_SECRET>"
  310.  
  311. try {
  312.     $lmsTokenHeader = @{"Content-Type" = "application/x-www-form-urlencoded"}
  313.     $lmsTokenBody = [ordered]@{
  314.         client_id     = $lmsKey
  315.         client_secret = $lmsAuth
  316.         grant_type    = "client_credentials"
  317.     }
  318.     $lmsToken = Invoke-RestMethod -Uri "$lmsUrl/token" -Method Post -Headers $lmsTokenHeader -Body $lmsTokenBody
  319.     Write-Log "Successfully connected to LMS API"
  320. }
  321. catch {
  322.     Write-Log "WARNING: Failed to connect to LMS API: $($_.Exception.Message). LMS account creation will be skipped." -Level 'WARNING'
  323.     $lmsToken = $null
  324. }
  325. #endregion
  326.  
  327. #region Licensing
  328. # get the available licenses
  329. $licenses = Get-MgSubscribedSku
  330. #important sku part numbers to group mapping:
  331. #AAD_PREMIUM = o365Lic_aadp1
  332. #SPE_E3 = o365Lic_E3_Installed Office
  333. #atp_enterprise=defender
  334. #desklesspack = o365Lic_F3_No Installed Office
  335.  
  336. #currently used sku part numbers
  337. $o365Lic_E3count=($licenses|Where-Object {$_.SkuPartNumber -eq "spe_e3"}).ConsumedUnits
  338. $o365Lic_F3count=($licenses|Where-Object {$_.SkuPartNumber -eq "desklesspack"}).ConsumedUnits
  339. $defendercount=($licenses|Where-Object {$_.SkuPartNumber -eq "atp_enterprise"}).ConsumedUnits
  340. $aadp1count=($licenses|Where-Object {$_.SkuPartNumber -eq "aad_premium"}).ConsumedUnits
  341.  
  342. #licenses owned
  343. $aadp1Owned=($licenses|Where-Object {$_.SkuPartNumber -eq "aad_premium"}).PrepaidUnits.enabled
  344. $o365Lic_E3Owned=($licenses|Where-Object {$_.SkuPartNumber -eq "spe_e3"}).PrepaidUnits.enabled
  345. $o365Lic_F3Owned=($licenses|Where-Object {$_.SkuPartNumber -eq "desklesspack"}).PrepaidUnits.enabled
  346. $defenderOwned=($licenses|Where-Object {$_.SkuPartNumber -eq "atp_enterprise"}).PrepaidUnits.enabled
  347.  
  348. #write these variables to log so we can email them in the report
  349. Write-Log "AAD P1 Owned: $aadp1Owned and Used: $aadp1count"
  350. Write-log "O365 E3 Owned: $o365Lic_E3Owned and Used: $o365Lic_E3count"
  351. Write-log "O365 F3 Owned: $o365Lic_F3Owned and Used: $o365Lic_F3count"
  352. Write-log "Defender Owned: $defenderOwned and Used: $defendercount"
  353.  
  354. #initalize variables for how many of each we are adding
  355. $e3consumed=0
  356. $f3consumed=0
  357. $defenderconsumed=0
  358. $aadp1consumed=0
  359.  
  360. #endregion
  361.  
  362. #initialize other variables
  363.  
  364.  
  365. #Process each user
  366. foreach ($user in $users) {
  367.     # setup a list variable to hold what licenses this user requires
  368.     $requiredLicenses= [System.Collections.ArrayList]::new()
  369.  
  370.     # Track that we're processing a user
  371.     $script:UsersProcessed++
  372.    
  373.     Write-Log "`n========================================"
  374.     Write-Log "Processing user: $($user.Name) ($($user.UserPrincipalName))"
  375.     Write-Log "Validating user information..."
  376.    
  377.     # Check for missing required fields
  378.     $deptValue = if ($null -eq $user.Department -or $user.Department -eq '') { "MISSING" } else { $user.Department }
  379.     $officeValue = if ($null -eq $user.Office -or $user.Office -eq '') { "MISSING" } else { $user.Office }
  380.     $countryValue = if ($null -eq $user.Country -or $user.Country -eq '') { "MISSING" } else { $user.Country }
  381.    
  382.     # Validate email format (should be [email protected])
  383.     $emailValid = $true
  384.     $emailError = ""
  385.     if ($user.UserPrincipalName -notmatch '^[a-z]+\.[a-z-0-9]+@contoso\.com$') {
  386.         $emailValid = $false
  387.         $emailError = "Email format is incorrect. Expected: [email protected], Got: $($user.UserPrincipalName)"
  388.         Write-Log "ERROR: $emailError" -Level 'ERROR'
  389.     }
  390.    
  391.     # Check if any validation failed
  392.     if($null -eq $user.Department -or $null -eq $user.Office -or $null -eq $user.Country -or
  393.        $user.Department -eq '' -or $user.Office -eq '' -or $user.Country -eq '' -or
  394.        -not $emailValid) {
  395.  
  396.         if ($null -ne $user.Department -and $null -ne $user.Office -and $null -ne $user.Country -and
  397.             $user.Department -ne '' -and $user.Office -ne '' -and $user.Country -ne '') {
  398.             # Only email issue
  399.             Write-Log "ERROR: User email format is invalid" -Level 'ERROR'
  400.         }
  401.         else {
  402.             # Field missing issue (or both)
  403.             Write-Log "ERROR: User information is incomplete - Department: $deptValue, Office: $officeValue, Country: $countryValue" -Level 'ERROR'
  404.         }
  405.        
  406.         # Email HR about incomplete user setup
  407.         try {
  408.             $emailSection = if (-not $emailValid) { "`n`nEmail Format Issue:`n$emailError" } else { "" }
  409.             $fieldSection = ""
  410.             if ($deptValue -eq "MISSING" -or $officeValue -eq "MISSING" -or $countryValue -eq "MISSING") {
  411.                 $fieldSection = @"
  412.  
  413. Missing/Invalid Fields:
  414. - Department: $deptValue
  415. - Office: $officeValue
  416. - Country: $countryValue
  417. "@
  418.             }
  419.            
  420.             $body = @"
  421. User $($user.Name) ($($user.UserPrincipalName)) is not setup properly in Rippling.$fieldSection$emailSection
  422.  
  423. Please update the user's information in Rippling and they will be processed on the next run.
  424. "@
  425.             Send-MailMessage -To "[email protected]" -From "[email protected]" -Subject "User Setup Error: $($user.Name)" -Body $body -SmtpServer "mail.contoso.local" -Port 25
  426.             Write-Log "Error notification email sent to IT"
  427.         }
  428.         catch {
  429.             Write-Log "WARNING: Failed to send error notification email: $_" -Level 'WARNING'
  430.         }
  431.        
  432.         Write-Log "Skipping user due to validation errors. Moving to next user..."
  433.         continue
  434.     }
  435.    
  436.     Write-Log "✓ User information validated - Department: $($user.Department), Office: $($user.Office), Country: $($user.Country)"
  437.     Write-Log "✓ Email format validated: $($user.UserPrincipalName)"
  438.  
  439.     Write-Log "Checking Azure AD user existence..."
  440.  
  441.     # Check if user exists in Azure AD (may need to wait for sync)
  442.     $userInfo = $null
  443.     $maxRetries = 3
  444.     $retryDelay = 10
  445.     $azureAdUserFound = $false
  446.  
  447.     for ($retry = 1; $retry -le $maxRetries; $retry++) {
  448.         try {
  449.             $userInfo = Get-MgUser -UserId $user.userPrincipalName -ErrorAction Stop
  450.             if ($userInfo) {
  451.                 $azureAdUserFound = $true
  452.                 Write-Log "Azure AD user found (attempt $retry): $($userInfo.Id)"
  453.                 break
  454.             }
  455.         }
  456.         catch {
  457.             if ($_.Exception.Message -like "*Request_ResourceNotFound*" -or $_.Exception.Message -like "*does not exist*") {
  458.                 if ($retry -lt $maxRetries) {
  459.                     Write-Log "Azure AD user not found yet, waiting ${retryDelay}s before retry ($retry/$maxRetries)..."
  460.                     Start-Sleep -Seconds $retryDelay
  461.                 }
  462.                 else {
  463.                     Write-Log "ERROR: User does not exist in Azure AD after $maxRetries attempts. User was likely just created and needs time to sync from on-prem AD." -Level 'ERROR'
  464.                 }
  465.             }
  466.             else {
  467.                 Write-Log "ERROR: Unexpected error querying Graph API: $_" -Level 'ERROR'
  468.                 break
  469.             }
  470.         }
  471.     }
  472.  
  473.     # Skip user entirely if not found in Azure AD - they need to sync first
  474.     if (-not $azureAdUserFound) {
  475.         Write-Log "ERROR: Cannot process user '$($user.Name)' - not found in Azure AD. Skipping all setup. User will be processed on next run after AD sync completes." -Level 'ERROR'
  476.         continue
  477.     }
  478.  
  479.     # Generate random password for user
  480.     Write-Log "Generating secure password for user..."
  481.     $password = New-TemporaryPassword -Length 16
  482.  
  483.     # Reset user password in AD
  484.     try {
  485.         $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
  486.         Set-ADAccountPassword -Identity $user.SamAccountName -NewPassword $securePassword -Reset -Server $passwordWriteServer
  487.         Set-ADUser -Identity $user.SamAccountName -ChangePasswordAtLogon $false -Server $passwordWriteServer
  488.         Write-Log "Password reset successfully"
  489.  
  490.         $passwordAuditUser = Get-ADUser -Identity $user.SamAccountName -Server $passwordWriteServer -Properties pwdLastSet, whenChanged
  491.         $pwdLastSetText = if ($passwordAuditUser.pwdLastSet -and [int64]$passwordAuditUser.pwdLastSet -gt 0) {
  492.             [datetime]::FromFileTimeUtc([int64]$passwordAuditUser.pwdLastSet).ToString('yyyy-MM-dd HH:mm:ss') + ' UTC'
  493.         }
  494.         else {
  495.             'not set'
  496.         }
  497.         $whenChangedText = if ($passwordAuditUser.whenChanged) {
  498.             $passwordAuditUser.whenChanged.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss') + ' UTC'
  499.         }
  500.         else {
  501.             'unknown'
  502.         }
  503.         Write-Log "Password write verification on $passwordWriteServer - pwdLastSet: $pwdLastSetText, whenChanged: $whenChangedText"
  504.     }
  505.     catch {
  506.         Write-Log "ERROR resetting password: $_" -Level 'ERROR'
  507.         continue
  508.     }
  509.  
  510.     try {
  511.         switch ($true) {
  512.             ($user.Office -eq "field") {
  513.                 if ($user.country -eq 'US' -or $user.country -eq 'CA') {
  514.                     #add user to f3 license group
  515.                     get-adgroup -filter 'name -like "o365Lic_F3_No Installed Office"'|Add-ADGroupMemberSafe -Members $user -server $domain
  516.                     $requiredLicenses.Add("o365Lic_F3_No Installed Office") | Out-Null
  517.                 }
  518.                 else {
  519.                     #add user to e3 license group
  520.                     get-adgroup -filter 'name -like "o365Lic_E3_Installed Office"'|Add-ADGroupMemberSafe -Identity "CN=o365Lic_E3_Installed Office,OU=o365_Licensing_Groups,OU=SecurityGroups,OU=root-users,DC=contoso,DC=local" -Members $user -server $domain
  521.                     $requiredLicenses.Add("o365Lic_E3_Installed Office") | Out-Null
  522.                 }
  523.             }
  524.             #check to see if the user is in dept 067 - RSU-Transducer but does not have manager in their title
  525.             ($user.Department -eq "dept 067 - Assembly" -and $user.Title -notlike "*manager*") {
  526.                 get-adgroup -filter 'name -like "o365Lic_F3_No Installed Office"'|Add-ADGroupMemberSafe -Identity "o365Lic_F3_No Installed Office" -Members $user -server $domain
  527.                 $requiredLicenses.Add("o365Lic_F3_No Installed Office") | Out-Null
  528.                 get-adgroup -filter 'name -like "o365Lic_aadp1"'|Add-ADGroupMemberSafe -Identity "o365Lic_aadp1" -Members $user -server $domain
  529.                 $requiredLicenses.Add("o365Lic_aadp1") | Out-Null
  530.             }
  531.             default {
  532.                 #add user to e3 license group
  533.                 get-adgroup -filter 'name -like "o365Lic_E3_Installed Office"'|Add-ADGroupMemberSafe -Identity "CN=o365Lic_E3_Installed Office,OU=o365_Licensing_Groups,OU=SecurityGroups,OU=root-users,DC=contoso,DC=local" -Members $user -server $domain
  534.                 $requiredLicenses.Add("o365Lic_E3_Installed Office") | Out-Null
  535.             }
  536.         }
  537.     }
  538.     catch{
  539.         Write-Log "ERROR determining base office licenses for user: $($user.Name). Error: $($_.Exception.Message)" -Level 'ERROR'
  540.         continue
  541.     }
  542.  
  543.     Write-Log "User requires the following licenses: $($requiredLicenses -join ', ')"
  544.  
  545.     #increment license counters
  546.     foreach ($license in $requiredLicenses) {
  547.         switch ($license) {
  548.             "o365Lic_E3_Installed Office" {
  549.                 $e3consumed++
  550.                 $defenderconsumed++
  551.             }
  552.             "o365Lic_F3_No Installed Office" {
  553.                 $f3consumed++
  554.                 $defenderconsumed++
  555.             }
  556.             "o365Lic_aadp1" {
  557.                 $aadp1consumed++
  558.             }
  559.         }
  560.     }
  561.  
  562.     # Check if licensing this user will put us over the limit
  563.     if ($e3consumed -gt $o365Lic_E3Owned) {
  564.         Write-Log "WARNING: E3 license limit exceeded for user: $($user.Name)" -Level 'WARNING'
  565.     }
  566.     if ($f3consumed -gt $o365Lic_F3Owned) {
  567.         Write-Log "WARNING: F3 license limit exceeded for user: $($user.Name)" -Level 'WARNING'
  568.     }
  569.     if ($defenderconsumed -gt $defenderOwned) {
  570.         Write-Log "WARNING: Defender license limit exceeded for user: $($user.Name)" -Level 'WARNING'
  571.     }
  572.     if ($aadp1consumed -gt $aadp1Owned) {
  573.         Write-Log "WARNING: AAD P1 license limit exceeded for user: $($user.Name)" -Level 'WARNING'
  574.     }
  575.  
  576.     # Setup groups for the user
  577.     Write-Log "Setting up groups and teams for user..."
  578.     $groupsAdded=[System.Collections.ArrayList]::new()
  579.    
  580.     #default group assignment
  581.     #groups that all users get added to:
  582.     try{
  583.         Add-ADGroupMemberSafe -Identity "SEC_LmsTraining" -Members $user -Server $domain
  584.         $groupsAdded.Add("SEC_LmsTraining") | Out-Null
  585.  
  586.         # Only add to cloud groups if user exists in Azure AD
  587.         if ($azureAdUserFound -and $userInfo) {
  588.             New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId "<GROUP_OBJECT_ID>" #Object ID of the group "SEC_viewer_production_read_only"
  589.             $groupsAdded.Add("SEC_viewer_production_read_only") | Out-Null
  590.         }
  591.         else {
  592.             Write-Log "Skipping cloud group assignment - user not yet synced to Azure AD"
  593.         }
  594.     }
  595.     catch{
  596.         Write-Log "ERROR adding user to default groups: $($user.Name). Error: $($_.Exception.Message)" -Level 'ERROR'
  597.         # Continue processing - don't fail the whole user for group errors
  598.     }
  599.     try{
  600.     #if user is a non field, US, CA or MX user
  601.         if ($user.Office -ne "field" -and ($user.country -eq "US" -or $user.country -eq "CA" -or $user.country -eq "MX")) {
  602.             Add-ADGroupMemberSafe -Identity "map_shelton_w" -Members $user -Server $domain
  603.             $groupsAdded.Add("map_shelton_w") | Out-Null
  604.         }
  605.     }
  606.     catch{
  607.         Write-Log "ERROR adding user to map_shelton_w group: $($user.Name). Error: $($_.Exception.Message)" -Level 'ERROR'
  608.         # Non-critical error, continue processing
  609.     }
  610.  
  611.     try{
  612.         if ($user.Office -eq 'field' -and ($user.country -eq 'US' -or $user.country -eq 'CA' -or $user.country -eq 'MX')) {
  613.             if ($exchangeOnlineConnected) {
  614.                 Add-DistributionGroupMemberSafe -Identity '[email protected]' -Member $user.UserPrincipalName
  615.                 $groupsAdded.Add("[email protected]") | Out-Null
  616.             }
  617.             else {
  618.                 Write-Log "Skipping [email protected] assignment - Exchange Online is not connected"
  619.             }
  620.         }
  621.     }
  622.     catch{
  623.         Write-Log "ERROR adding user to [email protected]: $($user.Name). Error: $($_.Exception.Message)" -Level 'ERROR'
  624.         # Non-critical error, continue processing
  625.     }
  626.  
  627.     try {
  628.         switch ($true) {
  629.         {$user.Department -eq "096 - Analysis" -and $user.country -eq "US"} {
  630.             # Add groups for Non-Stop Analysis US
  631.             Add-ADGroupMemberSafe -Identity "Map_hvnz_qualarchive" -Members $user -Server $domain
  632.             Add-ADGroupMemberSafe -Identity "map_m_cardata" -Members $user -Server $domain
  633.             Add-ADGroupMemberSafe -Identity "MAP_Intune_US Analysis" -Members $user -Server $domain
  634.             Add-ADGroupMemberSafe -Identity "fol_global_analysis" -Members $user -Server $domain
  635.             Add-ADGroupMemberSafe -Identity "vpn" -Members $user -Server $domain
  636.             $groupsAdded.Add("vpn") | Out-Null
  637.             $groupsAdded.Add("Map_hvnz_qualarchive") | Out-Null
  638.             $groupsAdded.Add("map_m_cardata") | Out-Null
  639.             $groupsAdded.Add("MAP_Intune_US Analysis") | Out-Null
  640.             $groupsAdded.Add("fol_global_analysis") | Out-Null
  641.  
  642.             # Teams operations require Azure AD user
  643.             if ($azureAdUserFound -and $userInfo) {
  644.                 #US Analysis (Team)
  645.                 #find us analysis team
  646.                 $usAnalysisTeam = get-mgteam -filter "displayName eq 'US Analysis'"
  647.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $usAnalysisTeam.Id
  648.                 $groupsAdded.Add("US Analysis (Team)") | Out-Null
  649.  
  650.                 # Add to Analysis-Software-IT US channel in Contoso Public team
  651.                 $publicTeam = Get-MgTeam -Filter "displayName eq 'Contoso Public'"
  652.                 $channel = Get-MgTeamChannel -TeamId $publicTeam.Id | Where-Object { $_.DisplayName -eq 'Analysis-Software-IT US' }
  653.                 # Build conversation member payload
  654.                 $payload = @{
  655.                     "@odata.type"     = "#microsoft.graph.aadUserConversationMember"
  656.                     "roles"           = @() # empty for normal member; use @("owner") only if intended
  657.                     "[email protected]" = "https://graph.microsoft.com/v1.0/users('$($userInfo.Id)')"
  658.                 } | ConvertTo-Json -Depth 5
  659.                 # POST member to the channel
  660.                 $uri = "https://graph.microsoft.com/v1.0/teams/$($publicTeam.Id)/channels/$($channel.Id)/members"
  661.  
  662.                 $response = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $payload -ContentType "application/json"
  663.                 if ($null -ne $response['displayname'])
  664.                     {write-log "$($response['displayname']) has been added to channel Analysis-Software-IT US"}
  665.                 else{write-log "$($user.name) was not added to Analysis-Software-IT US correctly"}
  666.  
  667.                 $groupsAdded.Add("Analysis-Software-IT US (Channel in public team)") | Out-Null
  668.             }
  669.             else {
  670.                 Write-Log "Skipping Teams assignments for Non-Stop Analysis - user not yet in Azure AD"
  671.             }
  672.         }
  673.         {$user.Department -eq "062 - Production" -and $user.country -eq "US"} {
  674.             # Add groups for Production US
  675.             Add-ADGroupMemberSafe -Identity "fol_production" -Members $user -Server $domain
  676.             $groupsAdded.Add("fol_production") | Out-Null
  677.         }
  678.         {$user.Department -eq "087 - Engineering R&D" -and $user.country -eq "US"} {
  679.             # Add groups for Engineering R&D US
  680.             Add-ADGroupMemberSafe -Identity "map_h_engineer" -Members $user -Server $domain
  681.             Add-ADGroupMemberSafe -Identity "fol_engineering" -Members $user -Server $domain
  682.             Add-ADGroupMemberSafe -Identity "vpn" -Members $user -Server $domain
  683.             $groupsAdded.Add("vpn") | Out-Null
  684.             $groupsAdded.Add("map_h_engineer") | Out-Null
  685.             $groupsAdded.Add("fol_engineering") | Out-Null
  686.  
  687.             # Teams operations require Azure AD user
  688.             if ($azureAdUserFound -and $userInfo) {
  689.                 # Add to Engineering Team
  690.                 $engineeringTeam = Get-MgTeam -Filter "displayName eq 'Engineering'"
  691.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $engineeringTeam.Id
  692.                 $groupsAdded.Add("Engineering (Team)") | Out-Null
  693.             }
  694.             else {
  695.                 Write-Log "Skipping Teams assignments for Engineering R&D - user not yet in Azure AD"
  696.             }
  697.         }
  698.         {$user.Department -eq "088 - Engineering Embedded" -and $user.country -eq "US"} {
  699.             # Add groups for Engineering Embedded US
  700.             Add-ADGroupMemberSafe -Identity "fol_engineering_Embedded" -Members $user -Server $domain
  701.             Add-ADGroupMemberSafe -Identity "map_h_engineer" -Members $user -Server $domain
  702.             Add-ADGroupMemberSafe -Identity "vpn" -Members $user -Server $domain
  703.             $groupsAdded.Add("vpn") | Out-Null
  704.             $groupsAdded.Add("fol_engineering_Embedded") | Out-Null
  705.             $groupsAdded.Add("map_h_engineer") | Out-Null
  706.  
  707.             # Teams operations require Azure AD user
  708.             if ($azureAdUserFound -and $userInfo) {
  709.                 # Add to Engineering Team
  710.                 $engineeringTeam = Get-MgTeam -Filter "displayName eq 'Engineering'"
  711.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $engineeringTeam.Id
  712.  
  713.                 # Add to Embedded Systems Team
  714.                 $embeddedTeam = Get-MgTeam -Filter "displayName eq 'Embedded Systems'"
  715.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $embeddedTeam.Id
  716.  
  717.                 $groupsAdded.Add("Engineering (Team)") | Out-Null
  718.                 $groupsAdded.Add("Embedded Systems (Team)") | Out-Null
  719.             }
  720.             else {
  721.                 Write-Log "Skipping Teams assignments for Engineering Embedded - user not yet in Azure AD"
  722.             }
  723.         }
  724.         {$user.Department -eq "042 - Accounting" -and $user.country -eq "US"} {
  725.             # Add groups for Accounting US
  726.             Add-ADGroupMemberSafe -Identity "fol_accounting" -Members $user -Server $domain
  727.             Add-ADGroupMemberSafe -Identity "fol_finance" -Members $user -Server $domain
  728.             Add-ADGroupMemberSafe -Identity "vpn" -Members $user -Server $domain
  729.             $groupsAdded.Add("vpn") | Out-Null
  730.             $groupsAdded.Add("fol_accounting") | Out-Null
  731.             $groupsAdded.Add("fol_finance") | Out-Null
  732.  
  733.             # Teams operations require Azure AD user
  734.             if ($azureAdUserFound -and $userInfo) {
  735.                 # Add to Finance Team
  736.                 $financeTeam = Get-MgTeam -Filter "displayName eq 'Finance'"
  737.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $financeTeam.Id
  738.                 $groupsAdded.Add("Finance (Team)") | Out-Null
  739.             }
  740.             else {
  741.                 Write-Log "Skipping Teams assignments for Accounting - user not yet in Azure AD"
  742.             }
  743.         }
  744.         {$user.Department -eq "052 - Procurement" -and $user.country -eq "US"} {
  745.             # Add groups for Procurement US
  746.             Add-ADGroupMemberSafe -Identity "fol_logistics" -Members $user -Server $domain
  747.             $groupsAdded.Add("fol_logistics") | Out-Null
  748.  
  749.             # Teams operations require Azure AD user
  750.             if ($azureAdUserFound -and $userInfo) {
  751.                 # Add to Procurement Team
  752.                 $procurementTeam = Get-MgTeam -Filter "displayName eq 'Procurement'"
  753.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $procurementTeam.Id
  754.                 $groupsAdded.Add("Procurement (Team)") | Out-Null
  755.             }
  756.             else {
  757.                 Write-Log "Skipping Teams assignments for Procurement - user not yet in Azure AD"
  758.             }
  759.         }
  760.         {$user.Department -eq "049 - Quality" -and $user.country -eq "US"} {
  761.             # Add groups for Quality US
  762.             Add-ADGroupMemberSafe -Identity "fol_quality" -Members $user -Server $domain
  763.             Add-ADGroupMemberSafe -Identity "vpn" -Members $user -Server $domain
  764.             $groupsAdded.Add("vpn") | Out-Null
  765.             $groupsAdded.Add("fol_quality") | Out-Null
  766.            
  767.  
  768.             # Teams operations require Azure AD user
  769.             if ($azureAdUserFound -and $userInfo) {
  770.                 # Add to Service Integrity Team
  771.                 $serviceIntegrityTeam = Get-MgTeam -Filter "displayName eq 'Service Integrity'"
  772.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $serviceIntegrityTeam.Id
  773.                 $groupsAdded.Add("service integrity (team)") | Out-Null
  774.             }
  775.             else {
  776.                 Write-Log "Skipping Teams assignments for Quality - user not yet in Azure AD"
  777.             }
  778.         }
  779.         {$user.Department -eq "045 - Training" -and $user.country -eq "US"} {
  780.             # Add groups for Training US
  781.             Add-ADGroupMemberSafe -Identity "fol_training" -Members $user -Server $domain
  782.             $groupsAdded.Add("fol_training") | Out-Null
  783.  
  784.             # Teams operations require Azure AD user
  785.             if ($azureAdUserFound -and $userInfo) {
  786.                 # Add to Training Team
  787.                 $trainingTeam = Get-MgTeam -Filter "displayName eq 'Training'"
  788.                 New-MgGroupMemberSafe -DirectoryObjectId $userInfo.id -GroupId $trainingTeam.Id
  789.                 $groupsAdded.Add("Training (Team)") | Out-Null
  790.             }
  791.             else {
  792.                 Write-Log "Skipping Teams assignments for Training - user not yet in Azure AD"
  793.             }
  794.         }
  795.  
  796.        
  797.         default {
  798.             # No additional groups for other departments/countries
  799.         }
  800.     }
  801.     }
  802.     catch {
  803.         $additionalGroupError = "Failed to assign additional department/country groups for user '$($user.Name)' ($($user.UserPrincipalName)). Department='$($user.Department)', Country='$($user.Country)'. Error: $($_.Exception.Message)"
  804.         Write-Log $additionalGroupError -Level 'ERROR'
  805.         continue
  806.         #throw $additionalGroupError
  807.     }
  808.  
  809.     Write-Log "Groups and teams added: $($groupsAdded -join ', ')"
  810.    
  811.     # Move user to appropriate OU based on country
  812.     Write-Log "Moving user to appropriate OU based on country: $($user.Country)..."
  813.     try {
  814.         $destinationOU = $DestinationOUs[$user.Country]
  815.  
  816.         if ($null -eq $destinationOU) {
  817.             $destinationOU = 'OU=Global,OU=user accounts,OU=root-users,DC=contoso,DC=local'
  818.             Write-Log "Country '$($user.Country)' not in predefined OUs. Using Global OU." -Level 'INFO'
  819.         }
  820.  
  821.         # Move the user
  822.         Move-ADObject -Identity $user.DistinguishedName -TargetPath $destinationOU -Server $domain
  823.  
  824.         # Verify the move
  825.         $movedUser = Get-ADUser -Identity $user.SamAccountName -Server $domain
  826.         if ($movedUser.DistinguishedName -like "*$destinationOU") {
  827.             Write-Log "✓ User successfully moved to: $destinationOU" -Level 'SUCCESS'
  828.         }
  829.         else {
  830.             Write-Log "WARNING: User move verification failed. Current location: $($movedUser.DistinguishedName)" -Level 'WARNING'
  831.         }
  832.     }
  833.     catch {
  834.         Write-Log "ERROR moving user to destination OU: $($_.Exception.Message)" -Level 'ERROR'
  835.         continue
  836.     }
  837.    
  838.     #region LMS User Creation
  839.     if ($null -ne $lmsToken) {
  840.         Write-Log "Creating LMS account for user..."
  841.         try {
  842.             $lmsHeader = @{
  843.                 Authorization  = $lmsToken.access_token
  844.                 "Content-Type" = "application/x-www-form-urlencoded"
  845.             }
  846.             $lmsUserBody = @{
  847.                 userid    = $user.UserPrincipalName
  848.                 firstname = $user.GivenName
  849.                 lastname  = $user.Surname
  850.                 email1    = $user.UserPrincipalName
  851.                 status    = "A"
  852.             }
  853.             $lmsJson = ConvertTo-Json @($lmsUserBody)
  854.             $null = Invoke-RestMethod -Uri "$lmsUrl/user" -Method Post -Headers $lmsHeader -Body "users=$lmsJson"
  855.             Write-Log "✓ LMS account created/updated for $($user.UserPrincipalName)"
  856.         }
  857.         catch {
  858.             Write-Log "WARNING: Failed to create LMS account for $($user.UserPrincipalName): $($_.Exception.Message)" -Level 'WARNING'
  859.             $script:LmsWarnings += [PSCustomObject]@{
  860.                 UserPrincipalName = $user.UserPrincipalName
  861.                 DisplayName       = $user.Name
  862.                 Issue             = "Failed to create LMS account: $($_.Exception.Message)"
  863.             }
  864.         }
  865.     }
  866.     else {
  867.         Write-Log "Skipping LMS account creation - API not connected"
  868.     }
  869.     #endregion
  870.  
  871.     Write-Log "User $($user.Name) processing completed successfully"
  872.  
  873.     # Track successful completion and store password for summary
  874.     $script:UsersSuccessfullyProcessed++
  875.  
  876.     # Store password for summary email (only for successfully processed users)
  877.     $script:UserPasswords += [PSCustomObject]@{
  878.         DisplayName = $(if ([string]::IsNullOrWhiteSpace($user.DisplayName)) { $user.Name } else { $user.DisplayName })
  879.         UserPrincipalName = $user.UserPrincipalName
  880.         Title = $user.Title
  881.         Department = $user.Department
  882.         EmployeeNumber = $user.Employeenumber
  883.         Password = $password
  884.     }
  885.  
  886.     if ($user.Department -eq '046 - Field Training') {
  887.         Write-Log "Skipping manager notification for Field Training user; details will be included in trainee setup summary email."
  888.     }
  889.     else {
  890.         # Send notification email to manager
  891.         Write-Log "Preparing manager notification email..."
  892.         try {
  893.             # Get manager information
  894.             $manager = $null
  895.             if ($user.Manager) {
  896.                 $manager = Get-ADUser -Identity $user.Manager -Properties EmailAddress -Server $domain -ErrorAction SilentlyContinue
  897.             }
  898.            
  899.             $recipientEmails = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
  900.             $isCanadaSV = ($user.Country -eq 'CA' -and $user.Department -eq '063 - SV')
  901.  
  902.             if ($isCanadaSV) {
  903.                 Write-Log "Special routing active for CA + 063 - SV user."
  904.                 $directorRecipients = Get-ADUser -Filter "Department -eq '077 - Canada' -and Title -like '*Director, Customer Operations*'" -Properties EmailAddress -Server $domain -ErrorAction SilentlyContinue
  905.  
  906.                 foreach ($directorRecipient in $directorRecipients) {
  907.                     if (-not [string]::IsNullOrWhiteSpace($directorRecipient.EmailAddress)) {
  908.                         [void]$recipientEmails.Add($directorRecipient.EmailAddress)
  909.                     }
  910.                 }
  911.  
  912.                 if ($manager -and $manager.EmailAddress) {
  913.                     [void]$recipientEmails.Add($manager.EmailAddress)
  914.                     Write-Log "Manager included in recipient list: $($manager.EmailAddress)"
  915.                 }
  916.             }
  917.             elseif ($manager -and $manager.EmailAddress) {
  918.                 [void]$recipientEmails.Add($manager.EmailAddress)
  919.                 Write-Log "Manager found: $($manager.Name) ($($manager.EmailAddress))"
  920.             }
  921.  
  922.             if ($recipientEmails.Count -gt 0) {
  923.                 $recipientList = @($recipientEmails)
  924.                 $greetingName = if ($manager -and $manager.GivenName) { $manager.GivenName } else { 'Team' }
  925.                
  926.                 # Build email body with user setup details
  927.                 $managerEmailBody = @"
  928. Hello $greetingName,
  929.  
  930. This is an automated notification that a new user account has been created and configured.
  931.  
  932. New User Details:
  933. - Name: $($user.Name)
  934. - Email: $($user.UserPrincipalName)
  935. - Department: $($user.Department)
  936. - Office: $($user.Office)
  937. - Country: $($user.Country)
  938.  
  939. Temporary Password: $password
  940. (User will be required to change this password at first login)
  941.  
  942. Account Setup Completed:
  943. - Office 365 licenses assigned
  944. - Active Directory groups configured
  945. - Microsoft Teams access granted
  946. - Email account activated
  947.  
  948. The user should be able to log in and access their email and other resources shortly.
  949.  
  950. IMPORTANT: Please provide the temporary password to the user securely. They will be required to change it upon first login.
  951.  
  952. If you have any questions or need assistance, please contact IT Support.
  953.  
  954. ---
  955. This is an automated message from the IT User Provisioning System.
  956. "@
  957.                
  958.                 $managerEmailParams = @{
  959.                     To = $recipientList
  960.                     From = '[email protected]'
  961.                     Subject = "New User Account Created: $($user.Name)"
  962.                     Body = $managerEmailBody
  963.                     SmtpServer = $SmtpServer
  964.                     Port = $SmtpPort
  965.                 }
  966.                
  967.                 <#
  968.                 if user has Country=CA and department is '063 - SV' then instead of only emailing
  969.                 the user's manager, email all users with the title of Director, Customer Operations in department 077 - Canada
  970.                 #>
  971.  
  972.                 # COMMENTED OUT FOR TESTING - Uncomment when ready to send to managers
  973.                 Send-MailMessage @managerEmailParams
  974.                 Write-Log "Manager notification email prepared"
  975.                 Write-Log "  Sent to: $($recipientList -join ', ')"
  976.             }
  977.             else {
  978.                 if ($isCanadaSV) {
  979.                     Write-Log "WARNING: No valid Director, Customer Operations recipients found in department 077 - Canada, and no manager email available. Notification not sent." -Level 'WARNING'
  980.                 }
  981.                 else {
  982.                     Write-Log "WARNING: Manager not found or manager has no email address. Notification not sent." -Level 'WARNING'
  983.                 }
  984.             }
  985.         }
  986.         catch {
  987.             Write-Log "ERROR preparing manager notification: $_" -Level 'ERROR'
  988.         }
  989.     }
  990.  
  991.     $operationsCoordinationEmail = switch ($true) {
  992.         ($user.Office -eq 'field' -and $user.Country -eq 'US') { '[email protected]' }
  993.         ($user.Office -eq 'field' -and $user.Country -eq 'CA') { '[email protected]' }
  994.         default { $null }
  995.     }
  996.  
  997.     if ($operationsCoordinationEmail) {
  998.         Write-Log "Preparing operations coordination notification email..."
  999.         try {
  1000.             $operationsCoordinationBody = @"
  1001. Hello Operations Coordination,
  1002.  
  1003. This is an automated notification that a new field user account has been created and configured.
  1004.  
  1005. New User Details:
  1006. - Name: $($user.Name)
  1007. - Email: $($user.UserPrincipalName)
  1008. - Department: $($user.Department)
  1009. - Office: $($user.Office)
  1010. - Country: $($user.Country)
  1011.  
  1012. Temporary Password: $password
  1013. (User will be required to change this password at first login)
  1014.  
  1015. Account Setup Completed:
  1016. - Office 365 licenses assigned
  1017. - Active Directory groups configured
  1018. - Microsoft Teams access granted
  1019. - Email account activated
  1020.  
  1021. The user should be able to log in and access their email and other resources shortly.
  1022.  
  1023. IMPORTANT: Please provide the temporary password to the user securely. They will be required to change it upon first login.
  1024.  
  1025. If you have any questions or need assistance, please contact IT Support.
  1026.  
  1027. ---
  1028. This is an automated message from the IT User Provisioning System.
  1029. "@
  1030.  
  1031.             $operationsCoordinationParams = @{
  1032.                 To = $operationsCoordinationEmail
  1033.                 From = '[email protected]'
  1034.                 Subject = "New Field User Account Created: $($user.Name)"
  1035.                 Body = $operationsCoordinationBody
  1036.                 SmtpServer = $SmtpServer
  1037.                 Port = $SmtpPort
  1038.             }
  1039.  
  1040.             Send-MailMessage @operationsCoordinationParams
  1041.             Write-Log "Operations coordination notification sent to: $operationsCoordinationEmail"
  1042.         }
  1043.         catch {
  1044.             Write-Log "ERROR sending operations coordination notification: $_" -Level 'ERROR'
  1045.         }
  1046.     }
  1047. }
  1048.  
  1049. Write-Log "`n========================================"
  1050. Write-Log "Script completed. Attempted: $script:UsersProcessed | Successfully processed: $script:UsersSuccessfullyProcessed users."
  1051.  
  1052. # Send CO Training summary email only if there are successfully processed CO Training users
  1053. $coTrainingProcessed = $script:UserPasswords | Where-Object { $_.Department -eq '046 - CO Training' }
  1054. if ($coTrainingProcessed.Count -gt 0) {
  1055.     Write-Log "Sending CO Training summary email..."
  1056.  
  1057.     $coTrainingPasswordList = $coTrainingProcessed | ForEach-Object {
  1058.         "- $($_.displayName) ($($_.UserPrincipalName)): $($_.Password)"
  1059.     }
  1060.  
  1061.     $coTrainingSummaryBody = @"
  1062. New CO Training users created in this run:
  1063.  
  1064. $($coTrainingPasswordList -join "`n")
  1065.  
  1066. NOTE: Users must change password at first login.
  1067. "@
  1068.  
  1069.     $coTrainingSummaryEmailParams = @{
  1070.         To = '[email protected]'
  1071.         From = '[email protected]'
  1072.         Subject = "CO Training New User Setup Summary: $($coTrainingProcessed.Count) users"
  1073.         Body = $coTrainingSummaryBody
  1074.         SmtpServer = $SmtpServer
  1075.         Port = $SmtpPort
  1076.     }
  1077.  
  1078.     try {
  1079.         Send-MailMessage @coTrainingSummaryEmailParams
  1080.         Write-Log "CO Training summary email sent successfully"
  1081.     }
  1082.     catch {
  1083.         Write-Log "ERROR sending CO Training summary email: $_" -Level 'ERROR'
  1084.     }
  1085. }
  1086.  
  1087. # Synchronize AD changes - local DC only (running on $env:COMPUTERNAME)
  1088. if ($script:UsersSuccessfullyProcessed -gt 0) {
  1089.     Write-Log "`nSynchronizing Active Directory changes..."
  1090.  
  1091.     # Only trigger replication on the local DC - remote DCs require different credentials
  1092.     $localDC = $env:COMPUTERNAME + '.contoso.local'
  1093.     try {
  1094.         Write-Log "Triggering AD replication on local DC ($localDC)..."
  1095.         repadmin /syncall /AdeP | Out-Null
  1096.         Write-Log "  $localDC - Replication triggered successfully"
  1097.     }
  1098.     catch {
  1099.         Write-Log "  WARNING: Failed to trigger replication on ${localDC}: $($_.Exception.Message)" -Level 'WARNING'
  1100.     }
  1101.  
  1102.     # Note: Azure AD Connect sync runs on dc01.contoso.local. Changes will sync on the next scheduled cycle (typically 30 minutes)
  1103.     Write-Log "`nAzure AD Connect sync runs on dc01 and will process changes on the next scheduled cycle (typically 30 minutes)"
  1104.  
  1105.     Write-Log "Synchronization process completed"
  1106. }
  1107.  
  1108. # Send email notification only if users were successfully processed
  1109. if ($script:UsersSuccessfullyProcessed -gt 0) {
  1110.     Write-Log "Sending email notification..."
  1111.    
  1112.     # Build certificate warning section
  1113.     $certWarningSectionHtml = ""
  1114.     if ($null -ne $script:CertWarning) {
  1115.         $certWarningText = [System.Net.WebUtility]::HtmlEncode($script:CertWarning)
  1116.         $certWarningSectionHtml = @"
  1117. <div style='border: 1px solid #cc0000; background-color: #fff3f3; padding: 10px; margin: 12px 0;'>
  1118.  <strong>CERTIFICATE WARNING</strong><br />
  1119.  $certWarningText<br />
  1120.  <strong>ACTION REQUIRED</strong>
  1121. </div>
  1122. "@
  1123.     }
  1124.  
  1125.     # Build password list section
  1126.     $passwordSectionHtml = ""
  1127.     if ($script:UserPasswords.Count -gt 0) {
  1128.         $passwordTableRows = $script:UserPasswords | ForEach-Object {
  1129.             $displayName = [System.Net.WebUtility]::HtmlEncode($(if ([string]::IsNullOrWhiteSpace($_.DisplayName)) { '' } else { $_.DisplayName }))
  1130.             $upn = [System.Net.WebUtility]::HtmlEncode($(if ([string]::IsNullOrWhiteSpace($_.UserPrincipalName)) { '' } else { $_.UserPrincipalName }))
  1131.             $title = [System.Net.WebUtility]::HtmlEncode($(if ([string]::IsNullOrWhiteSpace($_.Title)) { '' } else { $_.Title }))
  1132.             $department = [System.Net.WebUtility]::HtmlEncode($(if ([string]::IsNullOrWhiteSpace($_.Department)) { '' } else { $_.Department }))
  1133.             $tempPassword = [System.Net.WebUtility]::HtmlEncode($(if ([string]::IsNullOrWhiteSpace($_.Password)) { '' } else { $_.Password }))
  1134.             $employeeNumber = [System.Net.WebUtility]::HtmlEncode($(if ([string]::IsNullOrWhiteSpace($_.EmployeeNumber)) { '' } else { $_.EmployeeNumber }))
  1135.  
  1136.             "<tr><td style='border: 1px solid #cccccc; padding: 6px;'>$displayName</td><td style='border: 1px solid #cccccc; padding: 6px;'>$upn</td><td style='border: 1px solid #cccccc; padding: 6px;'>$title</td><td style='border: 1px solid #cccccc; padding: 6px;'>$department</td><td style='border: 1px solid #cccccc; padding: 6px;'>$employeeNumber</td><td style='border: 1px solid #cccccc; padding: 6px;'>$tempPassword</td></tr>"
  1137.         }
  1138.  
  1139.         $passwordSectionHtml = @"
  1140. <h3>User Passwords (Temporary)</h3>
  1141. <table style='border-collapse: collapse; width: 100%;'>
  1142.  <thead>
  1143.    <tr>
  1144.      <th style='border: 1px solid #cccccc; padding: 6px; text-align: left;'>Display Name</th>
  1145.      <th style='border: 1px solid #cccccc; padding: 6px; text-align: left;'>UPN</th>
  1146.      <th style='border: 1px solid #cccccc; padding: 6px; text-align: left;'>Title</th>
  1147.      <th style='border: 1px solid #cccccc; padding: 6px; text-align: left;'>Department</th>
  1148.      <th style='border: 1px solid #cccccc; padding: 6px; text-align: left;'>Employee Number</th>
  1149.      <th style='border: 1px solid #cccccc; padding: 6px; text-align: left;'>Password</th>
  1150.    </tr>
  1151.  </thead>
  1152.  <tbody>
  1153.    $($passwordTableRows -join "`n")
  1154.  </tbody>
  1155. </table>
  1156. <p><strong>NOTE:</strong> Users must change password at first login.<br />Passwords have been sent to managers or trainee setup mailbox as applicable.</p>
  1157. "@
  1158.     }
  1159.  
  1160.     # Build LMS warnings section
  1161.     $lmsSectionHtml = ""
  1162.     if ($script:LmsWarnings.Count -gt 0) {
  1163.         $lmsRows = $script:LmsWarnings | ForEach-Object {
  1164.             $escapedName = [System.Net.WebUtility]::HtmlEncode($_.DisplayName)
  1165.             $escapedUPN = [System.Net.WebUtility]::HtmlEncode($_.UserPrincipalName)
  1166.             $escapedIssue = [System.Net.WebUtility]::HtmlEncode($_.Issue)
  1167.             "<tr><td style='border:1px solid #cc0000; padding:6px;'>$escapedName</td><td style='border:1px solid #cc0000; padding:6px;'>$escapedUPN</td><td style='border:1px solid #cc0000; padding:6px;'>$escapedIssue</td></tr>"
  1168.         }
  1169.         $lmsSectionHtml = @"
  1170. <div style='border:2px solid red; padding:12px; background-color:#fff3f3; margin:12px 0;'>
  1171.  <strong style='color:red; font-size:12pt;'>LMS Account Failures</strong>
  1172.  <p style='margin:6px 0; color:red;'>The following users could not be created in LMS and require manual attention:</p>
  1173.  <table style='border-collapse:collapse; width:100%;'>
  1174.    <thead>
  1175.      <tr style='background-color:#ffcccc;'>
  1176.        <th style='border:1px solid #cc0000; padding:6px; text-align:left;'>Display Name</th>
  1177.        <th style='border:1px solid #cc0000; padding:6px; text-align:left;'>UPN</th>
  1178.        <th style='border:1px solid #cc0000; padding:6px; text-align:left;'>Error</th>
  1179.      </tr>
  1180.    </thead>
  1181.    <tbody>
  1182.      $($lmsRows -join "`n")
  1183.    </tbody>
  1184.  </table>
  1185. </div>
  1186. "@
  1187.     }
  1188.  
  1189.     $logContentHtml = [System.Net.WebUtility]::HtmlEncode(($script:LogMessages -join "`r`n"))
  1190.  
  1191.     $EmailBody = @"
  1192. <html>
  1193. <body style='font-family: Segoe UI, Arial, sans-serif; font-size: 13px;'>
  1194. <h2>Automated User Setup Script Report</h2>
  1195. <p>
  1196. <strong>Run Date:</strong> $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')<br />
  1197. <strong>Server:</strong> $([System.Net.WebUtility]::HtmlEncode($env:COMPUTERNAME))<br />
  1198. <strong>Users Attempted:</strong> $script:UsersProcessed<br />
  1199. <strong>Users Successfully Processed:</strong> $script:UsersSuccessfullyProcessed
  1200. </p>
  1201. $certWarningSectionHtml
  1202. $lmsSectionHtml
  1203. $passwordSectionHtml
  1204. <h3>Detailed Log</h3>
  1205. <pre style='background-color: #f7f7f7; border: 1px solid #cccccc; padding: 10px; white-space: pre-wrap;'>$logContentHtml</pre>
  1206. </body>
  1207. </html>
  1208. "@
  1209.    
  1210.     $EmailParams = @{
  1211.         To = $EmailTo
  1212.         From = $EmailFrom
  1213.         Subject = "Auto-User Setup: $script:UsersSuccessfullyProcessed/$script:UsersProcessed users successfully processed on $env:COMPUTERNAME"
  1214.         Body = $EmailBody
  1215.         BodyAsHtml = $true
  1216.         SmtpServer = $SmtpServer
  1217.         Port = $SmtpPort
  1218.     }
  1219.    
  1220.     try {
  1221.         Send-MailMessage @EmailParams
  1222.         Write-Log "Email notification sent successfully"
  1223.     }
  1224.     catch {
  1225.         Write-Log "ERROR sending email notification: $_" -Level 'ERROR'
  1226.     }
  1227. }
  1228.  
  1229.  
Advertisement
Add Comment
Please, Sign In to add comment