Public/Wait-ADRole.ps1

using namespace System.Collections.Generic
using namespace System.Collections.Concurrent
using namespace System.Management.Automation
using namespace Microsoft.Graph.PowerShell.Models
function Wait-ADRole {
    [OutputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphUnifiedRoleAssignmentScheduleInstance])]
    <#
        .SYNOPSIS
        Waits for an AD role request to complete.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)][MicrosoftGraphUnifiedRoleAssignmentScheduleRequest]$RoleRequest,
        #How often to check for an update. Defaults to 1 second
        [double]$Interval = 1,
        #Keep checking until this specified number of seconds. Default to 10 minutes to allow for approval workflows.
        $Timeout = 600,
        #How many roles to check simultaneously. You shouldn't normally need to modify this.
        $ThrottleLimit = 5,
        #If specified, will return the activated role instances which can be later passed to Disable-AdRole
        [Switch]$PassThru,
        #By Default, Wait-JAzADRole will wait for 1 second to show a summary result, specify this to skip that.
        [Switch]$NoSummary
    )
    begin {
        [List[MicrosoftGraphUnifiedRoleAssignmentScheduleRequest]]$RoleRequests = @{}
        #Used to track progress
        $parentId = Get-Random
    }
    process {
        if ($RoleRequest.EndDateTime) {
            $localEndTime = $RoleRequest.EndDateTime.ToLocalTime()
            if ($localEndTime -lt [DateTime]::Now) {
                Write-CmdletError "$($RoleRequest.RoleName) role end date already expired at $localEndTime. Skipping.."
                return
            }
        }

        $RoleRequests.Add($RoleRequest)
    }
    end {
        #This synchronized dictionary is used to keep the status of the requests.
        [ConcurrentDictionary[Int, hashtable]]$info = @{}

        $waitJobs = $RoleRequests | ForEach-Object -ThrottleLimit $ThrottleLimit -AsJob -Parallel {
            Import-Module 'Microsoft.Graph.Authentication' -Verbose:$false 4>$null
            $VerbosePreference = 'continue'
            [Microsoft.Graph.PowerShell.Models.MicrosoftGraphUnifiedRoleAssignmentScheduleRequest]$requestItem = $PSItem
            $name = $requestItem.RoleDefinition.DisplayName
            $created = $requestItem.CreatedDateTime

            function Get-Timestamp ($created = $created) {
                $since = [datetime]::UtcNow - $created
                if ($since.TotalSeconds -gt $USING:Timeout) {
                    throw "$name`: Exceeded Timeout of $($USING:Timeout) seconds waiting for role request to be completed"
                }
                ' - ' + [int]($since.totalSeconds) + ' secs elapsed'
            }

            function Set-JobStatus ($Status, $PercentComplete, $jobInfo = $jobInfo) {
                if ($status) { $jobInfo.Status = $Status.PadRight(30) + " $(Get-TimeStamp)" }
                if ($percentComplete) { $jobInfo.PercentComplete = $PercentComplete }
            }

            #Register a job info tracker
            $jobInfo = @{
                Activity = "$Name".padRight(30)
                Status   = 'Provisioning'
            }
            do {
                $isUnique = ($USING:info).TryAdd((Get-Random), $jobInfo)
            } until ($isUnique)

            if ($status -ne 'Provisioned') {
                do {
                    #HACK: Command doesn't exist for this yet
                    $request = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests/filterByCurrentUser(on='principal')?`$select=status&`$filter=id eq '$($requestItem.Id)'"
                    $status = (Invoke-MgGraphRequest -Verbose:$false -ErrorAction stop -Method Get -Uri $request).value.status
                    Set-JobStatus $status -PercentComplete 30
                    Start-Sleep $USING:Interval
                } while (
                    #This is a generic consent request type
                    #https://docs.microsoft.com/en-us/graph/api/resources/request?view=graph-rest-1.0
                    $status -like 'Pending*'
                )
                if ($status -ne 'Provisioned') {
                    Write-Error "$name`: Request failed with status $status"
                    return
                }
            }

            #Now we need to wait until the instance actually appears in the directory, the role definition request updates don't provide status on this.
            #We have to match on the schedule id from the request, it's a 1:1 relationship so this is safe and should never return multiple results.
            $activatedRole = $null
            do {
                Set-JobStatus 'Activating' -PercentComplete 30
                $uri = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances/filterByCurrentUser(on='principal')?`$select=startDateTime&`$filter=roleAssignmentScheduleId eq '$($requestItem.TargetScheduleId)'"
                $response = (Invoke-MgGraphRequest -Verbose:$false -Method Get -Uri $uri).Value

                Start-Sleep $USING:Interval
            } until ($response)

            $activatedStartDateTime = $response.startDateTime.ToLocalTime()
            Set-JobStatus "Activated at $activatedStartDateTime" -PercentComplete 100
        }


        #Report progress
        try {
            Write-Progress -Id $parentId -Activity 'Azure AD PIM Role Activation'
            $runningStates = 'AtBreakpoint', 'Running', 'Stopping', 'Suspending'
            $i = 0
            $notFirstLoop = $false
            do {
                Start-Sleep 0.5
                if (!$notFirstLoop) { Start-Sleep 0.5; $notFirstLoop = $true }
                foreach ($infoItem in $info.GetEnumerator()) {
                    $jobInfo = $infoItem.Value
                    Write-Progress -ParentId $parentId -Id $infoItem.Key @jobInfo
                }
                #Get an average progress from child jobs

                $totalProgress = (($info.Values.PercentComplete | Measure-Object -Sum).Sum) / $waitJobs.ChildJobs.Count
                $completeJobCount = ($waitJobs.ChildJobs | Where-Object state -NotIn $runningStates).count
                Write-Progress -Id $parentId -Activity 'Azure AD PIM Role Activation' -Status "$completeJobCount of $($waitJobs.ChildJobs.count)" -PercentComplete $totalProgress
                #Runs the loop one last time to get final status
                if ($waitJobs.State -notin $RunningStates) {
                    $i++
                }
            } until (
                $waitJobs.State -notin $RunningStates -and
                $i -gt 1
            )

            if (-not $NoSummary) { Start-Sleep 1 }
            Write-Progress -Id $parentId -Activity 'Azure AD PIM Role Activation' -Completed
        } catch { throw } finally {
            $waitJobs | Receive-Job -Wait -AutoRemoveJob
        }

        if ($PassThru) {
            Get-JAzADRole -Activated
            | Where-Object { $_.roleAssignmentScheduleId -in $RoleRequests.TargetScheduleId }
        }
    }
}