Zoom Licensing Reconciliation Utilizing Zoom's API (Powershell)

Our rapid deployment of Zoom for COVID-19 response compressed planning and configuration to a very small amount of time. One of the challenges we encountered involved ensuring and auditing that all users had appropriate licensing assigned. Shibboleth IDP/SSO is our primary means for authentication and assignment of licensing, based on affiliation to the organization. However, the rules/policies configured for SSO login and licensing assignment works ONLY if this is the first time a user has ever logged into a Zoom environment. If a user has an existing Zoom account associated to any other zoom "account", such as a personal account, this will (in short) cause the user to go through an alternate user creation process that does not allow Shibboleth to validate the user's affiliation, and thus, the user will be assigned the "default license", which our account/tenant is configured.

While the majority of our user base was served perfectly by the Shibboleth SSO login process, a good number of thousands did not have the proper onboarding experience, and this created licensing mismatches that became a laborious daily task for us as hundreds of new users were ingressing daily. The challenge was ensuring that the right users have the right licenses, and the reason I developed this script.

    
#
# Purpose: Audit users in the Zoom tenant with no group membership, determine if they have the right user license, and assign them to Faculty or Students
#
# Requirements: PowerShell 5.1 (and likely anything newer), running on PowerShell older than 5.1 yielded hanging and eratic performance.
#               
# Logs: "C:\logs\zoom\" <---- Directory must exist for logging to work.
#
# Created by: Gabe McGinnis ([email protected])
#


# Logging
$ErrorActionPreference = "Continue"
$logDir = "C:\logs\zoom\"
$timeStamp = Get-Date -Format "yyMMddHHmm"
$logFile = "ZoomUsersWithNoGroupLicenseReconcile_$($timeStamp).log"
Start-Transcript (Join-Path $logDir $logFile) -Append

# Use TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12


# Affiliatons
$facultyAffs = "administrative faculty","associate","courtesy","emeritus","faculty","fixed short term faculty","fixed term enduring faculty","staff","gtf","temporary employee"
$nonPersonAffs = "departmental"

# api header/creds. JWT expires 00:00 n/n/nnnn, JWT is deployed via the custom personal app 'User License/Group Reconcile', where it can be renewed.
$headers=@{} 
$headers.Add("authorization", "Bearer ******************************************************************************* ") 


# gather users from zoom tenant / pagination
$apiCurrentPageNumber = 1
$apiListUsers = Invoke-RestMethod -Uri "https://api.zoom.us/v2/users?role_id=2&page_number=$apiCurrentPageNumber&page_size=300&status=active" -Method GET -Headers $headers
$apiUserPagesTotal = $apiListUsers.page_count
Write-Host "Total pages of query:" $apiUserPagesTotal
$apiCurrentPageNumber = $apiListUsers.page_number
$userCollection = @()
$userCollection += $apiListUsers.users

while ($apiCurrentPageNumber -lt $apiUserPagesTotal)
    {
        $apiCurrentPageNumber++
        $apiListUsers = Invoke-RestMethod -Uri "https://api.zoom.us/v2/users?role_id=2&page_number=$apiCurrentpagenumber&page_size=300&status=active" -Method GET -Headers $headers
        $userCollection += $apiListUsers.users
    }

Write-Host "Total users in uoregon tenant:" $userCollection.count  -ForegroundColor Green

#$userCollection | Out-File -FilePath "c:\temp\zoom\UserCollectionDump.txt" -Append

# Gather non-grouped users and audit their assigned license
$counter = 0
$facultyCounter = 0
$studentsCounter = 0
$nonPersonCounter = 0
$foreignDomainCounter = 0

Foreach ($user in $userCollection)
    {
        #$user.email | Out-File -FilePath "c:\temp\zoom\UserCollectionDump.txt" -Append
        # grab users who do not have group membership
        If ($user.group_ids -eq $null)
            {
                $counter++
                # get user zoom user properties
                $userLicense = $user.type
                $userEmail = $user.email
                # get user affiliation in AD and count the faculty-associated affs
                $domainID = ($user.email).split('@')[0]
                $emailSuffix = ($user.email).split('@')[1]
                If ($emailSuffix -eq 'domain.com')
                    {
                        If ($affs)
                            {
                                Remove-Variable affs
                            }
                        $affs = Get-ADUser $domainID -Properties "uoad-uopersonaffiliation" | select -ExpandProperty "uoad-uopersonaffiliation"
                        $facAffCounter = 0
                        $nonPersonAffCounter = 0
                        Foreach ($aff in $affs)
                            {
                                If ($facultyAffs -contains $aff)
                                    {
                                        $facAffCounter++
                                    }
                                If ($nonPersonAffs -contains $aff)
                                    {
                                        $nonPersonAffCounter++
                                    }
                            }
                       # determine if license type needs to be changed based on current affiliation
                       If (($facAffCounter -gt 0) -and ($nonPersonAffCounter -eq 0))
                           {
                               # Assign Education license if user object doesnt already have one assigned
                               If ($userLicense -ne '2')
                                   {
                                       Write-Host "$domainID  -  Assigning Licensed (type=2)"
                                       Invoke-RestMethod -Uri https://api.zoom.us/v2/users/$userEmail -Method PATCH -Headers $headers -ContentType 'application/json' -Body '{"type":2}'
                                   }
                               # Add user to Faculty group
                               $body = '{"members":[{"email":' + '"' + "$userEmail" + '"' + '}]}'
                               Write-Host "$domainID  -  Adding to Faculty group"
                               Invoke-RestMethod -Uri "https://api.zoom.us/v2/groups/xH0XHzg4S2iLoNpICNrrUw/members" -Method POST -Headers $headers -ContentType 'application/json' -Body $body
                               $affs
                               $facultyCounter++                  
                           }
                       If (($facAffCounter -eq 0) -and ($nonPersonAffCounter -eq 0))
                           {
                               # Assign basic license if user object doesnt already have one assigned
                               If ($userLicense -ne '1')
                                   {
                                       Write-Host "$domainID  -  Assigning Basic (type=1)"                           
                                       Invoke-RestMethod -Uri https://api.zoom.us/v2/users/$userEmail -Method PATCH -Headers $headers -ContentType 'application/json' -Body '{"type":1}'
                                   }
                               # Add user to Students group
                               $body = '{"members":[{"email":' + '"' + "$userEmail" + '"' + '}]}'
                               Write-Host "$domainID  -  Adding to Students group"
                               Invoke-RestMethod -Uri "https://api.zoom.us/v2/groups/B6GAETAxQEaXX9f3Ga34TQ/members" -Method POST -Headers $headers -ContentType 'application/json' -Body $body
                               $affs
                               $studentsCounter++
                           }
                       If (($nonPersonAffCounter -gt 0) -and ($facAffCounter -eq 0))
                           {
                               # Assign basic license if user object doesnt already have one assigned
                               If ($userLicense -ne '1')
                                   {
                                       Write-Host "$domainID  -  Assigning Basic (type=1)"                           
                                       Invoke-RestMethod -Uri https://api.zoom.us/v2/users/$userEmail -Method PATCH -Headers $headers -ContentType 'application/json' -Body '{"type":1}'
                                   }
                               # Add user to Alum-Retired-Role-Etc
                               $body = '{"members":[{"email":' + '"' + "$userEmail" + '"' + '}]}'
                               Write-Host "$domainID  -  Adding to Alum-Retired-Role-Etc group" -ForegroundColor DarkYellow
                               Invoke-RestMethod -Uri "https://api.zoom.us/v2/groups/nOCA3TPtT1iMQR_6IZxJ2Q/members" -Method POST -Headers $headers -ContentType 'application/json' -Body $body
                               $affs
                               $nonPersonCounter++
                           }
                    }
                If ($emailSuffix -ne 'domain.com')
                    {
                        # Assign basic license if user object doesnt already have one assigned
                        If ($userLicense -ne '1')
                            {
                                Write-Host "$domainID  -  Assigning Basic (type=1)"                           
                                Invoke-RestMethod -Uri https://api.zoom.us/v2/users/$userEmail -Method PATCH -Headers $headers -ContentType 'application/json' -Body '{"type":1}'
                            }
                        # Add user to Alum-Retired-Role-Etc
                        $body = '{"members":[{"email":' + '"' + "$userEmail" + '"' + '}]}'
                        Write-Host "$domainID  -  Adding to Alum-Retired-Role-Etc group" -ForegroundColor Cyan
                        Invoke-RestMethod -Uri "https://api.zoom.us/v2/groups/nOCA3TPtT1iMQR_6IZxJ2Q/members" -Method POST -Headers $headers -ContentType 'application/json' -Body $body
                        $foreignDomainCounter++
                    }
            }
    }

# Non-grouped users counter
Write-Host "Total users without a group that were processed:" $counter -ForegroundColor Green
Write-Host "Total faculty without a group:" $facultyCounter -ForegroundColor Green
Write-Host "Total students without a group:" $studentsCounter -ForegroundColor Green
Write-Host "Total non-persons without a group:" $nonPersonCounter -ForegroundColor Green
Write-Host "Total foreign domain users without a group:" $foreignDomainCounter -ForegroundColor Green


# Email

$body1 = "Script: zoomReconcile-nonGrouped.ps1"
$body2 = "Server: is-toolbox.ad.domain.com"
$body3 = "File location: C:\Scripts\ids\zoom\zoomReconcile-nongGrouped.ps1"
$body4 = "Scheduled Task name: Task Scheduler > IDS Team > Zoom Reconcile"
$body5 = "Service Account: is-svc-serviceaccount"
$body6 = "Logs: c:\logs\zoom\"
$body7 = "Git repo: puppet_ids"
$body8 = "Documentation: https://wiki.domain.com/display/IDM/Zoom+Reconciliation"
$bodyLine = ""
$bodyDashes = "+-------------------------- Results -----------------------------------+"
$bodyLine = ""
$body9 = "Total users without a group that were processed: $counter" | Out-String
$body10 = "Total faculty without a group: $facultyCounter" | Out-String
$body11 = "Total students without a group: $studentsCounter" | Out-String
$body12 = "Total non-persons without a group: $nonPersonCounter" | Out-String
$body13 = "Total foreign domain users without a group: $foreignDomainCounter" | Out-String
$bodyMerge = $body1,$body2,$body3,$body4,$body5,$body6,$body7,$body8,$bodyLine,$bodyDashes,$bodyLine,$body9,$body10,$body11,$body12,$body13 | Out-String

Send-MailMessage -SmtpServer "smtp.domain.com" -From "[email protected]" -To "[email protected]" -Subject "**** Zoom Reconcile: Non-grouped Users ****" -Body "$bodyMerge"


# Stops logging
Stop-Transcript