Get-OktaURIRecursive.psm1

$FunctionScriptName = "Get-OktaURIRecursive"
Write-Verbose "Import-Start| [$($FunctionScriptName)]"

#* Dependencies
# N/A

function Get-OktaURIRecursive {
    <#
        .SYNOPSIS
            Get Okta API data - recursive
        .Description
            Get recursive Okta API data
            Uses nextLink property
        .NOTES
            AUTHOR: Ken Dobrunz // Ken.Dobrunz@skaylink.com & Ken@Dobrunz.tech
             
            LASTEDIT: 07.06.2024 - Version: 2.4.1
        #>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory = $true)]$uri,
        [Parameter()][Alias('filter')]$QueryFilter,
        [Parameter()]$QueryLimit = 1000,
        [Parameter()]$APIkey,
        [Parameter()][Alias('Header_OkktaResponse')]$Header_OktaResponse, #? Alias = Legacy compatibility
        [Parameter()]$RateLimitMinimumRemaining = 5,
        [Parameter()]$MaxPaginationCount = 1000,
        [Parameter()]$TimeoutSec = 60,
        [Parameter()][switch]$ReturnPaginationObjectsOnly,
        [Parameter()][switch]$ReturnContext,
        [Parameter()][switch]$ReturnContextOnly,
        [Parameter()][switch]$DontFixQuotationMarks
    )
    Begin {
        $ProgressPreference = 'SilentlyContinue'
        
        # Check APIKey
        if($APIkey -like "SSWS*"){
            Write-Error "Invalid APIKey syntax - Only provide the key!"
        }
        
        # Build Header
        $OktaHeader = @{
            Accept          = "application/json"
            "Content-Type"  = "application/json"
            "Authorization" = "SSWS " + $APIkey
        }
        
        # Set OktaResponse
        if ($Header_OktaResponse) {
            $OktaHeader.'okta-response' = $Header_OktaResponse
        }
                
        # Add Query filter
        if($QueryFilter.Length -gt 0){
            if(!$DontFixQuotationMarks){
                $QueryFilter = $QueryFilter.Replace("'",'"')
            }
            if ($uri -match "\?") {
                $uri = $uri + "&filter=" + $QueryFilter
            } else {
                $uri = $uri + "?filter=" + $QueryFilter
            }
        }
        
        # Add limit #? Only add if not in uri
        if ($uri -match "\?" -and $QueryLimit -and $uri -notlike "*limit=*") {
            $uri = $uri + "&limit=" + $QueryLimit
        } elseif ($QueryLimit -and $uri -notlike "*limit=*") {
            $uri = $uri + "?limit=" + $QueryLimit
        }

        # Ensure no NULL pagination
        if($MaxPaginationCount -eq $null){ $MaxPaginationCount = 1000 }
        if($TimeoutSec -eq $null){ $TimeoutSec = 60 }
        
        $IsErrorRun = $false
        $RunCount = 1
        Write-Verbose "URI: [$uri] @ MaxPagination: [$MaxPaginationCount] with QueryLimit [$QueryLimit]"
    }
    process {
        $functionlist = @(); $PaginationCount = 0
        while ($null -ne $uri) {
            # Get response from API
            Try {
                # Get response from OKTA API
                $response = $null
                $response = (Invoke-WebRequest -Method GET -Uri $uri -Headers $OktaHeader -TimeoutSec $TimeoutSec)
                
                # Rate Limit / Pagination
                Write-Debug "Rate Limit: [Remaining: $($response.Headers.'x-rate-limit-remaining') / Limit: $($response.Headers.'x-rate-limit-limit')] - QRY Limit: $($RateLimitMinimumRemaining)"
                if ([int]($response.Headers.'x-rate-limit-remaining')[0] -le $RateLimitMinimumRemaining) {
                    # Wait until "x-rate-limit-reset"
                    $RateLimitWaitSeconds = ($response.Headers.'x-rate-limit-reset')[0] - (([DateTimeOffset](($response.Headers.'date')[0])).ToUnixTimeSeconds())
                    Write-Warning "Waiting for Rate Limit [Remaining: $($response.Headers.'x-rate-limit-remaining') / Limit: $($response.Headers.'x-rate-limit-limit')] - Waiting [$($RateLimitWaitSeconds)] seconds"
                    Start-Sleep -Seconds $RateLimitWaitSeconds
                } else {
                    # Pagination - Skipping current turn to ensure clean data
                    Write-Verbose "Page: [$RunCount] - MaxPaginationCount: [$MaxPaginationCount]";$RunCount++
                    $functionlist = $functionlist + ($response.content | ConvertFrom-Json)
                    if ($null -ne ($response.RelationLink.next)) {
                        $uri = $response.RelationLink.next
                    } else {
                        $uri = $null
                    }
                }
            
            } catch {
                # Check for error & Rate limit
                if ($_.Exception.StatusCode -eq "TooManyRequests") {
                    # Wait until "x-rate-limit-reset"
                    $ErrorHeaderResetTime = ($_.Exception.Response.Headers).GetValues('x-rate-limit-reset')
                    $ErrorHeaderDateTime = ($_.Exception.Response.Headers).GetValues('date')                   
                    $RateLimitWaitSeconds = ($ErrorHeaderResetTime)[0] - (([DateTimeOffset](($ErrorHeaderDateTime)[0])).ToUnixTimeSeconds())
                    Write-Warning "Too Many requests - Waiting [$($RateLimitWaitSeconds)] seconds"
                    Start-Sleep -Seconds $RateLimitWaitSeconds
                } else {
                    #todo: Error value (.StatusCode) not available (NUll) in LINUX function
                    $TryError = $_.Exception
                    Write-Error "ERROR: [$($TryError.StatusCode.value__)] $($TryError.StatusCode) @ [$($uri)]"
                    Write-Debug "$($_.Exception.Message)"
                    $nexturi = $uri; $uri = $null
                    $IsErrorRun = $true
                }
            }
            
            # Stop pagination
            $PaginationCount++
            if ($PaginationCount -ge $MaxPaginationCount) {
                if ($uri -ne $null) {
                    Write-Warning "Paginationlimit of [$MaxPaginationCount] reached - Ignoring additional responses"
                }
                $nexturi = $uri; $uri = $null
                $ReachedMaxPagination = $true
            }
        }
        
        #* Return as selected
        if ($IsErrorRun -eq $false) {
            if (($ReachedMaxPagination -or $ReturnContext -or $ReturnContextOnly)) {
                #* Build Return
                if ($ReturnPaginationObjectsOnly -eq $true -and ($ReturnContextOnly -or $ReturnContext)) {
                    #* catch selection error
                    Write-Error "Invalid selection (Can not use PaginationObjectOnly with Context)"
                    return "Invalid selection (Can not use PaginationObjectOnly with Context)"
                } elseif ($ReturnPaginationObjectsOnly -eq $true) {
                    #* Only Objects
                    return $functionlist
                } elseif ($ReturnContextOnly) {
                    #* Ignore Pagination and objects
                    $ReturnBody = [ordered]@{
                        RateLimitTotal     = [int]($response.Headers.'x-rate-limit-limit')[0]
                        RateLimitRemaining = [int]($response.Headers.'x-rate-limit-remaining')[0]
                        RateLimitReset     = [int]($response.Headers.'x-rate-limit-reset')[0]
                        NextUri            = $nexturi
                    }

                    if ($nexturi.Length -gt 0) {
                        $ReturnBody.HasNextLink = $true
                    } else {
                        $ReturnBody.HasNextLink = $false
                    }

                    return $ReturnBody
                } else {
                    #* Build context using all relevant details
                    # Create Context
                    $ReturnBody = [ordered]@{ }
                
                    # Add PaginationLimit
                    if ($ReachedMaxPagination) {
                        if ($nexturi.Length -gt 0) {
                            $ReturnBody.Message = "Paginationlimit of [$MaxPaginationCount] reached - Ignoring additional responses"
                        } else {
                            $ReturnBody.Message = "Paginationlimit of [$MaxPaginationCount] reached - All values received"
                        }
                    }
                
                    # Add Context
                    if ($ReturnContext) {
                        $ReturnBody.RateLimitTotal = [int]($response.Headers.'x-rate-limit-limit')[0]
                        $ReturnBody.RateLimitRemaining = [int]($response.Headers.'x-rate-limit-remaining')[0]
                        $ReturnBody.RateLimitReset = [int]($response.Headers.'x-rate-limit-reset')[0]
                        $ReturnBody.NextUri = $nexturi
                    }
                
                    if ($nexturi.Length -gt 0) {
                        $ReturnBody.HasNextLink = $true
                    } else {
                        $ReturnBody.HasNextLink = $false
                    }
                
                    # Add Objects
                    $ReturnBody.ObjectCount = (($functionlist.count) + 0)
                    $ReturnBody.ObjectList = $functionlist
                
                    return $ReturnBody
                }              
            
            } else {
                # DEFAULT: Return objects
                return $functionlist
            }
        }
    } 
} #v2.4.1

Export-ModuleMember -Function *
Write-Verbose "Import-END| [$($FunctionScriptName)]"