GDAPRelationships.psm1

# This is a locally sourced Imports file for local development.
# It can be imported by the psm1 in local development to add script level variables.
# It will merged in the build process. This is for local development only.

# region script variables
# $script:resourcePath = "$PSScriptRoot\Resources"


class BoolParseTransformation : System.Management.Automation.ArgumentTransformationAttribute
{
    [Object] Transform([System.Management.Automation.EngineIntrinsics] $engineIntrinsics, [Object] $inputData)
    {
        $int = 0
        if ([int]::TryParse($inputData, [ref] $int))
        {
            return [bool] $int
        }

        return [bool]::Parse($inputData)
    }
}

<#
.SYNOPSIS
    Convert ISO8601 duration to a .NET timespan object

.DESCRIPTION
    Convert ISO8601 duration to a .NET timespan object
    https://en.wikipedia.org/wiki/ISO_8601#Durations

    A month is always 30 days
    A year is always 365 days
    No support for miliseconds

.PARAMETER Duration
    ISO8601 duration

.EXAMPLE
    Convert-ISO8601ToTimespan -Duration "PT39M6.3580667S"

.NOTES
    Copyright: (c) 2018 Fabian Bader
    License: MIT https://opensource.org/licenses/MIT
#>

Function Convert-ISO8601ToTimespan
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateScript(
            {
                if ($_ -match "^P(?<years>\d*Y)?(?<months>\d*M)?(?<days>\d*D)?(T)?(?<hours>\d*H)?(?<minutes>\d*M)?(?<seconds>[\d.]*S)?$" )
                {
                    $true
                }
                else
                {
                    throw "Not a valid ISO8601 duration"
                }
            }
        )]
        [string]$Duration
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Convert-ISO8601ToTimespan"
    }

    Process
    {
        if ($Duration -match "^P(?<years>\d*Y)?(?<months>\d*M)?(?<days>\d*D)?(T)?(?<hours>\d*H)?(?<minutes>\d*M)?(?<seconds>[\d.]*S)?$" )
        {
            # Write-LogMessage -Severity Verbose -Message "Converting ISO8601 $($Duration) date to TimeSpan"
            $years = [Int32]($matches['years'] -replace "[^\d.,]")
            $months = [Int32]($matches['months'] -replace "[^\d.,]")
            $days = [Int32]($matches['days'] -replace "[^\d.,]")
            $hours = [Int32]($matches['hours'] -replace "[^\d.,]")
            $minutes = [Int32]($matches['minutes'] -replace "[^\d.,]")
            $seconds = [Int32]($matches['seconds'] -replace "[^\d.,]")
            #region Convert years and month to days
            if ($years -gt 0)
            {
                $days = $years * 365 + $days
            }
            if ($months -gt 0)
            {
                $days = $months * 30 + $days
            }
            #endregion
            $TimeSpan = New-TimeSpan -Days $days -Hours $hours -Minutes $minutes -Seconds $seconds
        }
    }

    End
    {
        # Write-LogMessage -Severity Verbose -Message "Found TimeSpan of $TimeSpan"
        $PSCmdlet.WriteObject($TimeSpan, $true)
        # Write-LogMessage -Severity Verbose -Message "Ending Convert-ISO8601ToTimespan"
    }
}

Function Format-AccessAssignment
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [object]$AccessAssignment,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$GDAPRelationshipID,

        [Parameter()]
        [switch]$Detailed,

        [Parameter()]
        [switch]$Generalize,

        [Parameter(Mandatory)]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Format-AccessAssignment"
    }

    process
    {
        try
        {
            Write-LogMessage -Severity Verbose -Message "Processing accessAssignment $($AccessAssignment.id)"
            # Add detailed object information if the detailed flag is present
            if ($Detailed)
            {
                Write-LogMessage -Severity Verbose -Message "Detailed flag found, updating delegatedAdminAccessAssignment object with delegatedAdminRelationshipId, accessContainerDisplayName, and roleDefinition details"
                if (-not [string]::IsNullOrEmpty($GDAPRelationshipID) -and -not $Generalize)
                {
                    Add-Member -InputObject $AccessAssignment -Name "delegatedAdminRelationshipId" -MemberType NoteProperty -Value $GDAPRelationshipID
                }
                $GroupDisplayName = Get-EntraGroupbyNameorId -Group $AccessAssignment.accessContainer.accessContainerId -GraphBaseURL $GraphBaseURL -ErrorAction SilentlyContinue | Select-Object -Property displayName
                if (-not [string]::IsNullOrEmpty($GroupDisplayName.displayName))
                {
                    Add-Member -InputObject $AccessAssignment.accessContainer -Name "accessContainerDisplayName" -MemberType NoteProperty -Value $GroupDisplayName.displayName
                }
                $AccessAssignment.accessDetails.unifiedRoles = Get-GDAPAccessRolebyNameorId -RoleDefinition $AccessAssignment.accessDetails.unifiedRoles.roleDefinitionId
            }
            if ($Generalize)
            {
                $AccessAssignment = $AccessAssignment | Select-Object -Property accessContainer, accessDetails
            }
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }
    end
    {
        $PSCmdlet.WriteObject($AccessAssignment, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Format-AccessAssignment"
    }
} #Format-AccessAssignment

Function Format-AdminRelationship
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [object]$AdminRelationship,

        [Parameter()]
        [switch]$Detailed
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Format-AdminRelationship"
    }

    process
    {
        try
        {
            Write-LogMessage -Severity Verbose -Message "Processing adminRelationship $($AdminRelationship.id)"
            # Add detailed object information if the detailed flag is present
            if ($Detailed)
            {
                Write-LogMessage -Severity Verbose -Message "Detailed flag found, updating delegatedAdminRelationship object with roleDefinition details"
                $AdminRelationship.accessDetails.unifiedRoles = Get-GDAPAccessRolebyNameorId -RoleDefinition $AdminRelationship.accessDetails.unifiedRoles.roleDefinitionId
            }
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }
    end
    {
        $PSCmdlet.WriteObject($AdminRelationship, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Format-AdminRelationship"
    }
} #Format-AdminRelationship

Function Get-CallerPreference
{
    <#
    .Synopsis
       Fetches "Preference" variable values from the caller's scope.
    .DESCRIPTION
       Script module functions do not automatically inherit their caller's variables, but they can be
       obtained through the $PSCmdlet variable in Advanced Functions. This function is a helper function
       for any script module Advanced Function; by passing in the values of $ExecutionContext.SessionState
       and $PSCmdlet, Get-CallerPreference will set the caller's preference variables locally.
    .PARAMETER Cmdlet
       The $PSCmdlet object from a script module Advanced Function.
    .PARAMETER SessionState
       The $ExecutionContext.SessionState object from a script module Advanced Function. This is how the
       Get-CallerPreference function sets variables in its callers' scope, even if that caller is in a different
       script module.
    .PARAMETER Name
       Optional array of parameter names to retrieve from the caller's scope. Default is to retrieve all
       Preference variables as defined in the about_Preference_Variables help file (as of PowerShell 4.0)
       This parameter may also specify names of variables that are not in the about_Preference_Variables
       help file, and the function will retrieve and set those as well.
    .EXAMPLE
       Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

       Imports the default PowerShell preference variables from the caller into the local scope.
    .EXAMPLE
       Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -Name 'ErrorActionPreference','SomeOtherVariable'

       Imports only the ErrorActionPreference and SomeOtherVariable variables into the local scope.
    .EXAMPLE
       'ErrorActionPreference','SomeOtherVariable' | Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

       Same as Example 2, but sends variable names to the Name parameter via pipeline input.
    .INPUTS
       String
    .OUTPUTS
       None. This function does not produce pipeline output.
    .LINK
       about_Preference_Variables
    .LINK
        https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
    #>


    [CmdletBinding(DefaultParameterSetName = 'AllVariables')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ $_.GetType().FullName -eq 'System.Management.Automation.PSScriptCmdlet' })]
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.SessionState]
        $SessionState,

        [Parameter(ParameterSetName = 'Filtered', ValueFromPipeline = $true)]
        [string[]]
        $Name
    )

    begin
    {
        $filterHash = @{}
    }

    process
    {
        if ($null -ne $Name)
        {
            foreach ($string in $Name)
            {
                $filterHash[$string] = $true
            }
        }
    }

    end
    {
        # List of preference variables taken from the about_Preference_Variables help file in PowerShell version 4.0

        $vars = @{
            'ErrorView'                     = $null
            'FormatEnumerationLimit'        = $null
            'LogCommandHealthEvent'         = $null
            'LogCommandLifecycleEvent'      = $null
            'LogEngineHealthEvent'          = $null
            'LogEngineLifecycleEvent'       = $null
            'LogProviderHealthEvent'        = $null
            'LogProviderLifecycleEvent'     = $null
            'MaximumAliasCount'             = $null
            'MaximumDriveCount'             = $null
            'MaximumErrorCount'             = $null
            'MaximumFunctionCount'          = $null
            'MaximumHistoryCount'           = $null
            'MaximumVariableCount'          = $null
            'OFS'                           = $null
            'OutputEncoding'                = $null
            'ProgressPreference'            = $null
            'PSDefaultParameterValues'      = $null
            'PSEmailServer'                 = $null
            'PSModuleAutoLoadingPreference' = $null
            'PSSessionApplicationName'      = $null
            'PSSessionConfigurationName'    = $null
            'PSSessionOption'               = $null

            'ErrorActionPreference'         = 'ErrorAction'
            'DebugPreference'               = 'Debug'
            'ConfirmPreference'             = 'Confirm'
            'WhatIfPreference'              = 'WhatIf'
            'VerbosePreference'             = 'Verbose'
            'InformationPreference'         = 'InformationAction'
            'WarningPreference'             = 'WarningAction'
        }

        foreach ($entry in $vars.GetEnumerator())
        {
            if (([string]::IsNullOrEmpty($entry.Value) -or -not $Cmdlet.MyInvocation.BoundParameters.ContainsKey($entry.Value)) -and
                ($PSCmdlet.ParameterSetName -eq 'AllVariables' -or $filterHash.ContainsKey($entry.Name)))
            {
                $variable = $Cmdlet.SessionState.PSVariable.Get($entry.Key)

                if ($null -ne $variable)
                {
                    if ($SessionState -eq $ExecutionContext.SessionState)
                    {
                        Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                    }
                    else
                    {
                        $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                    }
                }
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'Filtered')
        {
            foreach ($varName in $filterHash.Keys)
            {
                if (-not $vars.ContainsKey($varName))
                {
                    $variable = $Cmdlet.SessionState.PSVariable.Get($varName)

                    if ($null -ne $variable)
                    {
                        if ($SessionState -eq $ExecutionContext.SessionState)
                        {
                            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                        }
                        else
                        {
                            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                        }
                    }
                }
            }
        }

    } # end

} #Get-CallerPreference


Function Get-CompletionCommand
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory)]
        [string]$CommandInvocation,
        [Parameter(Mandatory)]
        [object[]]$CommandArgumentParameters,
        [Parameter(Mandatory)]
        [string]$GDAPRelationshipId,
        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$LogFilePath
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        # Get this function's invocation as a command line
        # with literal (expanded) values.
        $CommandArguments = $(foreach ($bp in $CommandArgumentParameters)
            {
                # argument list
                $valRep =
                if ($bp.Value -isnot [switch])
                {
                    foreach ($val in $bp.Value)
                    {
                        if ($val -is [bool])
                        {
                            # a Boolean parameter (rare)
                  ('$false', '$true')[$val] # Booleans must be represented this way.
                        }
                        else
                        {
                            # all other types: stringify in a culture-invariant manner.
                            if (-not ($val.GetType().IsPrimitive -or $val.GetType() -in [string], [datetime], [datetimeoffset], [decimal], [bigint]))
                            {
                                Write-LogMessage -Severity Warning -Message "Argument of type [$($val.GetType().FullName)] will likely not round-trip correctly; stringifies to: $val"
                            }
                            # Single-quote the (stringified) value only if necessary
                            # (if it contains argument-mode metacharacters).
                            if ($val -match '[ $''"`,;(){}|&<>@#]') { "'{0}'" -f ($val -replace "'", "''") }
                            else { "$val" }
                        }
                    }
                }
                # Synthesize the parameter-value representation.
                [PSCustomObject]@{
                    Parameter = $bp.Key
                    Value     = ($valRep -join ', ')
                }
            })
    }
    process
    {
        try
        {
            [string]$CompletionCommand = "$($CommandInvocation)"

            $CommandArguments | ForEach-Object {
                switch ($_.Parameter)
                {
                    "SecurityGroupsJSONURL"
                    {
                        $CompletionCommand += " -SecurityGroupsJSONURL $($_.Value)"
                    }
                    "GraphBaseURL"
                    {
                        $CompletionCommand += " -GraphBaseURL $($_.Value)"
                    }
                    "Simplified"
                    {
                        if ($_.Value -ne $false) { $CompletionCommand += " -Simplified" }
                    }
                    "TerminateExisting"
                    {
                        if ($_.Value -ne $false) { $CompletionCommand += " -TerminateExisting" }
                    }
                }
            }

            if ($LogFilePath)
            {
                $CompletionCommand += " -LogFilePath '$($LogFilePath)'"
            }

            $CompletionCommand += " -GDAPRelationshipID '$($GDAPRelationshipId)'"
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }
    end
    {
        if (-not ([string]::IsNullOrEmpty($CompletionCommand)))
        {
            $PSCmdlet.WriteObject($AccessAssignment, $true)
        }
    }
} #Get-CompletionCommand

Function Get-EntraGroupbyNameorId
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Group,

        [Parameter(Mandatory)]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-EntraGroupbyNameorId"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }
    process
    {
        try
        {
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }
            if (Test-Guid $Group)
            {
                $GroupLookupURL = "$($GraphBaseURL)groups/$([System.Uri]::EscapeDataString($Group.Trim()))?`$select=id,displayName,groupTypes,mailEnabled,securityEnabled"
            }
            else
            {
                $GroupLookupURL = "$($GraphBaseURL)groups?`$filter=displayName eq ('$([System.Uri]::EscapeDataString($Group.Trim()))')&`$select=id,displayName,groupTypes,mailEnabled,securityEnabled"
            }

            Write-LogMessage -Severity Verbose -Message "Retrieving group information from Graph API: 'Invoke-MgGraphRequest -Method Get -Uri $GroupLookupURL -OutputType PSObject'"
            $GroupInformation = Invoke-MgGraphRequest -Method Get -Uri $GroupLookupURL -OutputType PSObject
            Write-LogMessage -Severity Debug -Message "Result:`n $($GroupInformation | ConvertTo-Json -Depth 5)"

            $GroupObject = $null

            if ($GroupInformation.value)
            {
                $GroupObject = $GroupInformation.value | Sort-Object -Property @{Expression = { $_.securityEnabled }; Descending = $true }, @{Expression = { $_.groupTypes }; Descending = $true }, @{Expression = { $_.mailEnabled }; Descending = $true } | Select-Object -First 1 -Property id, displayName
            }
            elseif (-not ([string]::IsNullOrEmpty($GroupInformation.id)))
            {
                $GroupObject = $GroupInformation | Select-Object -Property id, displayName
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when retrieving Graph Groups, status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }
    end
    {
        $PSCmdlet.WriteObject($GroupObject, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Get-EntraGroupbyNameorId"
    }
} #Get-EntraGroupbyNameorId

Function Get-ExistingCustomer
{
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[object]])]
    param(
        [Parameter(Mandatory)]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-ExistingCustomer"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }

        if ($GraphBaseURL -notmatch '.*/$')
        {
            $GraphBaseURL = $GraphBaseURL + "/"
        }
        $ExistingCustomersURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminCustomers"
    }
    process
    {
        try
        {
            Write-LogMessage -Severity Verbose -Message "Getting existing customers from Graph API: 'Invoke-MgGraphRequest -Method GET -Uri $ExistingCustomersURL -OutputType PSObject'"
            [System.Collections.Generic.List[object]]$ExistingCustomersObjects = (Invoke-MgGraphRequest -Method GET -Uri $ExistingCustomersURL -OutputType PSObject).value
            Write-LogMessage -Severity Debug -Message "Result:`n $($ExistingCustomersObjects | ConvertTo-Json -Depth 5)"
            Write-LogMessage -Severity Verbose -Message "Generating and displaying GridView for customer selection"
            [System.Collections.Generic.List[object]]$SelectedCustomerObject = $ExistingCustomersObjects | Select-Object -Property displayName, tenantId | Sort-Object -Property displayName | Out-GridView -Title 'Select Customer' -PassThru
            Write-LogMessage -Severity Debug -Message "Result:`n $($SelectedCustomerObject | ConvertTo-Json -Depth 5)"
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "Unable to retrieve existing customers from $($ExistingCustomersURL), status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }
    end
    {
        if ($null -ne $SelectedCustomerObject)
        {
            $PSCmdlet.WriteObject($SelectedCustomerObject, $true)
        }
        else
        {
            throw "No existing customer object selected, please run the script again or remove the `"-SelectFromExistingCustomers`" flag."
        }
        Write-LogMessage -Severity Verbose -Message "Ending Get-ExistingCustomer"
    }
} #Get-ExistingCustomer

Function Get-HttpQueryStringList
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Scope = 'Function')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ } )]
        $Url
    )

    Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $Url -split "[?&]" -like "*=*" | ForEach-Object -Begin { $h = @{} } -Process { $h[($_ -split "=", 2 | Select-Object -Index 0)] = ($_ -split "=", 2 | Select-Object -Index 1) } -End { $h }
} #Get-HttpQueryStrings

Function Get-JSONFromURL
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory)]
        [string]$JSONFileURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-JSONFromURL"
    }
    process
    {
        try
        {
            Write-LogMessage -Severity Verbose -Message "Attempting to download JSON file from $($JSONFileURL) using Invoke-RestMethod"
            $JSONObject = Invoke-RestMethod -Method Get -Uri $JSONFileURL
            if ($JSONObject.GetType().Name -eq "String")
            {
                $JSONObject = $JSONObject.Substring(1) | ConvertFrom-Json
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "Unable to download JSON file from $($JSONFileURL), status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }
    end
    {
        $PSCmdlet.WriteObject($JSONObject, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Get-JSONFromURL"
    }
} #Get-JSONFromURL

function Get-MgGraphAllPaging
{
    [CmdletBinding(
        ConfirmImpact = 'Medium'
    )]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [object]$SearchResult
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-MgGraphAllPaging"
    }

    process
    {
        # Set the current page to the search result provided
        $page = $SearchResult

        # Extract the NextLink
        $currentNextLink = $page.'@odata.nextLink'

        #let's check for nextlinks specifically as a hashtable key
        if (Get-Member -InputObject $page -Name "@odata.count" -MemberType Properties)
        {
            Write-LogMessage -Severity Verbose -Message "First page value count: $($Page.'@odata.count')"
        }

        if ((Get-Member -InputObject $page -Name "@odata.nextLink" -MemberType Properties) -or (Get-Member -InputObject $page -Name "value" -MemberType Properties))
        {
            $values = $page.value
        }
        else
        {
            # set value to a single item if there is only 1 page
            $values = $page
        }

        # Output the values
        if ($values)
        {
            $PSCmdlet.WriteObject($values, $true)
        }


        while (-Not ([string]::IsNullOrWhiteSpace($currentNextLink)))
        {
            # Make the call to get the next page
            try
            {
                $page = Invoke-MgGraphRequest -Uri $currentNextLink -Method GET -OutputType PSObject
            }
            catch
            {
                throw $_
            }

            # Extract the NextLink
            $currentNextLink = $page.'@odata.nextLink'

            # Output the items in the page
            $values = $page.value

            if (Get-Member -InputObject $page -Name "@odata.count" -MemberType Properties)
            {
                Write-LogMessage -Severity Verbose -Message "Current page value count: $($Page.'@odata.count')"
            }

            # Output the values
            if ($values)
            {
                $PSCmdlet.WriteObject($values, $true)
            }
        }
    }

    end
    {
        Write-LogMessage -Severity Verbose -Message "Ending Get-MgGraphAllPaging"
    }
} #Get-MgGraphAllPaging

Function Get-PSUnique
{
    [cmdletbinding()]
    [alias("gpsu")]
    [OutputType("object")]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [object]$InputObject
    )

    Begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting $($MyInvocation.MyCommand)"
        Write-LogMessage -Severity Debug -Message "Initializing list"
        $UniqueList = [System.Collections.Generic.List[object]]::new()
    } #begin

    Process
    {
        foreach ($item in $InputObject)
        {
            if ($UniqueList.Exists( { -not(Compare-Object $args[0].PSObject.properties.value $item.PSObject.Properties.value) }))
            {
                Write-LogMessage -Severity Debug -Message "Skipping: $($item |Out-String)"
            }
            else
            {
                Write-LogMessage -Severity Debug -Message "Adding as unique: $($item | Out-String)"
                $UniqueList.Add($item)
            }
        }
    } #process

    End
    {
        Write-LogMessage -Severity Verbose -Message "Found $($UniqueList.count) unique objects"
        Write-LogMessage -Severity Debug -Message "Writing results to the pipeline"
        $PSCmdlet.WriteObject($UniqueList, $true)
        Write-LogMessage -Severity Verbose -Message "Ending $($MyInvocation.MyCommand)"
    } #end
} #Get-PSUnique

Function Get-TenantDisplayNamebyId
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory)]
        [ValidateScript( { Test-Guid -InputObject $_ })]
        [string]$TenantID,

        [Parameter(Mandatory)]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-TenantDisplayNamebyId"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }
    process
    {
        try
        {
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }
            $TenantInformationbyIDURL = "$($GraphBaseURL)tenantRelationships/findTenantInformationByTenantId(tenantId='$([uri]::EscapeDataString($TenantID.Trim()))')"

            Write-LogMessage -Severity Verbose -Message "Retrieving tenant Display Name from Graph API: 'Invoke-MgGraphRequest -Method Get -Uri $TenantInformationbyIDURL -OutputType PSObject'"
            $TenantInformation = Invoke-MgGraphRequest -Method Get -Uri $TenantInformationbyIDURL -OutputType PSObject
            Write-LogMessage -Severity Debug -Message "Result:`n $($TenantInformation | ConvertTo-Json -Depth 5)"
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when generating GDAP relationship request, status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }

    end
    {
        if (-not ([string]::IsNullOrEmpty($TenantInformation.displayName)))
        {
            $PSCmdlet.WriteObject($TenantInformation.displayName, $true)
        }
        Write-LogMessage -Severity Verbose -Message "Ending Get-TenantDisplayNamebyId"
    }
} #Get-TenantDisplayNamebyId

# Function New-GDAPAction
# {
# [CmdletBinding()]
# param(
# [Parameter(Mandatory)]
# [string]$ActionFile,

# [Parameter(Mandatory)]
# [object]$ActionFileId,

# [Parameter(Mandatory)]
# [ValidateSet('New-GDAPRelationship', 'New-GDAPRelationshipAccessAssignment', 'Get-GDAPRelationship', 'Get-GDAPRelationshipAccessAssignment', 'Get-GDAPRelationshipRequestLink', 'Remove-GDAPRelationship', 'Remove-GDAPRelationshipAccessAssignment', 'Test-GDAPRelationshipStatus')]
# [string]$Action,

# [Parameter(Mandatory)]
# [object]$Parameters,

# [Parameter(Mandatory = $false)]
# [object]$ValueFrom,

# [Parameter(Mandatory)]
# [int]$Stage
# )

# $AllowedParameters = (Get-Command -Name $Action).Parameters
# [System.Collections.Generic.List[string]]$ParameterString = $Parameters | Where-Object { $_.Parameter -in $AllowedParameters.Keys } | ForEach-Object {
# "-$($_.Parameter)$(if ($AllowedParameters."$($_.Parameter)".SwitchParameter) { ":`$$($_.Value)"} else { " $($_.Value)"})"
# }
# if ($ValueFrom)
# {
# $After = $ValueFrom.ActionId
# $InputValue = Get-GDAPAction -ActionFile $ActionFile -Action $ValueFrom.ActionId

# $ValueFrom.Parameters | Where-Object { $_.Parameter -in $AllowedParameters.Keys } | ForEach-Object {
# "-$($_.Parameter)$(if ($AllowedParameters."$($_.Parameter)".SwitchParameter) { ":`$$($InputValue.Result."($($_.Value))")"} else { " $($InputValue.Result."($($_.Value))")"})"
# }
# }
# $CommandString = "$($Action) $($ParameterString -join ' ')"
# $ScriptBlock = [Scriptblock]::Create($CommandString)

# switch ($Action)
# {
# "New-GDAPRelationship"
# {
# $ExpectedResult = [System.Collections.Generic.List[object]]::new(@(
# @{
# Command = [Scriptblock]::Create('-not ([string]::IsNullorEmpty(($Result.id)))')
# Value = $true
# },
# @{
# Command = [Scriptblock]::Create('$Result.status -iin "approvalPending","approved","active"')
# Value = $true
# },
# @{
# Command = [Scriptblock]::Create('($Result.accessDetails.unifiedRoles).Count -eq ($Input.RoleDefinition).Count')
# Value = $true
# }
# ))

# $CheckStatusCommand = [Scriptblock]::Create('Get-GDAPRelationship -GDAPRelationshipID $Result.id -GraphBaseURL $Input.GraphBaseURL')
# }

# "Get-GDAPRelationshipRequestLink"
# {
# $ExpectedResult = [System.Collections.Generic.List[object]]::new(@(
# @{
# Command = [Scriptblock]::Create('-not ([string]::IsNullorEmpty(($Result.GDAPInvitationLink)))')
# Value = $true
# }
# ))
# }

# "New-GDAPRelationshipAccessAssignment"
# {
# $ExpectedResult = [System.Collections.Generic.List[object]]::new(@(
# @{
# Command = [Scriptblock]::Create('-not ([string]::IsNullorEmpty(($Result.id)))')
# Value = $true
# },
# @{
# Command = [Scriptblock]::Create('$Result.accessContainer.accessContainerId -eq $Input.Group -or $Result.accessContainer.accessContainerDisplayName -eq $Input.Group')
# Value = $true
# },
# @{
# Command = [Scriptblock]::Create('($Result.accessDetails.unifiedRoles).Count -eq ($Input.RoleDefinition).Count')
# Value = $true
# }
# ))

# $CheckStatusCommand = [Scriptblock]::Create('Get-GDAPRelationshipAccessAssignment -GDAPRelationshipID $Input.GDAPRelationshipId -Group $Input.Group -GraphBaseURL $Input.GraphBaseURL')
# }

# "Remove-GDAPRelationship"
# {
# $ExpectedResult = [System.Collections.Generic.List[object]]::new(@(
# @{
# Command = [Scriptblock]::Create('$Result')
# Value = $true
# }
# ))

# $CheckStatusCommand = [Scriptblock]::Create('(Get-GDAPRelationship -GDAPRelationshipID $Input.GDAPRelationshipObject.id -GraphBaseURL $Input.GraphBaseURL).status -iin "terminated","terminating,"terminationRequested"')
# }

# "Remove-GDAPRelationshipAccessAssignment"
# {
# $ExpectedResult = [System.Collections.Generic.List[object]]::new(@(
# @{
# Command = [Scriptblock]::Create('$Result')
# Value = $true
# }
# ))

# $CheckStatusCommand = [Scriptblock]::Create('(Get-GDAPRelationshipAccessAssignment -GDAPRelationshipID $Input.AccessAssignmentObject.delegatedAdminRelationshipId -AccessAssignmentId $Input.AccessAssignmentObject.id -GraphBaseURL $Input.GraphBaseURL).status -iin "deleting","deleted"')
# }
# }

# $ActionObject = @{
# Action = $Action
# Parameters = $Parameters
# Command = $CommandString
# Execute = $ScriptBlock
# ValueFrom = $ValueFrom
# ExpectedResult = $ExpectedResult
# CheckStatusCommand = $CheckStatusCommand
# Stage = $Stage
# After = $After
# ActionId = (New-Guid).Guid
# }

# $PSCmdlet.WriteObject($ActionObject, $true)
# } #New-GDAPAction

Function New-GDAPRelationshipRequest
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([object])]
    param (
        [Parameter(Mandatory)]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory)]
        [ValidateSet('lockForApproval', 'terminate', 'approve', 'reject')]
        [string]$Action,

        [Parameter(Mandatory)]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting New-GDAPRelationshipRequest"

        $DelegatedAdminCreateRequestURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships/$($GDAPRelationshipID)/requests"
    }
    process
    {
        try
        {
            $DelegatedAdminRelationshipRequestBody = @{
                action = $Action
            }

            # Verify that the base URL ends in trailing slash
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }

            Write-LogMessage -Severity Verbose -Message "Creating new GDAP request from Graph API: 'Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminCreateRequestURL -Body (`$DelegatedAdminRelationshipRequestBody | ConvertTo-Json) -OutputType PSObject'"
            Write-LogMessage -Severity Debug -Message "Graph Request Body value for new request:`n$($DelegatedAdminRelationshipRequestBody | ConvertTo-Json)"
            if ($PSCmdlet.ShouldProcess(("Request body: `n$($DelegatedAdminRelationshipRequestBody | ConvertTo-Json)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminCreateRequestURL")))
            {
                $GDAPRelationshipRequest = Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminCreateRequestURL -Body ($DelegatedAdminRelationshipRequestBody | ConvertTo-Json -Depth 5) -StatusCodeVariable IMGRStatusCode -OutputType PSObject
                Write-LogMessage -Severity Debug -Message "Result:`n $($GDAPRelationshipRequest | ConvertTo-Json -Depth 5)"

                # Validate the relationship request ID is valid before returning
                if ($IMGRStatusCode -eq '201')
                {
                    Write-LogMessage -Severity Verbose -Message "Successfully created GDAP relationship request $($GDAPRelationshipObject.id)"
                    return $GDAPRelationshipRequest
                }
                else
                {
                    Write-LogMessage -Severity Warning -Message "Graph API status code: $($IMGRStatusCode)"
                    throw "GDAP Relationship Request was not created successfully."
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when generating GDAP relationship request, status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }

    end
    {
        Write-LogMessage -Severity Verbose -Message "Ending New-GDAPRelationshipRequest"
    }
} #New-GDAPRelationshipRequest

Function Test-AccessAssignment
{
    [CmdletBinding()]
    [OutputType([bool])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'GDAPRoles', Justification = 'False positive as rule missing valid calls outside if scopes')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ValidTemplate', Justification = 'False positive as rule missing valid calls outside if scopes')]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [object]$AccessAssignment,

        [Parameter(Mandatory)]
        [string]$GraphBaseURL
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Test-AccessAssignment"

        if (-not $script:GDAPRoleList)
        {
            Write-LogMessage -Severity Verbose -Message "Retrieving the list of GDAP Roles"
            $GDAPRoles = Import-GDAPRoleList
        }
        else
        {
            Write-LogMessage -Severity Verbose -Message "GDAP Roles already loaded, continuing"
            $GDAPRoles = $script:GDAPRoleList
        }
    }

    process
    {
        $ValidTemplate = $true
        if ($AccessAssignment.PSobject.Properties.Name -notcontains "accessContainer") { $ValidTemplate = $false; Throw "AccessAssignment is missing the accessContainer property" }
        if ($AccessAssignment.accessContainer.PSobject.Properties.Name -notcontains "accessContainerId") { $ValidTemplate = $false; Throw "AccessAssignment is missing the accessContainerId property" }
        if ($AccessAssignment.PSobject.Properties.Name -notcontains "accessDetails") { $ValidTemplate = $false; Throw "AccessAssignment is missing the accessDetails property" }
        if ($AccessAssignment.accessDetails.PSobject.Properties.Name -notcontains "unifiedRoles") { $ValidTemplate = $false; Throw "AccessAssignment is missing the unifiedRoles property" }

        if (-not (Test-Guid $AccessAssignment.accessContainer.accessContainerId)) { $ValidTemplate = $false; Throw "accessContainerId `"$($AccessAssignment.accessContainer.accessContainerId)`" is not in GUID format" }
        if (-not (Get-EntraGroupbyNameorId -Group $AccessAssignment.accessContainer.accessContainerId -GraphBaseURL $GraphBaseURL)) { $ValidTemplate = $false; Throw "accessContainerId `"$($AccessAssignment.accessContainer.accessContainerId)`" does not match an existing Entra ID group object" }

        $AccessAssignment.accessDetails.unifiedRoles | ForEach-Object {
            if ($_.PSobject.Properties.Name -notcontains "roleDefinitionId") { $ValidTemplate = $false; Throw "Found a unifiedRoles object missing a roleDefinitionId: $_" }
            if (-not (Test-Guid $_.roleDefinitionId)) { $ValidTemplate = $false; Throw "Found a unifiedRoles object with a roleDefinitionId not in GUID format: $_" }
            if ($_.roleDefinitionId -notin $GDAPRoleList.roleDefinitionId) { $ValidTemplate = $false; Throw "Found a unifiedRoles object with a roleDefinitionId `"$($_.roleDefinitionId) that does not match a known Entra ID Role" }
        }

    }

    end
    {
        $ValidTemplate
        Write-LogMessage -Severity Verbose -Message "Ending Test-AccessAssignment"
    }
} #Test-AccessAssignment

<#
.SYNOPSIS
    Validates a given input string and checks string is a valid GUID
.DESCRIPTION
    Validates a given input string and checks string is a valid GUID by using the .NET method Guid.TryParse
.EXAMPLE
    PS> Test-Guid -InputObject "3363e9e1-00d8-45a1-9c0c-b93ee03f8c13"
.NOTES
    Uses .NET method [guid]::TryParse()
.LINK
    Adapted from code by Nicola Suter: https://tech.nicolonsky.ch/validating-a-guid-with-powershell/
#>

Function Test-Guid
{
    [Cmdletbinding()]
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [string]$InputObject
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }
    process
    {
        [guid]::TryParse($InputObject, $([ref][guid]::Empty))
    }
} #Test-Guid

Function Test-Url
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String] $Url
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    Process
    {
        if ([system.uri]::IsWellFormedUriString($Url, [System.UriKind]::Absolute))
        {
            $true
        }
        else
        {
            $false
        }
    }
} #Test-Url

Function Write-LogMessage
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateSet('Debug', 'Verbose', 'Information', 'Warning', 'Error', IgnoreCase = $false)]
        [string]$Severity = "Information",
        [Parameter(Mandatory)]
        [string]$Message,
        [Parameter(Mandatory = $false)]
        [string]$RecommendedAction,
        [Parameter(Mandatory = $false)]
        [object]$TargetObject,
        [Parameter(Mandatory = $false)]
        [switch]$StopOnError,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.ErrorRecord]$LastException
    )

    Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $LogObject = [PSCustomObject]@{
        Timestamp         = (Get-Date).ToString()
        Severity          = $Severity
        Message           = $Message
        RecommendedAction = $RecommendedAction
        TargetObject      = $TargetObject
    }

    if ($Severity -in "Debug", "Verbose", "Error")
    {
        $CallStackDepth = 0
        $fullCallStack = Get-PSCallStack
        $CallingFunction = $fullCallStack[1].FunctionName

        if ($CallingFunction -eq "<ScriptBlock>")
        {
            $CallingFunction = "$($fullCallStack[1].Command)$($CallingFunction)"
        }
        $LogObject | Add-Member -MemberType NoteProperty -Name CallingFunction -Value $CallingFunction
    }

    if ($Severity -eq "Error")
    {

        if ($LastException.ErrorRecord)
        {
            #PSCore Error
            $LastError = $LastException.ErrorRecord
        }
        else
        {
            #PS 5.1 Error
            $LastError = $LastException
        }


        if ($LastException.InvocationInfo.MyCommand.Version)
        {
            $version = $LastError.InvocationInfo.MyCommand.Version.ToString()
        }
        if ($LastError)
        {
            $LastErrorObject = @{
                'ExceptionMessage'    = $LastError.Exception.Message
                'ExceptionSource'     = $LastError.Exception.Source
                'ExceptionStackTrace' = $LastError.Exception.StackTrace
                'PositionMessage'     = $LastError.InvocationInfo.PositionMessage
                'InvocationName'      = $LastError.InvocationInfo.InvocationName
                'MyCommandVersion'    = $version
                'ScriptName'          = $LastError.InvocationInfo.ScriptName
            }

            $LogObject | Add-Member -MemberType NoteProperty -Name LastError -Value $LastErrorObject
        }


    }
    if ($Severity -in "Debug", "Error")
    {
        $FullCallStackWithoutLogFunction = $fullCallStack | ForEach-Object {
            #loop through all the objects in the callstack result.
            #excluding the 0 position of the call stack which would represent this write-logmessage function.
            if ($CallStackDepth -gt 0)
            {
                [PSCustomObject]@{
                    CallStackDepth   = $CallStackDepth
                    ScriptLineNumber = $_.ScriptLineNumber
                    FunctionName     = $_.FunctionName
                    ScriptName       = $_.ScriptName
                    Location         = $_.Location
                    Command          = $_.Command
                    Arguments        = $_.Arguments
                }
            }
            $CallStackDepth++
        }

        $LogObject | Add-Member -MemberType NoteProperty -Name fullCallStackDump -Value $FullCallStackWithoutLogFunction
    }

    switch ($Severity)
    {
        'Debug'
        {
            $DebugSplat = @{
                Message = "$($LogObject.Timestamp) CallingFunction=$($LogObject.CallingFunction)`n $($LogObject.Message)`n $($LogObject.fullCallStackDump | ConvertTo-Json -Depth 5 | Out-String)"
            }
            Write-Debug @DebugSplat
        }
        'Verbose'
        {
            $VerboseSplat = @{
                Message = "$($LogObject.Timestamp) CallingFunction=$($LogObject.CallingFunction)`n $($LogObject.Message)"
            }
            Write-Verbose @VerboseSplat
        }
        'Information'
        {
            $InformationSplat = @{
                MessageData = "INFORMATION: $($LogObject.Timestamp)`n $($LogObject.Message)"
            }
            Write-Information @InformationSplat
        }
        'Warning'
        {
            $WarningSplat = @{
                Message = "$($LogObject.Timestamp)`n $($LogObject.Message)"
            }
            Write-Warning @WarningSplat
        }
        'Error'
        {
            $ErrorSplat = @{
                Message           = "$($LogObject.Timestamp) CallingFunction=$($LogObject.CallingFunction)`n $($LogObject.Message)$(if($LogObject.LastError){ "`n $($LogObject.LastError | ConvertTo-Json -Depth 5 | Out-String)" })"
                RecommendedAction = $LogObject.RecommendedAction
                TargetObject      = $LogObject.TargetObject
            }
            if ($StopOnError)
            {
                $ErrorSplat.ErrorAction = "Stop"
            }
            Write-Error @ErrorSplat
        }
    }
} #Write-LogMessage

<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Build-GDAPRemediation
{

    [CmdletBinding()]
    param(
        # [Parameter(Mandatory,
        # HelpMessage = "The GDAP Template object containing the specifics for the remediation to operate against")]
        # [ValidateNotNullOrEmpty()]
        # [object]$GDAPTemplate,

        # [Parameter(Mandatory,
        # HelpMessage = "List of client tenants that the remediation should operate against, requires property `"ClientTenantId`", `"ClientTenantName`" is optional but recommended. `"RelationshipName`" can be specified to manually set the created relationship name")]
        # [ValidateScript({
        # if ([string]::IsNullOrEmpty($_.ClientTenantId))
        # {
        # throw "Passed ClientTenant object is not valid, property `"ClientTenantId`" is missing or empty"
        # }
        # elseif (-not (Test-Guid -InputObject $_.ClientTenantId))
        # {
        # throw "Passed ClientTenant object is not valid, property `"ClientTenantId`" with value `"$($_.ClientTenantId)`" is not in guid format"
        # }
        # else
        # {
        # $true
        # }
        # })]
        # [System.Collections.Generic.List[object]]$ClientTenant,

        # [Parameter(Mandatory = $false,
        # HelpMessage = "The prefix to use in generated GDAP relationship names")]
        # [ValidateNotNullOrEmpty()]
        # [string]$RelationshipPrefix,

        # [Parameter(Mandatory = $false)]
        # [ValidateNotNullOrEmpty()]
        # [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )

    begin
    {
        # Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        # Write-LogMessage -Severity Verbose -Message "Starting Build-GDAPRemediation"
        # if (-not (Get-MgContext))
        # {
        # Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        # }
    }

    process
    {
        # Verify that the base URL ends in trailing slash
        # if ($GraphBaseURL -notmatch '.*/$')
        # {
        # $GraphBaseURL = $GraphBaseURL + "/"
        # }

        # Write-LogMessage -Severity Information -Message "Retrieving current GDAP Relationships for $($ClientTenant.Count) Client Tenant(s)"
        # [System.Collections.Generic.List[object]]$ClientRelationship = $ClientTenant | ForEach-Object {
        # $_.ExistingRelationship = (Get-GDAPRelationship -Filter "contains(customer/tenantId,'$($ClientTenant.ClientTenantID)')" -GraphBaseURL $GraphBaseURL)
        # }
        # Write-LogMessage -Severity Information -Message "Retrieved $($ClientRelationship.Count) existing GDAP Relationship(s)"

        # $ActiveRelationshipStatus = [System.Collections.Generic.List[object]]::new()

        # $ClientRelationship | ForEach-Object {
        # if ($_.status -in 'active', 'approved', 'activating')
        # {
        # Write-LogMessage -Severity Information -Message "Testing status of relationship with ID $($_.id)"

        # $TestResults = Test-GDAPRelationshipStatus -GDAPRelationshipID $_.id -DelegatedAdminAccessAssignment $GDAPTemplate.AccessAssignment -RoleDefinition $GDAPTemplate.RoleDefinition -Differences
        # $ActiveRelationshipStatus.Add($TestResults)
        # }
        # }
    }

    end
    {
        Write-LogMessage -Severity Verbose -Message "Ending Build-GDAPRemediation"
    }

} #Build-GDAPRemediation


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Compare-GDAPAccessAssignment
{

    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID to use for accessAssignments lookup")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory, Position = 1, ParameterSetName = "Object", ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Object containing a delegatedAdminAccessAssignment object to compare")]
        [object]$DelegatedAdminAccessAssignment,

        [Parameter(Mandatory, Position = 1, ParameterSetName = "Separate", ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Entra ID Group by ID or Name to use to search for existing accessAssignments")]
        [ValidateNotNullOrEmpty()]
        [string]$Group,

        [Parameter(Mandatory, Position = 2, ParameterSetName = "Separate", ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Array of Entra ID role Guids or role Names to compare to the existing accessAssignments")]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Generic.List[string]]$RoleDefinition,

        [Parameter(Mandatory = $false, ParameterSetName = "Object", ValueFromPipelineByPropertyName,
            HelpMessage = "Enable the return of the reason the object doesn't match")]
        [Parameter(Mandatory = $false, ParameterSetName = "Separate", ValueFromPipelineByPropertyName,
            HelpMessage = "Enable the return of the reason the object doesn't match")]
        [BoolParseTransformation()]
        [switch]$Reason,

        [Parameter(Mandatory = $false, ParameterSetName = "Object", ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [Parameter(Mandatory = $false, ParameterSetName = "Separate", ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        Write-LogMessage -Severity Verbose -Message "Starting Compare-GDAPAccessAssignment"
    }

    process
    {
        switch ($PsCmdlet.ParameterSetName)
        {
            "Object"
            {
                if (Test-AccessAssignment -AccessAssignment $DelegatedAdminAccessAssignment -GraphBaseURL $GraphBaseURL)
                {
                    $Group = $DelegatedAdminAccessAssignment.accessContainer.accessContainerId
                    $RoleDefinitionId = $DelegatedAdminAccessAssignment.accessDetails.unifiedRoles.roleDefinitionId
                }
                elseif (-not (Get-EntraGroupbyNameorId -Group $DelegatedAdminAccessAssignment.accessContainer.accessContainerId -GraphBaseURL $GraphBaseURL))
                {
                    throw "Provided delegatedAdminAccessAssignment is invalid"
                    if ($Reason)
                    {
                        $PSCmdlet.WriteObject("Invalid Group", $true)
                    }
                    else
                    {
                        $false
                    }
                    break
                }
                else
                {
                    throw "Provided delegatedAdminAccessAssignment is invalid"
                    if ($Reason)
                    {
                        $PSCmdlet.WriteObject("Invalid delegatedAdminAccessAssignment", $true)
                    }
                    else
                    {
                        $false
                    }
                    break
                }
            }
            "Separate"
            {
                if (-not (Get-EntraGroupbyNameorId -Group $Group -GraphBaseURL $GraphBaseURL))
                {
                    throw "Provided delegatedAdminAccessAssignment is invalid"
                    if ($Reason)
                    {
                        $PSCmdlet.WriteObject("Invalid Group", $true)
                    }
                    else
                    {
                        $false
                    }
                    break
                }
                $RoleDefinitionId = Get-GDAPAccessRolebyNameorId -RoleDefinition $RoleDefinition -ReturnID
                if ($RoleDefinitionId.Count -ne $RoleDefinition.Count)
                {
                    Write-LogMessage -Severity Warning -Message "Provided roles included $($RoleDefinition.Count - $RoleDefinitionId.Count) invalid roles"
                }
            }
        }

        $AccessAssignmentMatch = $true

        $ExistingGDAPAccessAssignment = Get-GDAPRelationshipAccessAssignment -GDAPRelationshipID $GDAPRelationshipID -Group $Group -GraphBaseURL $GraphBaseURL

        if ($ExistingGDAPAccessAssignment)
        {
            Write-LogMessage -Severity Verbose -Message "Comparing discovered roles with provided roles."
            Write-LogMessage -Severity Debug -Message "Comparing roleDefinitionId(s) from:`n Discovered roles: $($ExistingGDAPAccessAssignment.accessDetails.unifiedRoles.roleDefinitionId -join ", ")`n Provided roles: $($RoleDefinitionId -join ", ")"
            $CompareRoles = Compare-Object -ReferenceObject $ExistingGDAPAccessAssignment.accessDetails.unifiedRoles.roleDefinitionId -DifferenceObject $RoleDefinitionId
            $CompareRoleswithExisting = ( $CompareRoles | Where-Object -FilterScript { $_.SideIndicator -eq '=>' }).InputObject
            if ($CompareRoleswithExisting.Count -ge 1)
            {
                Write-LogMessage -Severity Warning -Message "The following provided Role IDs were not found in the active GDAP relationship:`n$($CompareRoleswithExisting -join ',')"
                $AccessAssignmentMatch = $false
                if ($Reason)
                {
                    $Explanation = "Missing Roles"
                }
            }
            $CompareRoleswithProvided = ( $CompareRoles | Where-Object -FilterScript { $_.SideIndicator -eq '<=' }).InputObject
            if ($CompareRoleswithProvided.Count -ge 1)
            {
                Write-LogMessage -Severity Warning -Message "The following Role IDs were not found in the provided Role IDs but exist in the active GDAP relationship:`n$($CompareRoleswithProvided -join ',')"
                $AccessAssignmentMatch = $false
                if ($Reason)
                {
                    $Explanation = "Extra Roles"
                }
            }
        }

        else
        {
            Write-LogMessage -Severity Warning -Message "No accessAssignment found matching the specified details."
            $AccessAssignmentMatch = $false
            if ($Reason)
            {
                $Explanation = "Missing Assignment"
            }
        }

        if ($AccessAssignmentMatch)
        {
            $Explanation = $true
        }
    }

    end
    {
        if ($Reason)
        {
            $PSCmdlet.WriteObject($Explanation, $true)
        }
        else
        {
            $AccessAssignmentMatch
        }
        Write-LogMessage -Severity Verbose -Message "Ending Compare-GDAPAccessAssignment"
    }
} #Compare-GDAPAccessAssignment


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Export-GDAPTemplateFromExistingRelationship
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP Relationship ID to use for template details lookup")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that role and security group details should be included in the template")]
        [BoolParseTransformation()]
        [switch]$Detailed,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        Write-LogMessage -Severity Verbose -Message "Starting Export-GDAPTemplateFromExistingRelationship"
    }

    process
    {
        Write-LogMessage -Severity Verbose -Message "Retrieving the existing GDAP Relationship"
        $ExistingGDAPRelationship = Get-GDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -GraphBaseURL $GraphBaseURL

        if ($ExistingGDAPRelationship.status -notin 'active', 'approved', 'activating')
        {
            Write-LogMessage -Severity Warning -Message "The GDAP relationship request with ID $($GDAPRelationshipID)$(if ($ExistingGDAPRelationship.customer.displayName) { " for $($ExistingGDAPRelationship.customer.displayName)" }) is not in an active state." ; break
        }
        else
        {
            Write-LogMessage -Severity Verbose -Message "Retrieving the existing accessAssignments"
            $ExistingGDAPAccessAssignment = Get-GDAPRelationshipAccessAssignment -GDAPRelationshipID $GDAPRelationshipID -Active -GraphBaseURL $GraphBaseURL
        }

        if ($Detailed)
        {
            [System.Collections.Generic.List[object]]$Roles = Get-GDAPAccessRolebyNameorId -RoleDefinition $ExistingGDAPRelationship.accessDetails.unifiedRoles.RoleDefinitionId
        }
        else
        {
            [System.Collections.Generic.List[string]]$Roles = $ExistingGDAPRelationship.accessDetails.unifiedRoles
        }

        [System.Collections.Generic.List[object]]$AccessAssignment = $ExistingGDAPAccessAssignment | ForEach-Object {
            Format-AccessAssignment -AccessAssignment $_ -Detailed:$Detailed -Generalize -GraphBaseURL $GraphBaseURL
        }

        if ($ExistingGDAPRelationship.autoExtendDuration -eq "P180D") { $AutoExtendRelationship = $true } else { $AutoExtendRelationship = $false }

        [string]$GDAPTemplate = [PSCustomObject]@{
            TemplateId             = (New-Guid).Guid
            Roles                  = $Roles
            AccessAssignment       = $AccessAssignment
            Duration               = [string]$ExistingGDAPRelationship.duration
            AutoExtendDuration     = [string]$ExistingGDAPRelationship.autoExtendDuration
            AutoExtendRelationship = $AutoExtendRelationship
        } | ConvertTo-Json -Depth 64
    }

    end
    {
        $PSCmdlet.WriteObject($GDAPTemplate, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Export-GDAPTemplateFromExistingRelationship"
    }
} #Export-GDAPTemplateFromExistingRelationship


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-GDAPAccessRolebyNameorId
{
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[object]])]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The Role Definition string(s) to find its matching Role Name or Role Id GUID")]
        [System.Collections.Generic.List[string]]$RoleDefinition,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Indicates that only the roleDefinitionID should be returned in List<String> format")]
        [BoolParseTransformation()]
        [switch]$ReturnID
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        Write-LogMessage -Severity Verbose -Message "Starting Get-GDAPAccessRolebyNameorId"

        if (-not $script:GDAPRoleList)
        {
            Write-LogMessage -Severity Verbose -Message "Retrieving the list of GDAP Roles"
            $GDAPRoles = Import-GDAPRoleList
        }
        else
        {
            Write-LogMessage -Severity Verbose -Message "GDAP Roles already loaded, continuing"
            $GDAPRoles = $script:GDAPRoleList
        }
    }

    process
    {
        [System.Collections.Generic.List[object]]$GDAPRolesObject = $RoleDefinition | ForEach-Object {
            $RoleDefinitionId = $false
            $RoleDefinitionString = $null
            $GDAPRoleMatch = $null
            $RoleDefinitionString = $_.Trim()

            Write-LogMessage -Severity Verbose -Message "Checking if the RoleDefinition was provided in GUID Id format"
            if (Test-Guid $RoleDefinitionString)
            {
                Write-LogMessage -Severity Debug -Message "GUID detected, Setting `$RoleDefinitionID to true"
                $RoleDefinitionId = $true
            }

            if ($RoleDefinitionId)
            {
                Write-LogMessage -Severity Verbose -Message "Looking up GDAP Role `"$RoleDefinitionString`" by RoleDefinitionId"
                Write-LogMessage -Severity Debug -Message "Parsing `$GDAPRoles for a role with RoleDefinitionId value of $RoleDefinitionString"
                $GDAPRoleMatch = $GDAPRoles | Where-Object { $_.RoleDefinitionId -ieq $RoleDefinitionString } | Select-Object -First 1
                if ($GDAPRoleMatch)
                {
                    Write-LogMessage -Severity Debug -Message "Found role match name for $RoleDefinitionString of $($GDAPRoleMatch.Name)"
                    $GDAPRoleMatch
                }
                else
                {
                    Write-LogMessage -Severity Warning -Message "Unable to find a matching role by GUID for $RoleDefinitionString."
                }
            }
            else
            {
                Write-LogMessage -Severity Verbose -Message "Looking up GDAP Role `"$RoleDefinitionString`" by Name"
                Write-LogMessage -Severity Debug -Message "Parsing `$GDAPRoles for a role with Name value of $RoleDefinitionString"
                $GDAPRoleMatch = $GDAPRoles | Where-Object { $_.Name -ieq $RoleDefinitionString } | Select-Object -First 1
                if ($GDAPRoleMatch)
                {
                    Write-LogMessage -Severity Debug -Message "Found role match ID for $RoleDefinitionString of $($GDAPRoleMatch.RoleDefinitionId)"
                    $GDAPRoleMatch
                }
                else
                {
                    Write-LogMessage -Severity Warning -Message "Unable to find a matching role by Name for $RoleDefinitionString."
                }
            }
        }
        [System.Collections.Generic.List[object]]$GDAPRolesObject = $GDAPRolesObject | Get-PSUnique
        if ($ReturnID)
        {
            [System.Collections.Generic.List[string]]$GDAPRolesObject = $GDAPRolesObject | Select-Object -ExpandProperty RoleDefinitionId
        }
    }

    end
    {
        Write-LogMessage -Severity Debug -Message "Found matches for $($GDAPRolesObject.Count) of $($RoleDefinition.Count) RoleDefinition(s)."
        $PSCmdlet.WriteObject($GDAPRolesObject, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Get-GDAPAccessRolebyNameorId"
    }
} #Get-GDAPAccessRolebyNameorId


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-GDAPRelationship
{
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([System.Collections.Generic.List[object]])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Detailed', Justification = 'False positive as rule does not scan child scopes')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = "ByID", ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID provided during the GDAP relationship request creation process")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory = $false, ParameterSetName = "ByFilter", ValueFromPipelineByPropertyName,
            HelpMessage = "Filter used to search relationships based on a specific value, uses OData query parameters")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { $_ -inotlike "`$filter=*" })]
        [string]$Filter,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that role and security group details should be included in the object")]
        [ValidateNotNullOrEmpty()]
        [BoolParseTransformation()]
        [switch]$Detailed,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-GDAPRelationship"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }
    process
    {
        try
        {
            # Verify that the base URL ends in trailing slash
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }

            # Build the Graph request base URL
            $DelegatedAdminRelationshipURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships"

            # Update the URL based on received input
            switch ($PsCmdlet.ParameterSetName)
            {
                "ByID"
                {
                    $DelegatedAdminRelationshipURL += "/$($GDAPRelationshipID)"
                }
                "ByFilter"
                {
                    $DelegatedAdminRelationshipURL += "?`$filter=$(([System.Uri]::EscapeDataString($Filter.Trim())))&`$top=15&`$count=true"
                }
                default
                {
                    $DelegatedAdminRelationshipURL += "?`$top=15&`$count=true"
                }
            }

            $DelegatedAdminRelationshipsObjects = [System.Collections.Generic.List[object]]::new()

            # Submit the Graph API request and receive the delegatedAdminRelationship object
            Write-LogMessage -Severity Verbose -Message "Retrieving existing GDAP relationships from Graph API: 'Invoke-MgGraphRequest -Method GET -Uri $($DelegatedAdminRelationshipURL) -OutputType PSObject'"
            $DelegatedAdminRelationships = Invoke-MgGraphRequest -Method GET -Uri $DelegatedAdminRelationshipURL -OutputType PSObject | Get-MgGraphAllPaging
            Write-LogMessage -Severity Debug -Message "Result:`n$($DelegatedAdminRelationships | ConvertTo-Json -Depth 10)"

            Write-LogMessage -Severity Verbose -Message "Processing returned array of objects"

            $DelegatedAdminRelationships | ForEach-Object {
                $DelegatedAdminRelationship = $null
                $DelegatedAdminRelationship = Format-AdminRelationship -AdminRelationship $_ -Detailed:$Detailed
                $DelegatedAdminRelationshipsObjects.Add($DelegatedAdminRelationship) | Out-Null
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when retrieving GDAP relationships, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }

    end
    {
        if ($DelegatedAdminRelationshipsObjects.Count -ge 1)
        {
            $PSCmdlet.WriteObject($DelegatedAdminRelationshipsObjects, $true)
        }
        Write-LogMessage -Severity Verbose -Message "Ending Get-GDAPRelationship"
    }
} #Get-GDAPRelationship


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-GDAPRelationshipAccessAssignment
{
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([System.Collections.Generic.List[object]])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Detailed', Justification = 'False positive as rule does not scan child scopes')]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID to use for accessAssignments lookup")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory, ParameterSetName = "ById", ValueFromPipelineByPropertyName,
            HelpMessage = "The AccessAssignment ID to lookup")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
                if (-not (Test-Guid -InputObject $_))
                {
                    throw "AccessAssignment ID `"$_`" is not in a valid guid format"
                }
                else { $true }
            })]
        [string]$AccessAssignmentId,

        [Parameter(Mandatory, ParameterSetName = "ByGroup", ValueFromPipelineByPropertyName,
            HelpMessage = "Filters the accessAssignments to specific groups")]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Generic.List[string]]$Group,

        [Parameter(Mandatory, ParameterSetName = "ByFilter", ValueFromPipelineByPropertyName,
            HelpMessage = "Filter used to search accessAssignments based on a specific value, uses OData query parameters")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { $_ -inotlike "`$filter=*" })]
        [string]$Filter,

        [Parameter(Mandatory = $false, ParameterSetName = "Default", ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that results should be filtered to only accessAssignments with 'active' or 'pending' states")]
        [Parameter(Mandatory = $false, ParameterSetName = "ByGroup", ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that results should be filtered to only accessAssignments with 'active' or 'pending' states")]
        [BoolParseTransformation()]
        [switch]$Active,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that role and security group details should be included in the object")]
        [BoolParseTransformation()]
        [switch]$Detailed,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-GDAPRelationshipAccessAssignment"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }
    process
    {
        try
        {
            # Verify that the base URL ends in trailing slash
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }

            # Build the Graph request base URL
            $DelegatedAdminAccessAssignmentURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships/$($GDAPRelationshipID)/accessAssignments"

            # Update the URL based on received input, perform group lookup if not provided in Guid format
            switch ($PsCmdlet.ParameterSetName)
            {
                "ById"
                {
                    $DelegatedAdminAccessAssignmentURL = "$($DelegatedAdminAccessAssignmentURL)/$($AccessAssignmentId)"
                }
                "ByGroup"
                {
                    $Group = $Group | ForEach-Object {
                        Write-LogMessage -Severity Verbose -Message "Checking if the Group was provided in GUID ObjectId format"
                        if (Test-Guid $_)
                        {
                            Write-LogMessage -Severity Debug -Message "GUID detected, returning group as is"
                            $_
                        }
                        else
                        {
                            Write-LogMessage -Severity Debug -Message "Performing group lookup"
                            (Get-EntraGroupbyNameorId -Group $_ -GraphBaseURL $GraphBaseURL).id
                        }
                    }
                    $FilterResults = "accessContainer/accessContainerId in ('$([uri]::EscapeDataString($Group.Trim()) -join "', '")')"
                }
                "ByFilter"
                {
                    $FilterResults = $([uri]::EscapeDataString($Filter.Trim()))
                }
            }

            if ($Active)
            {
                if ($FilterResults)
                {
                    $FilterResults = "(status eq ('active') or status eq ('pending')) and $($FilterResults)"
                }
                else
                {
                    $FilterResults = "(status eq ('active') or status eq ('pending'))"
                }
            }

            if ($FilterResults)
            {
                $DelegatedAdminAccessAssignmentURL += "?`$filter=$($FilterResults)&`$count=true"
            }
            else
            {
                $DelegatedAdminAccessAssignmentURL += "?`$count=true"
            }

            $DelegatedAdminRelationshipAccessObjects = [System.Collections.Generic.List[object]]::new()

            # Submit the Graph API request and receive the delegatedAdminRelationship object
            Write-LogMessage -Severity Verbose -Message "Retrieving existing GDAP relationship accessAssignments from Graph API: 'Invoke-MgGraphRequest -Method GET -Uri $($DelegatedAdminRelationshipURL)'"

            $DelegatedAdminRelationshipAccessItems = Invoke-MgGraphRequest -Method GET -Uri $DelegatedAdminAccessAssignmentURL -OutputType PSObject | Get-MgGraphAllPaging
            Write-LogMessage -Severity Debug -Message "Result:`n$($DelegatedAdminRelationshipAccessItems | ConvertTo-Json -Depth 5)"

            Write-LogMessage -Severity Verbose -Message "Processing returned array of objects"

            $DelegatedAdminRelationshipAccessItems | ForEach-Object {
                $DelegatedAdminRelationshipAccess = $null
                $DelegatedAdminRelationshipAccess = Format-AccessAssignment -AccessAssignment $_ -GDAPRelationshipID $GDAPRelationshipID -Detailed:$Detailed -GraphBaseURL $GraphBaseURL
                $DelegatedAdminRelationshipAccessObjects.Add($DelegatedAdminRelationshipAccess) | Out-Null
            }

            Write-LogMessage -Severity Debug -Message "Result:`n$($DelegatedAdminRelationshipAccessObjects | ConvertTo-Json -Depth 5)"
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when getting GDAP adminAccessAssignments, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }

    end
    {
        if ($DelegatedAdminRelationshipAccessObjects.Count -ge 1)
        {
            $PSCmdlet.WriteObject($DelegatedAdminRelationshipAccessObjects, $true)
        }
        Write-LogMessage -Severity Verbose -Message "Ending Get-GDAPRelationshipAccessAssignment"
    }
} #Get-GDAPRelationshipAccessAssignment


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-GDAPRelationshipRequestLink
{

    [CmdletBinding(DefaultParameterSetName = "NoEmail")]
    [OutputType([object])]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Email', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID provided during the GDAP relationship request creation process")]
        [Parameter(Mandatory, ParameterSetName = 'NoEmail', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID provided during the GDAP relationship request creation process")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory = $false, ParameterSetName = 'Email', ValueFromPipelineByPropertyName,
            HelpMessage = "String containing the link that should be included as an indirect reseller link")]
        [Parameter(Mandatory = $false, ParameterSetName = 'NoEmail', ValueFromPipelineByPropertyName,
            HelpMessage = "String containing the link that should be included as an indirect reseller link")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$IndirectResellerLink,

        [Parameter(Mandatory = $false, ParameterSetName = 'Email', ValueFromPipelineByPropertyName,
            HelpMessage = "Should boilerplate email text be generated")]
        [BoolParseTransformation()]
        [switch]$GenerateEmailText,

        [Parameter(Mandatory = $false, ParameterSetName = 'Email', ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Get-GDAPRelationshipRequestLink"
    }
    process
    {
        if ($PsCmdlet.ParameterSetName -eq 'Email' -and $GenerateEmailText)
        {
            try
            {
                # Get the existing GDAP relationship to pull specific information for the email text
                $GDAPRelationship = Get-GDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -Detailed -GraphBaseURL $GraphBaseURL

                if ($null -eq $GDAPRelationship)
                {
                    Write-LogMessage -Severity Warning -Message "Skipping email text generation, the provided GDAP Relationship ID did not match any existing GDAP Relationships."
                    $GenerateEmailText = $false
                }

                else
                {
                    # Use the Indirect Reseller Link query items to build the tenant display names, or the Graph context if not using Indirect Reseller.
                    if ($IndirectResellerLink)
                    {
                        $LinkQueries = Get-HttpQueryStringList -Url $IndirectResellerLink
                        if ($LinkQueries.GetEnumerator().Name -icontains 'indirectCSPId')
                        {
                            [string]$IndirectCSPName = Get-TenantDisplayNamebyId -TenantID ([regex]::match($LinkQueries.indirectCSPId , '((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))').Groups[1].Value) -GraphBaseURL $GraphBaseURL
                        }
                        if ($LinkQueries.GetEnumerator().Name -icontains 'partnerId')
                        {
                            [string]$TenantDisplayName = Get-TenantDisplayNamebyId -TenantID ([regex]::match($LinkQueries.partnerId , '((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))').Groups[1].Value) -GraphBaseURL $GraphBaseURL
                        }
                    }
                    else
                    {
                        [string]$TenantDisplayName = Get-TenantDisplayNamebyId -TenantID ((Get-MgContext).TenantId) -GraphBaseURL $GraphBaseURL
                    }
                }
            }
            catch
            {
                Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
            }
        }

        # Build the GDAP URL
        $GDAPInvitationLink = "https://admin.microsoft.com/AdminPortal/Home#/partners/invitation/granularAdminRelationships/$($GDAPRelationshipID)"

        try
        {

            # Generate the email text, include indirect reseller information if specified. Add the roles as individual lines with name and description (emulates the text from the GDAP in the partner portal)
            if ($PsCmdlet.ParameterSetName -eq 'Email' -and $GenerateEmailText)
            {
                if ([string]::IsNullOrEmpty($GDAPRelationship.customer.displayName))
                {
                    $EmailText = "$($TenantDisplayName) Microsoft 365 Partner Permissions Request`n`n"
                }
                else
                {
                    $EmailText = "$($GDAPRelationship.customer.displayName) - $($TenantDisplayName) Microsoft 365 Partner Permissions Request`n`n"
                }

                # Create the Role Name and Definition object from the existing GDAP Relationship unifiedRoles
                $GDAPRelationshipRoles = $GDAPRelationship.accessDetails.unifiedRoles

                if ([string]::IsNullOrEmpty($GDAPRelationship.endDateTime))
                {
                    $EndDate = ([System.DateTime]$GDAPRelationship.createdDateTime).Add((Convert-ISO8601ToTimespan -Duration $GDAPRelationship.duration))
                }
                else
                {
                    $EndDate = [System.DateTime]$GDAPRelationship.endDateTime
                }

                if ($IndirectResellerLink)
                {
                    $EmailText += @"
As your cloud services provider, $($TenantDisplayName) offers cloud solutions through our partner, $($IndirectCSPName).
Follow the link below to accept this offer and to subscribe to $($IndirectCSPName)'s solutions through $($TenantDisplayName) and to authorize $($TenantDisplayName) as your official local reseller.

$($IndirectResellerLink)
Note: User with Global Admin permission is required to accept relationship. Customer address must be completed first (https://admin.microsoft.com/Adminportal/Home?#/BillingAccounts/billing-accounts) before using the acceptance link above.

Additionally, by clicking the following link you will be able to accept the request for us to administer your cloud services using the roles listed below for the specified timeframe.`n`n
"@

                }
                else
                {
                    $EmailText += @"
As your cloud services provider, $($TenantDisplayName) provides the administration of your cloud services through our status as a Microsoft Partner.
Follow the link below to accept the request for us to administer your cloud services using the roles listed below for the specified timeframe.`n`n
"@

                }
                $EmailText += @"
Click to review and accept the below permissions:
$($GDAPInvitationLink)

Administrative Roles Expiration Date:
$(Get-Date -Format "MMMM dd, yyyy" -Date $EndDate)

Requested Entra ID roles:
"@


                $GDAPRelationshipRoles | Sort-Object -Property Name | ForEach-Object {
                    $EmailText += @"
`n
$($_.Name)
$($_.Description)
"@

                }
            }
            # Build and return the output object
            Write-LogMessage -Severity Verbose -Message "Generating GDAPInvitationLink value."
            $GDAPRelationshipRequest = @{
                GDAPInvitationLink = $GDAPInvitationLink
            }
            if ($IndirectResellerLink)
            {
                Write-LogMessage -Severity Verbose -Message "Adding IndirectResellerLink value."
                $GDAPRelationshipRequest.Add('IndirectResellerLink', $IndirectResellerLink)
            }
            if ($EmailText)
            {
                Write-LogMessage -Severity Verbose -Message "Generating EmailText value."
                $GDAPRelationshipRequest.Add('EmailText', $EmailText)
            }

        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }

    end
    {
        $PSCmdlet.WriteObject($GDAPRelationshipRequest)
        Write-LogMessage -Severity Verbose -Message "Ending Get-GDAPRelationshipRequestLink"
    }
} #Get-GDAPRelationshipRequestLinks


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Import-GDAPRoleList
{
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[object]])]
    param(
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Local file path or URL string with the JSON file containing GDAP Entra ID roles, defaults to the included roles file")]
        [string]$RoleFile
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Import-GDAPRoleList"
    }

    process
    {
        # Validate and retrieve RoleFile
        try
        {
            if ([string]::IsNullOrEmpty($RoleFile))
            {
                $RoleFile = (Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath 'Resources/Roles/GDAPRoles.json')
            }
            if (Test-Path $RoleFile -IsValid)
            {
                Write-LogMessage -Severity Verbose -Message "Local path of $RoleFile is valid, importing role file."
                $RoleFile = Resolve-Path -Path $($RoleFile)
                [System.Collections.Generic.List[object]]$ImportedRoleFile = Get-Content -Path $RoleFile | ConvertFrom-Json -Depth 64
            }
            elseif (Test-Url -Url $RoleFile)
            {
                Write-LogMessage -Severity Verbose -Message "URL of $RoleFile is valid, importing role file."
                [System.Collections.Generic.List[object]]$ImportedRoleFile = Get-JSONFromURL -JSONFileUrl $RoleFile
            }
            else
            {
                Write-LogMessage -Severity Error -Message "$($RoleFile) is not a valid local file path or URL" -TargetObject $RoleFile -RecommendedAction "Provide a valid `$RoleFile variable that points to a valid JSON local path or URL" -StopOnError
            }
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_ -TargetObject $RoleFile -StopOnError
        }

        # Get values from possible RoleList layouts
        Write-LogMessage -Severity Verbose -Message "Parsing imported Role list to determine which attributes were included for import."
        try
        {
            $Expressions = [System.Collections.Generic.List[object]]::new()
            $ArrayMember = $ImportedRoleFile | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | ForEach-Object { foreach ($RoleObject in $ImportedRoleFile) { if ($RoleObject."$_" -is [array]) { $_ } } } | Select-Object -Unique
            if ($ArrayMember)
            {
                $TempImportedRoleFile = [System.Collections.Generic.List[object]]::new()
                $InternalMemberName = $ImportedRoleFile | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | Where-Object { $_ -notin $ArrayMember }

                foreach ($ArrayMemberItem in $ArrayMember)
                {
                    foreach ($RoleObject in $ImportedRoleFile)
                    {
                        $RoleObject."$ArrayMemberItem" | ForEach-Object {
                            foreach ($InternalMemberNameItem in $InternalMemberName)
                            {
                                Add-Member -InputObject $_ -MemberType NoteProperty -Name "Group$([regex]::replace($InternalMemberNameItem, '^\w', {param($m) "$m".ToUpper()}))" -Value $RoleObject."$InternalMemberNameItem"
                            }
                            $TempImportedRoleFile.Add($_)
                        }
                    }
                }
                $ImportedRoleFile = $TempImportedRoleFile
            }
            [System.Collections.Generic.List[string]]$RoleOrder = "^(?!Group).*(?:Name)+?.*", "^(?!Group).*(?:RoleDefinition|Object|Id)+?.*", "^(?!Group).*(?:Desc)+?.*", ".*"
            $ImportedRoleFile | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | Sort-Object {
                $MemberName = $_
                $RoleOrder.IndexOf(($RoleOrder | Where-Object { $MemberName -imatch $_ } | Select-Object -First 1))
            } | ForEach-Object {
                [string]$ExpressionString = "`$_.$_"
                [ScriptBlock]$Expression = [ScriptBlock]::Create($ExpressionString)
                switch -regex ($_)
                {
                    "^(?!Group).*(?:Name)+?.*"
                    {
                        $Expressions.Add( @{n = "Name"; e = $Expression } )
                    }

                    "^(?!Group).*(?:Role|Object|Id)+?.*"
                    {
                        $Expressions.Add( @{n = "RoleDefinitionId"; e = $Expression } )
                    }

                    "^(?!Group).*(?:Desc)+?.*"
                    {
                        $Expressions.Add( @{n = "Description"; e = $Expression } )
                    }

                    Default
                    {
                        $Expressions.Add( @{n = "$($([regex]::replace($_, '^\w', {param($m) "$m".ToUpper()})))"; e = $Expression } )
                    }
                }
            }

            [System.Collections.Generic.List[object]]$RoleList = $ImportedRoleFile | Select-Object -Property $Expressions | Get-PSUnique
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_ -TargetObject $ImportedRoleFile -StopOnError
        }
    }

    end
    {
        Write-LogMessage -Severity Verbose -Message "Setting `$Script:GDAPRoleList variable with $($RoleList.Count) roles"
        $null = Set-Variable -Name GDAPRoleList -Value $RoleList -Scope Script -Option ReadOnly
        $PSCmdlet.WriteObject($RoleList, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Import-GDAPRoleList"
    }

} #Import-GDAPRoleList


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Import-GDAPTemplate
{
    [CmdletBinding()]
    [OutputType([object])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'GraphBaseURL', Justification = 'False positive as rule does not scan child scopes')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Local file path or URL string with the JSON template file containing one or more delegatedAdminAccessAssignment objects")]
        [ValidateNotNullOrEmpty()]
        [string]$TemplateFile,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Disable the validation of the template against known roles or groups")]
        [BoolParseTransformation()]
        [switch]$SkipValidation,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Import-GDAPTemplate"

        $MaximumDuration = "P730D"
        $AutoExtendDurationValues = "PT0S", "P0D", "P180D"

        $TemplateObject = [PSCustomObject]@{
            Roles                  = [System.Collections.Generic.List[object]]::new()
            AccessAssignment       = [System.Collections.Generic.List[object]]::new()
            Duration               = $null
            AutoExtendDuration     = $null
            AutoExtendRelationship = $null
        }
    }

    process
    {
        # Validate template file
        if (Test-Path $TemplateFile -IsValid)
        {
            Write-LogMessage -Severity Verbose -Message "Local path of $TemplateFile is valid, importing template file."
            $TemplateFile = Resolve-Path -Path $TemplateFile
            $ImportedTemplate = Get-Content -Path $TemplateFile | ConvertFrom-Json -Depth 64
        }
        elseif (Test-Url -Url $TemplateFile)
        {
            Write-LogMessage -Severity Verbose -Message "URL of $TemplateFile is valid, importing template file."
            [System.Collections.Generic.List[object]]$ImportedTemplate = Get-JSONFromURL -JSONFileUrl $TemplateFile
        }
        else
        {
            Write-LogMessage -Severity Error -Message "$($TemplateFile) is not a valid local file path or URL" -LastException $_ ; break
        }

        # Get values from possible template layouts
        Write-LogMessage -Severity Verbose -Message "Parsing imported template to determine which attributes were included for import."
        $ImportedTemplate | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | ForEach-Object {
            switch -Wildcard ($_)
            {
                "AccessAssignment*"
                {
                    [System.Collections.Generic.List[object]]$AccessAssignmentObjects = $ImportedTemplate."$_"
                }
                "Role*"
                {
                    if ("roleDefinitionId" -iin ($ImportedTemplate."$_" | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name))
                    {
                        [System.Collections.Generic.List[string]]$Roles = $ImportedTemplate."$_".roleDefinitionId
                    }
                    elseif ("Name" -iin ($ImportedTemplate."$_" | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name))
                    {
                        [System.Collections.Generic.List[string]]$Roles = $ImportedTemplate."$_".Name
                    }
                    else
                    {
                        [System.Collections.Generic.List[string]]$Roles = $ImportedTemplate."$_"
                    }
                }
                "Duration*"
                {
                    if ($SkipValidation)
                    {
                        $TemplateObject.Duration = [string]$ImportedTemplate."$_"
                    }
                    elseif ((Convert-ISO8601ToTimespan -Duration $ImportedTemplate."$_") -le (Convert-ISO8601ToTimespan -Duration $MaximumDuration))
                    {
                        $TemplateObject.Duration = [string]$ImportedTemplate."$_"
                    }
                    else
                    {
                        Write-LogMessage -Severity Warning -Message "Duration value of `"$($ImportedTemplate."$_")`" is not a valid duration, skipping value"
                    }
                }
                "AutoExtendDuration*"
                {
                    if ($SkipValidation -or $ImportedTemplate."$_" -in $AutoExtendDurationValues)
                    {
                        $TemplateObject.AutoExtendDuration = [string]$ImportedTemplate."$_"
                    }
                    else
                    {
                        Write-LogMessage -Severity Warning -Message "AutoExtendDuration value of `"$($ImportedTemplate."$_")`" is not in the list of valid values, $($AutoExtendDurationValues -join '; '), skipping value"
                    }
                }
                "AutoExtendRelationship*"
                {
                    if ($SkipValidation)
                    {
                        $TemplateObject.AutoExtendRelationship = $ImportedTemplate."$_"
                    }
                    elseif ($ImportedTemplate."$_" -isnot [Boolean])
                    {
                        Write-LogMessage -Severity Warning -Message "AutoExtendRelationship value of `"$($ImportedTemplate."$_")`" is not a boolean, skipping value"
                    }
                    else
                    {
                        $TemplateObject.AutoExtendRelationship = $ImportedTemplate."$_"
                    }
                }
                Default
                {
                    if ($ImportedTemplate."$_" -iin "accessContainer", "accessDetails")
                    {
                        [System.Collections.Generic.List[object]]$AccessAssignmentObjects = $ImportedTemplate
                    }
                    else
                    {
                        Add-Member -MemberType NoteProperty -InputObject $TemplateObject -Name $_ -Value ($ImportedTemplate."$_") -Force
                    }
                }
            }
        }

        if ($SkipValidation)
        {
            # Create the assignment objects with no validation, create an null invalid assignment object to prevent errors
            [System.Collections.Generic.List[object]]$ValidAccessAssignmentObjects = $AccessAssignmentObjects
            [System.Collections.Generic.List[object]]$InvalidAccessAssignmentObjects = $null
        }
        else
        {
            # Test if the assignment object(s) are valid
            [System.Collections.Generic.List[object]]$ValidAccessAssignmentObjects = $AccessAssignmentObjects | Where-Object { Test-AccessAssignment -AccessAssignment $_ -GraphBaseURL $GraphBaseURL }
            Write-LogMessage -Severity Verbose -Message "Found $($ValidAccessAssignmentObjects.Count) valid AccessAssignment objects out of $($AccessAssignmentObjects.Count) objects in the template file $($TemplateFile)."

            [System.Collections.Generic.List[object]]$InvalidAccessAssignmentObjects = $AccessAssignmentObjects | Where-Object { $_ -notin $ValidAccessAssignmentObjects }

            if ($null -ne $TemplateObject.AutoExtendRelationship -and $null -eq $TemplateObject.AutoExtendDuration)
            {
                Write-LogMessage -Severity Information -Message "AutoExtendRelationship is set and AutoExtendDuration is empty, auto-filling AutoExtendDuration based on AutoExtendRelationship value"
                if ($TemplateObject.AutoExtendRelationship)
                {
                    $TemplateObject.AutoExtendDuration = "P180D"
                }
                else
                {
                    $TemplateObject.AutoExtendDuration = "PT0S"
                }
            }
            elseif ($TemplateObject.AutoExtendRelationship -and $TemplateObject.AutoExtendDuration -ne "P180D")
            {
                Write-LogMessage -Severity Warning -Message "AutoExtendRelationship is `$true, but AutoExtendDuration is not equal to `"P180D`", preferring AutoExtendRelationship value and settings AutoExtendDuration to `"P180D`""
                $TemplateObject.AutoExtendDuration = "P180D"
            }
        }

        if ($InvalidAccessAssignmentObjects.Count -gt 0)
        {
            Write-LogMessage -Severity Warning -Message "Found $($InvalidAccessAssignmentObjects.Count) invalid AccessAssignment objects out of $($AccessAssignmentObjects.Count) objects in the template file $($TemplateFile)."
        }

        Write-LogMessage -Severity Verbose -Message "Generating the list of required role IDs from the accessAssignments"
        [System.Collections.Generic.List[string]]$RequiredRoleID = $ValidAccessAssignmentObjects | ForEach-Object {
            $_.accessDetails.unifiedRoles.roleDefinitionId
        } | Select-Object -Unique

        if ($Roles)
        {
            Write-LogMessage -Severity Verbose -Message "Expected Role Definition list included, adding to required roles list"
            $RequiredRoleID.AddRange($Roles)
        }

        $RequiredRoleIDResults = Get-GDAPAccessRolebyNameorId -RoleDefinition $RequiredRoleID
        $TemplateObject.Roles.AddRange($RequiredRoleIDResults)
        $TemplateObject.AccessAssignment.AddRange($ValidAccessAssignmentObjects)
    }

    end
    {
        $PSCmdlet.WriteObject($TemplateObject, $true)
        Write-LogMessage -Severity Verbose -Message "Ending Import-GDAPTemplate"
    }
} # Import-GDAPTemplate


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function New-GDAPRelationship
{
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "GeneratedwithDays")]
    [OutputType([object])]
    param (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the display name of the client's Entra ID tenant as displayed in the client's Entra Admin Center at https://entra.microsoft.com/#view/Microsoft_AAD_IAM/TenantOverview.ReactView")]
        [ValidateNotNullOrEmpty()]
        [string]$ClientTenantName,

        [Parameter(Mandatory = $false, ParameterSetName = 'NamedwithDays', ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the client's Tenant ID of their Entra ID tenant as displayed in the client's Entra Admin Center at https://entra.microsoft.com/#view/Microsoft_AAD_IAM/TenantOverview.ReactView")]
        [Parameter(Mandatory = $false, ParameterSetName = 'NamedwithDuration', ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the client's Tenant ID of their Entra ID tenant as displayed in the client's Entra Admin Center at https://entra.microsoft.com/#view/Microsoft_AAD_IAM/TenantOverview.ReactView")]
        [Parameter(Mandatory = $false, ParameterSetName = 'GeneratedwithDays', ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the client's Tenant ID of their Entra ID tenant as displayed in the client's Entra Admin Center at https://entra.microsoft.com/#view/Microsoft_AAD_IAM/TenantOverview.ReactView")]
        [Parameter(Mandatory = $false, ParameterSetName = 'GeneratedwithDuration', ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the client's Tenant ID of their Entra ID tenant as displayed in the client's Entra Admin Center at https://entra.microsoft.com/#view/Microsoft_AAD_IAM/TenantOverview.ReactView")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Guid -InputObject $_ })]
        [string]$ClientTenantID,

        [Parameter(Mandatory = $false, ParameterSetName = 'NamedwithDays', ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the display name of the GDAP relationship to provision, must be unique")]
        [Parameter(Mandatory = $false, ParameterSetName = 'NamedwithDuration', ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the display name of the GDAP relationship to provision, must be unique")]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(1, 50)]
        [ValidatePattern("[^a-zA-Z0-9.,&+_-]+")]
        [string]$GDAPRelationshipName,

        [Parameter(Mandatory = $false, ParameterSetName = 'NamedwithDays', ValueFromPipelineByPropertyName,
            HelpMessage = "The number of days for the GDAP relationship to live, maximum of 730 days. Defaults to 730 days")]
        [Parameter(Mandatory = $false, ParameterSetName = 'NamedwithDuration', ValueFromPipelineByPropertyName,
            HelpMessage = "The number of days for the GDAP relationship to live, maximum of 730 days. Defaults to 730 days")]
        [Parameter(Mandatory = $false, ParameterSetName = 'GeneratedwithDays', ValueFromPipelineByPropertyName,
            HelpMessage = "The number of days for the GDAP relationship to live, maximum of 730 days. Defaults to 730 days")]
        [Parameter(Mandatory = $false, ParameterSetName = 'GeneratedwithDuration', ValueFromPipelineByPropertyName,
            HelpMessage = "The number of days for the GDAP relationship to live, maximum of 730 days. Defaults to 730 days")]
        [ValidateRange(1, 730)]
        [int]$RelationshipExpirationInDays = 730,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The duration for the GDAP relationship to live in ISO8601 string format, maximum of 730 days. Defaults to 730 days")]
        [ValidateScript( {
                if (-not (Convert-ISO8601ToTimespan -Duration $_)) { throw "$_ is not a valid ISO8601 duration string" }
                elseif ((Convert-ISO8601ToTimespan -Duration $_ ) -gt (Convert-ISO8601ToTimespan -Duration "P730D")) { throw "$_ is greater than the maximum allowed 730 days, `"P730D`"" }
                else { return $true }
            } )]
        [string]$Duration = "P730D",

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Should the relationship be set to auto extend using the allowed `"P180D`" parameter (will not be used if the Global Administrator role is included in assigned roles)?")]
        [switch]$AutoExtendRelationship,

        [Parameter(Mandatory = $false, ParameterSetName = 'GeneratedwithDays', ValueFromPipelineByPropertyName,
            HelpMessage = "The prefix to use in the generated GDAP relationship name, e.g. 'CompName'")]
        [Parameter(Mandatory = $false, ParameterSetName = 'GeneratedwithDuration', ValueFromPipelineByPropertyName,
            HelpMessage = "The prefix to use in the generated GDAP relationship name, e.g. 'CompName'")]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(0, 8)]
        [string]$RelationshipPrefix,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName,
            HelpMessage = "GUID Role IDs or Names of Entra ID roles to be used in the GDAP Relationship")]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Generic.List[string]]$RoleDefinition,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that the a relationshipRequest should be created and set to LockForApproval")]
        [switch]$LockForApproval,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that role and security group details should be included in the return object")]
        [BoolParseTransformation()]
        [switch]$Detailed,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting New-GDAPRelationship"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }

    process
    {
        try
        {
            # Verify that the base URL ends in trailing slash
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }

            # Build Graph URL
            $DelegatedAdminRelationshipURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships"

            # If used, normalize the Relationship Prefix to match and fit
            if ([string]::IsNullOrEmpty($RelationshipPrefix) -and [string]::IsNullOrEmpty($GDAPRelationshipName) -and -not ([string]::IsNullOrEmpty($ClientTenantID)))
            {
                [string]$RelationshipPrefix = Get-TenantDisplayNamebyId -TenantID $ClientTenantID -GraphBaseURL $GraphBaseURL
            }
            if (-not ([string]::IsNullOrEmpty($RelationshipPrefix)) -and [string]::IsNullOrEmpty($GDAPRelationshipName))
            {
                $RelationshipPrefix = $RelationshipPrefix.Trim() -replace '\s+', '_' -replace '[^a-zA-Z0-9.,&+_-]'
                $RelationshipPrefix = $RelationshipPrefix.Substring(0, [System.Math]::Min(50, $RelationshipPrefix.Length)) -replace '[\s_-]+$'
            }

            # Build and normalize the GDAP Relationship displayName
            Write-LogMessage -Severity Verbose -Message "Normalizing/Generating GDAP Relationship Display Name"
            [string]$RelationshipDisplayName = $(if ($GDAPRelationshipName)
                {
                    $GDAPRelationshipName
                }
                elseif (-not ([string]::IsNullOrEmpty($ClientTenantID)) -and -not ([string]::IsNullOrEmpty($RelationshipPrefix)))
                {
                    "{0}_{1}_{2}" -f $RelationshipPrefix, $(Get-Date -Format yyyy), $ClientTenantID
                }
                elseif (-not ([string]::IsNullOrEmpty($RelationshipPrefix)))
                {
                    "{0}_{1}_{2}" -f $RelationshipPrefix, $(Get-Date -Format yyyy), $((New-Guid).Guid)
                }
                else
                {
                    "{0}_{1}_{2}" -f "GDAP", $(Get-Date -Format yyyy), $((New-Guid).Guid)
                })

            $RelationshipDisplayName = $RelationshipDisplayName.Substring(0, [System.Math]::Min(50, $RelationshipDisplayName.Length))

            # Ensure the GDAP Relationship displayName is unique amongst all GDAP relationships, update with random characters until it is unique
            Write-LogMessage -Severity Verbose -Message "Verifying that the GDAP Relationship displayName is unique amongst all GDAP relationship displayNames"
            if (-not (Get-GDAPRelationship -Filter "displayName eq '($($RelationshipDisplayName))'" -GraphBaseURL $GraphBaseURL))
            {
                $updateCount = 0
                do
                {
                    Write-LogMessage -Severity Verbose -Message "Generating random digits to attempt to make GDAP Relationship displayName unique"
                    if ($RelationshipDisplayName.Length -lt 50)
                    {
                        $RelationshipDisplayName += ([char]((97..122) + (48..57) | Get-Random))
                    }
                    else
                    {
                        $updateCount++
                        $RelationshipDisplayName = $RelationshipDisplayName.Substring(0, ($RelationshipDisplayName.Length - $updateCount))
                    }
                } until (-not (Get-GDAPRelationship -Filter "displayName eq '($($RelationshipDisplayName))'" -GraphBaseURL $GraphBaseURL))
            }
            Write-LogMessage -Severity Verbose -Message "GDAP Relationship Dispay Name: $($RelationshipDisplayName)"

            # Perform role definition lookups
            $RoleDefinitionId = Get-GDAPAccessRolebyNameorId -RoleDefinition $RoleDefinition -ReturnID

            # Get Duration
            switch -Wildcard ($PsCmdlet.ParameterSetName)
            {
                "*Days"
                {
                    $Duration = "P$($RelationshipExpirationInDays.ToString())D"
                }
            }

            # Build the Graph API Message Body with available variables
            $DelegatedAdminRelationshipBody = [PSCustomObject]@{
                displayName   = $RelationshipDisplayName
                duration      = $Duration
                accessDetails = @{
                    unifiedRoles = @(
                        $RoleDefinitionId | ForEach-Object {
                            @{ roleDefinitionId = $_ }
                        }
                    )
                }
            }
            if ($AutoExtendRelationship -and $RoleDefinitionId -notcontains '62e90394-69f5-4237-9190-012177145e10')
            {
                Add-Member -InputObject $DelegatedAdminRelationshipBody -MemberType NoteProperty -Name 'autoExtendDuration' -Value 'P180D'
            }
            else
            {
                Add-Member -InputObject $DelegatedAdminRelationshipBody -MemberType NoteProperty -Name 'autoExtendDuration' -Value 'PT0S'
            }
            if ($ClientTenantID -and $ClientTenantName)
            {
                Add-Member -InputObject $DelegatedAdminRelationshipBody -MemberType NoteProperty -Name 'customer' -Value @{
                    tenantId    = $ClientTenantID
                    displayName = $ClientTenantName
                }
            }
            elseif ($ClientTenantID)
            {
                Add-Member -InputObject $DelegatedAdminRelationshipBody -MemberType NoteProperty -Name 'customer' -Value @{
                    tenantId = $ClientTenantID
                }
            }
            elseif ($ClientTenantName)
            {
                Add-Member -InputObject $DelegatedAdminRelationshipBody -MemberType NoteProperty -Name 'customer' -Value @{
                    displayName = $ClientTenantName
                }
            }

            # Submit the Graph API request and receieve the generated relationship object
            Write-LogMessage -Severity Verbose -Message "Creating new GDAP relationship from Graph API: 'Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL -Body (`$DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 10 -Compress) -StatusCodeVariable IMGRStatusCode -OutputType PSObject'"
            Write-LogMessage -Severity Debug -Message "Graph Request Body value for new Relationship: `n$($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 10)"
            if ($PSCmdlet.ShouldProcess(("Request body: `n$($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 10 -Compress)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL -StatusCodeVariable IMGRStatusCode -OutputType PSObject")))
            {
                $CreateDelegatedAdminRelationship = Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL -Body ($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 10 -Compress) -StatusCodeVariable IMGRStatusCode -OutputType PSObject
                Write-LogMessage -Severity Debug -Message "Result:`n $($CreateDelegatedAdminRelationship | ConvertTo-Json -Depth 10)"
                # Validate the relationship request ID is valid before returning
                if (-not ([string]::IsNullOrEmpty($CreateDelegatedAdminRelationship.id)) -and $IMGRStatusCode -eq '201')
                {
                    # Check that the relationship is in the "created" state
                    Start-Sleep -Milliseconds 100
                    $Count = 0
                    do
                    {
                        Write-LogMessage -Severity Verbose -Message "Checking for an status on the new relationship of `"created`": 'Invoke-MgGraphRequest -Method GET -Uri `"$($DelegatedAdminRelationshipURL)/$($CreateDelegatedAdminRelationship.id)`" -OutputType PSObject'"
                        $CheckActive = Invoke-MgGraphRequest -Method GET -Uri "$($DelegatedAdminRelationshipURL)/$($CreateDelegatedAdminRelationship.id)" -OutputType PSObject
                        Start-Sleep -Milliseconds 200
                        $Count++
                    } until ($CheckActive.status -eq 'created' -or $Count -gt 10)

                    if ($CheckActive.status -eq 'created' -and $LockForApproval)
                    {
                        # Lock for approval
                        $NewRelationshipRequest = New-GDAPRelationshipRequest -GDAPRelationshipID $CreateDelegatedAdminRelationship.id -Action 'lockForApproval' -GraphBaseURL $GraphBaseURL

                        if ($NewRelationshipRequest.action -ne 'lockForApproval')
                        {
                            Write-LogMessage -Severity Warning -Message "Unable to create relationship request lock, returning adminRelationship anyway."
                        }

                        $DelegatedAdminRelationship = Format-AdminRelationship -AdminRelationship $CreateDelegatedAdminRelationship -Detailed:$Detailed

                    }

                    elseif ($CheckActive.status -eq 'created')
                    {
                        $DelegatedAdminRelationship = Format-AdminRelationship -AdminRelationship $CreateDelegatedAdminRelationship -Detailed:$Detailed
                    }

                    else
                    {
                        $DelegatedAdminRelationship = Format-AdminRelationship -AdminRelationship $CreateDelegatedAdminRelationship -Detailed:$Detailed
                        Write-LogMessage -Severity Warning -Message "Admin relationship created but was not found in correct state in a timely manner. Unable to complete relationship.`n RelationshipID: $($CreateDelegatedAdminRelationship.id)`n Current state: $($CheckActive.status)"
                    }
                }
                else
                {
                    Write-LogMessage -Severity Warning -Message "Graph API status code: $($IMGRStatusCode)"
                    throw "No valid admin relationship created."
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when created GDAP relationship, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_ -StopOnError
        }
    }

    end
    {
        $PSCmdlet.WriteObject($DelegatedAdminRelationship, $true)
        Write-LogMessage -Severity Verbose -Message "Ending New-GDAPRelationship"
    }
} #New-GDAPRelationship


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function New-GDAPRelationshipAccessAssignment
{

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([object])]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID provided during the GDAP relationship request creation process.")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory, Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Entra ID Group Object ID Guid or Display Name to assign specific Entra ID roles.")]
        [ValidateNotNullOrEmpty()]
        [string]$Group,

        [Parameter(Mandatory, Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "List of Entra ID role Guids or role Names to be assigned to the security group in the GDAP Relationship")]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Generic.List[string]]$RoleDefinition,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "Flag to indicate that role and security group details should be included in the object")]
        [BoolParseTransformation()]
        [switch]$Detailed,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting New-GDAPRelationshipAccessAssignment"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }

    process
    {
        try
        {
            # Verify that the base URL ends in trailing slash
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }

            $GDAPRelationshipStatus = Get-GDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -GraphBaseURL $GraphBaseURL
            if ($GDAPRelationshipStatus.status -notin 'active', 'approved', 'activating')
            {
                Write-LogMessage -Severity Warning -Message "The GDAP relationship request with ID $($GDAPRelationshipID)$(if ($GDAPRelationshipStatus.customer.displayName) { " for $($GDAPRelationshipStatus.customer.displayName)" }) has not been completed or needs more time to finalize provisioning." ; break
            }

            # Build the Graph request URL
            $DelegatedAdminAccessAssignmentURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships/$($GDAPRelationshipID)/accessAssignments"

            # Perform group lookup if not provided in Guid format
            Write-LogMessage -Severity Verbose -Message "Checking if the Group was provided in GUID ObjectId format"
            if (Test-Guid $Group)
            {
                Write-LogMessage -Severity Verbose -Message "GUID detected, leaving group as is"
            }
            else
            {
                Write-LogMessage -Severity Verbose -Message "Performing group lookup for $Group"
                $Group = (Get-EntraGroupbyNameorId -Group $Group -GraphBaseURL $GraphBaseURL).id
            }

            # Perform role definition lookups
            Write-LogMessage -Severity Verbose -Message "Looking up the provided RoleDefinition(s)"
            $RoleDefinitionId = Get-GDAPAccessRolebyNameorId -RoleDefinition $RoleDefinition -ReturnID

            # Build the RoleAccessContainer object with updated/provided values

            Write-LogMessage -Severity Verbose -Message "Building the RoleAccessContainer object"
            Write-LogMessage -Severity Debug -Message "RoleAccesContainer input parameters:`n GroupId: $($Group)`n roleDefinitionId: $($RoleDefinitionId -join ", ")"
            $RoleAccessContainer = @{
                accessContainer = @{
                    accessContainerId   = $Group
                    accessContainerType = "securityGroup"
                }
                accessDetails   = @{
                    unifiedRoles = @(
                        $RoleDefinitionId | ForEach-Object {
                            @{ roleDefinitionId = $_ }
                        }
                    )
                }
            }
            Write-LogMessage -Severity Debug -Message "Result: `n$($RoleAccessContainer | ConvertTo-Json -Depth 10)"

            # Compare the provided Role IDs with the Role IDs found in the GDAP relationship to verify they can all be used, remove those that can't from the RoleAccessContainer.
            Write-LogMessage -Severity Verbose -Message "Comparing specified roles with allowed role IDs in the GDAP relationship."
            Write-LogMessage -Severity Debug -Message "Comparing roleDefinitionId(s) from:`n GDAPRelationshipStatus: $($GDAPRelationshipStatus.accessDetails.unifiedRoles.roleDefinitionId -join ", ")`n RoleAccessContainer: $($RoleAccessContainer.accessDetails.unifiedRoles.roleDefinitionId -join ", ")"
            $CompareRoles = (Compare-Object -ReferenceObject $GDAPRelationshipStatus.accessDetails.unifiedRoles.roleDefinitionId -DifferenceObject $RoleAccessContainer.accessDetails.unifiedRoles.roleDefinitionId -IncludeEqual | Where-Object -FilterScript { $_.SideIndicator -eq '=>' }).InputObject
            if ($CompareRoles.Count -ge 1)
            {
                Write-LogMessage -Severity Warning -Message "The following Role IDs were not found in the active GDAP relationship, they will be skipped:`n$($CompareRoles)"
                $RoleAccessContainer.accessDetails.unifiedRoles = $RoleAccessContainer.accessDetails.unifiedRoles | Where-Object { $_.roleDefinitionId -notin $CompareRoles }
            }

            # Submit the Graph API request and receieve the generated accessAssignment object
            Write-LogMessage -Severity Verbose -Message "Setting GDAP access assignment from Graph API: 'Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminAccessAssignmentURL -Body (`$RoleAccessContainer | ConvertTo-Json -Depth 5) -StatusCodeVariable IMGRStatusCode -OutputType PSObject'"
            Write-LogMessage -Severity Debug -Message "Graph Request Body value for access assigments:`n$($RoleAccessContainer | ConvertTo-Json -Depth 10))"
            if ($PSCmdlet.ShouldProcess(("Request body: `n$($RoleAccessContainer | ConvertTo-Json -Depth 10)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminAccessAssignmentURL -StatusCodeVariable IMGRStatusCode -OutputType PSObject")))
            {
                $CreateDelegatedAdminRelationshipAccessAssignment = Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminAccessAssignmentURL -Body ($RoleAccessContainer | ConvertTo-Json -Depth 5) -StatusCodeVariable IMGRStatusCode -OutputType PSObject
                Write-LogMessage -Severity Debug -Message "Result:`n $($CreateDelegatedAdminRelationshipAccessAssignment | ConvertTo-Json -Depth 10)"
                if (-not ([string]::IsNullOrEmpty($CreateDelegatedAdminRelationshipAccessAssignment.id)) -and $IMGRStatusCode -eq '201')
                {
                    $FormattedAccessAssignment = Format-AccessAssignment -AccessAssignment $CreateDelegatedAdminRelationshipAccessAssignment -GDAPRelationshipID $GDAPRelationshipID -Detailed:$Detailed -GraphBaseURL $GraphBaseURL
                }
                else
                {
                    Write-LogMessage -Severity Warning -Message "Graph API status code: $($IMGRStatusCode)"
                    throw "No valid admin access assignment created for group ID $($RoleAccessContainer.accessContainer.accessContainerId)."
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when creating GDAP access assignments, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }

    end
    {
        $PSCmdlet.WriteObject($FormattedAccessAssignment, $true)
        Write-LogMessage -Severity Verbose -Message "Ending New-GDAPRelationshipAccessAssignment"
    }
} #New-GDAPRelationshipAccessAssignment


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Remove-GDAPRelationship
{
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "ByObject")]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory, ParameterSetName = "ByObject", Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Object containing details from type delegatedAdminRelationship")]
        [ValidateScript({
                if ([string]::IsNullOrEmpty($_.id))
                {
                    throw "Passed GDAPRelationshipObject is not a valid delegatedAdminRelationship object, property `"id`" is missing or empty"
                }
                elseif (-not ($_.id -match `
                            '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'))
                {
                    throw "Passed GDAPRelationshipObject is not a valid delegatedAdminRelationship object, property `"id`" with value `"$($_.id)`" is not in a valid format"
                }
                elseif ([string]::IsNullOrEmpty($_."@odata.etag"))
                {
                    throw "Passed GDAPRelationshipObject is not a valid delegatedAdminRelationship object, property `"@odata.etag`" is missing or empty"
                }
                elseif ([string]::IsNullOrEmpty($_.status) -or $_.status -notin "active", "created")
                {
                    throw "Passed GDAPRelationshipObject is not a valid delegatedAdminRelationship object, property `"status`" is missing, empty, or not in a valid state$(if (-not ([string]::IsNullOrEmpty($_.status))) { " ($($_.status))" })"
                }
                else
                {
                    $true
                }
            })]
        [object]$GDAPRelationshipObject,

        [Parameter(Mandatory, ParameterSetName = "ByID", Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID provided during the GDAP relationship request creation process.")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Remove-GDAPRelationship"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }
    process
    {
        $IMGRStatusCode = $null

        try
        {
            # Verify that the base URL ends in trailing slash
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }

            # Retrieve the delegatedAdminRelationship object if only the GDAPRelationshipID is provided
            switch ($PsCmdlet.ParameterSetName)
            {
                "ById"
                {
                    $GDAPRelationshipObject = Get-GDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -GraphBaseURL $GraphBaseURL
                    # Validate that the GDAP Relationship is in a valid state
                    if ($GDAPRelationshipObject.status -notin "active", "created")
                    {
                        Write-LogMessage -Severity Error -Message "Existing GDAP Relationship not in a terminatable state, current state $($GDAPRelationshipObject.status), skipping" -TargetObject $GDAPRelationshipObject -StopOnError
                    }
                }
            }

            Write-LogMessage -Severity Verbose -Message "Attempting to remove existing GDAP relationship $($GDAPRelationshipObject.displayName) ($($GDAPRelationshipObject.id))"

            if ($GDAPRelationshipObject.status -in "active")
            {
                # Terminate
                $NewRelationshipRequest = New-GDAPRelationshipRequest -GDAPRelationshipID $GDAPRelationshipObject.id -Action 'terminate' -GraphBaseURL $GraphBaseURL

                if ($NewRelationshipRequest.action -notin 'terminate')
                {
                    Write-LogMessage -Severity Warning -Message "Unable to create relationship request termination"
                    $false
                }
                else
                {
                    Write-LogMessage -Severity Verbose -Message "Successfully terminated existing GDAP relationship $($GDAPRelationshipObject.id)"
                    $true
                }
            }

            else
            {
                # Build the Graph API URL
                $GDAPRelationshipTerminationURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships/$($GDAPRelationshipObject.id)"

                Write-LogMessage -Severity Verbose -Message "Terminating GDAP Relationship from Graph API: 'Invoke-MgGraphRequest -Method DELETE -Uri $GDAPRelationshipTerminationURL -Headers @{ `"If-Match`" = ($($GDAPRelationshipObject."@odata.etag")) } -StatusCodeVariable IMGRStatusCode -OutputType PSObject'"
                if ($PSCmdlet.ShouldProcess($GDAPRelationshipTerminationURL, ("Invoke-MgGraphRequest -Method DELETE -Uri $GDAPRelationshipTerminationURL -Headers @{ `"If-Match`" = ($($GDAPRelationshipObject."@odata.etag")) } -StatusCodeVariable IMGRStatusCode -OutputType PSObject")))
                {
                    # Call the Graph API termination command
                    $GDAPRelationshipTermination = Invoke-MgGraphRequest -Method DELETE -Uri $GDAPRelationshipTerminationURL -Headers @{ "If-Match" = ($GDAPRelationshipObject."@odata.etag") } -StatusCodeVariable IMGRStatusCode -OutputType PSObject
                    Write-LogMessage -Severity Debug -Message "Result:`n $($GDAPRelationshipTermination | ConvertTo-Json -Depth 10)"

                    # Verify the Graph API command returned the valid Status Code, return the success or failure termination bool
                    if ($IMGRStatusCode -eq '204')
                    {
                        Write-LogMessage -Severity Verbose -Message "Successfully terminated existing GDAP relationship $($GDAPRelationshipObject.id)"
                        $true
                    }
                    else
                    {
                        Write-LogMessage -Severity Warning -Message "Failed to terminate existing GDAP relationship $($GDAPRelationshipObject.id)"
                        Write-LogMessage -Severity Warning -Message "Graph API status code: $($IMGRStatusCode)"
                        $false
                    }
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when trying to terminate GDAP relationship, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)" -LastException $_ -StopOnError
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_ -StopOnError
        }
    }

    end
    {
        Write-LogMessage -Severity Verbose -Message "Ending Remove-GDAPRelationship"
    }
} #Remove-GDAPRelationship


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Remove-GDAPRelationshipAccessAssignment
{
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "ByObject")]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory, ParameterSetName = "ByID", Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP Relationship ID for the relationship containing the delegatedAdminAccessAssignment to be removed")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory, ParameterSetName = "ByObject", Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Object containing details from type delegatedAdminAccessAssignment")]
        [ValidateScript({
                if ([string]::IsNullOrEmpty($_.delegatedAdminRelationshipId))
                {
                    throw "Passed AccessAssignmentObject is not a valid delegatedAdminAccessAssignment object, property `"delegatedAdminRelationshipId`" is missing or empty"
                }
                elseif ($_.delegatedAdminRelationshipId -notmatch `
                        '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$')
                {
                    throw "Passed AccessAssignmentObject is not a valid delegatedAdminAccessAssignment object, property `"delegatedAdminRelationshipId`" is not in a valid format"
                }
                elseif ([string]::IsNullOrEmpty($_.id))
                {
                    throw "Passed AccessAssignmentObject is not a valid delegatedAdminAccessAssignment object, property `"id`" is missing or empty"
                }
                elseif (-not (Test-Guid -InputObject $_.id))
                {
                    throw "Passed AccessAssignmentObject is not a valid delegatedAdminAccessAssignment object, property `"id`" with value `"$($_.id)`" is not in guid format"
                }
                elseif ([string]::IsNullOrEmpty($_."@odata.etag"))
                {
                    throw "Passed AccessAssignmentObject is not a valid delegatedAdminAccessAssignment object, property `"@odata.etag`" is missing or empty"
                }
                elseif ([string]::IsNullOrEmpty($_.status) -or $_.status -notin "active", "expiring")
                {
                    throw "Passed AccessAssignmentObject is not a valid delegatedAdminAccessAssignment object, property `"status`" is missing, empty, or not in a valid state$(if (-not ([string]::IsNullOrEmpty($_.status))) { " ($($_.status))" })"
                }
                else
                {
                    $true
                }
            })]
        [object]$AccessAssignmentObject,

        [Parameter(Mandatory, ParameterSetName = "ByID", Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GUID formatted id of the delegatedAdminAccessAssignment to be removed.")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Guid -InputObject $_ })]
        [string]$AccessAssignmentId,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Remove-GDAPRelationshipAccessAssignment"

        if (-not (Get-MgContext))
        {
            Write-LogMessage -Severity Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API" ; break
        }
    }
    process
    {
        $IMGRStatusCode = $null

        try
        {
            # Verify that the base URL ends in trailing slash
            if ($GraphBaseURL -notmatch '.*/$')
            {
                $GraphBaseURL = $GraphBaseURL + "/"
            }

            $GDAPRelationshipObject = Get-GDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -GraphBaseURL $GraphBaseURL

            if (-not ($GDAPRelationshipObject) -or $GDAPRelationshipObject.status -notin "active", "expiring")
            {
                Write-LogMessage -Severity Error -Message "The provided GDAP Relationship ID, $GDAPRelationshipID, did not return a valid GDAP Relationship or the GDAP Relationship is not in a valid state for this operation" -LastException $_ ; break
            }

            # Retrieve the delegatedAdminRelationship object if only the GDAPRelationshipID is provided
            switch ($PsCmdlet.ParameterSetName)
            {
                "ById"
                {
                    $AccessAssignmentObject = Get-GDAPRelationshipAccessAssignment -GDAPRelationshipID $GDAPRelationshipID -AccessAssignmentId $AccessAssignmentId -GraphBaseURL $GraphBaseURL | Select-Object -First 1
                }
            }

            Write-LogMessage -Severity Verbose -Message "Attempting to remove existing GDAP admin access assignment from GDAP Relationship $($GDAPRelationshipObject.displayName) with id of ($($AccessAssignmentObject.id))"

            # Build the Graph API URL
            $GDAPAccessAssignmentTerminationURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships/$($GDAPRelationshipObject.id)/accessAssignments/$($AccessAssignmentObject.id)"

            # Validate that the GDAP Relationship is in a valid state
            if ($AccessAssignmentObject.status -notin "active", "expiring")
            {
                throw "Existing GDAP admin access assignment not found or not in a terminatable state, current state $($AccessAssignmentObject.status), skipping"
            }
            else
            {
                Write-LogMessage -Severity Verbose -Message "Terminating GDAP admin access assignment from Graph API: 'Invoke-MgGraphRequest -Method DELETE -Uri $GDAPAccessAssignmentTerminationURL -Headers @{ `"If-Match`" = ($($AccessAssignmentObject."@odata.etag")) } -StatusCodeVariable IMGRStatusCode -OutputType PSObject'"
                if ($PSCmdlet.ShouldProcess($GDAPAccessAssignmentTerminationURL, ("Invoke-MgGraphRequest -Method DELETE -Uri $GDAPAccessAssignmentTerminationURL -Headers @{ `"If-Match`" = ($($AccessAssignmentObject."@odata.etag")) } -StatusCodeVariable IMGRStatusCode -OutputType PSObject")))
                {
                    # Call the Graph API termination command
                    $GDAPAccessAssignmentTermination = Invoke-MgGraphRequest -Method DELETE -Uri $GDAPAccessAssignmentTerminationURL -Headers @{ "If-Match" = ($AccessAssignmentObject."@odata.etag") } -StatusCodeVariable IMGRStatusCode -OutputType PSObject
                    Write-LogMessage -Severity Debug -Message "Result:`n $($GDAPAccessAssignmentTermination | ConvertTo-Json -Depth 10)"

                    # Verify the Graph API command returned the valid Status Code, return the success or failure termination bool
                    if ($IMGRStatusCode -eq '204')
                    {
                        Write-LogMessage -Severity Verbose -Message "Successfully terminated existing GDAP admin access assignment $($AccessAssignmentObject.id) from GDAP Relationship $($GDAPRelationshipObject.displayName)"
                        $true
                    }
                    else
                    {
                        Write-LogMessage -Severity Warning -Message "Failed to terminate existing GDAP relationship $($AccessAssignmentObject.id) from GDAP Relationship $($GDAPRelationshipObject.displayName)"
                        Write-LogMessage -Severity Warning -Message "Graph API status code: $($IMGRStatusCode)"
                        $false
                    }
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-LogMessage -Severity Error -Message "HTTP error when trying to terminate GDAP relationship, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)" -LastException $_
        }
        catch
        {
            Write-LogMessage -Severity Error -Message $_.Exception.Message -LastException $_
        }
    }

    end
    {
        Write-LogMessage -Severity Verbose -Message "Ending Remove-GDAPRelationshipAccessAssignment"
    }
} #Remove-GDAPRelationshipAccessAssignment


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Start-GDAPRemediation
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'GraphBaseURL', Justification = 'Testing')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "Local file path(s) or URL string(s) with the JSON template file(s) containing the GDAP Remediation template")]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Generic.List[string]]$RemediationTemplateFile,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Start-GDAPRemediation"
    }

    process
    {
        # Verify that the base URL ends in trailing slash
        if ($GraphBaseURL -notmatch '.*/$')
        {
            $GraphBaseURL = $GraphBaseURL + "/"
        }

        [System.Collections.Generic.List[object]]$RemediationObject = $RemediationTemplateFile | ForEach-Object {
            if (Test-Path $_ -IsValid)
            {
                Write-LogMessage -Severity Verbose -Message "Local path of $_ is valid, importing template file."
                Get-Content -Path (Resolve-Path -Path $_) | ConvertFrom-Json -Depth 64
            }
            elseif (Test-Url -Url $_)
            {
                Write-LogMessage -Severity Verbose -Message "URL of $_ is valid, importing template file."
                Get-JSONFromURL -JSONFileUrl $_
            }
            else
            {
                Write-LogMessage -Severity Error -Message "$($_) is not a valid local file path or URL" -LastException $_ ; break
            }
        }

        if ($RemediationObject.Count -eq 0)
        {
            Write-LogMessage -Severity Error -Message "No Remediation Templates imported, unable to continue." -LastException $_ ; break
        }


        if ($PSCmdlet.ShouldProcess(("Doing a thing"), ("Thing I might be doing")))
        {
            "Do the thing"
        }
    }

    end
    {
        Write-LogMessage -Severity Verbose -Message "Ending Start-GDAPRemediation"
    }

} #Start-GDAPRemediation


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Test-GDAPRelationshipStatus
{

    [CmdletBinding(DefaultParameterSetName = "Test")]
    [OutputType([bool], ParameterSetName = "Test")]
    [OutputType([object], ParameterSetName = "Differences")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'RelationshipMatches', Justification = 'False positive as rule does not scan child scopes')]
    param (
        [Parameter(Mandatory, ParameterSetName = "Test", Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID to use for accessAssignments lookup")]
        [Parameter(Mandatory, ParameterSetName = "Differences", Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "The GDAP relationship ID to use for accessAssignments lookup")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                $_ -match `
                    '^((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))-((?:\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\}{0,1}))$'
            } )]
        [string]$GDAPRelationshipID,

        [Parameter(Mandatory, ParameterSetName = "Test", Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "List containing the delegatedAdminAccessAssignment objects to validate the relationship against")]
        [Parameter(Mandatory, ParameterSetName = "Differences", Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "List containing the delegatedAdminAccessAssignment objects to validate the relationship against")]
        [System.Collections.Generic.List[object]]$DelegatedAdminAccessAssignment,

        [Parameter(Mandatory = $false, ParameterSetName = "Test", Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "List of Entra ID role Guids or role Names to compare to the list of roles assigned to the adminRelationship")]
        [Parameter(Mandatory = $false, ParameterSetName = "Differences", Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName,
            HelpMessage = "List of Entra ID role Guids or role Names to compare to the list of roles assigned to the adminRelationship")]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Generic.List[string]]$RoleDefinition,

        [Parameter(Mandatory = $false, ParameterSetName = "Differences", ValueFromPipelineByPropertyName,
            HelpMessage = "Enable the return of the differences between the existing and provided relationships")]
        [BoolParseTransformation()]
        [switch]$Differences,

        [Parameter(Mandatory = $false, ParameterSetName = "Test", ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [Parameter(Mandatory = $false, ParameterSetName = "Differences", ValueFromPipelineByPropertyName,
            HelpMessage = "The base Microsoft Graph API URL to use with trailing slash, e.g. https://graph.microsoft.com/v1.0/")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL = "https://graph.microsoft.com/v1.0/"
    )

    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        Write-LogMessage -Severity Verbose -Message "Starting Test-GDAPRelationshipStatus"

        $RelationshipMatches = $true

        # Differences object template
        $DifferencesObject = @{
            GDAPRelationshipID             = $null
            Matches                        = $RelationshipMatches
            MissingRelationshipRoles       = [System.Collections.Generic.List[string]]::new()
            ExtraRelationshipRoles         = [System.Collections.Generic.List[string]]::new()
            MissingAccessAssignments       = [System.Collections.Generic.List[object]]::new()
            ExtraAccessAssignments         = [System.Collections.Generic.List[object]]::new()
            IncorrectAccessAssignments     = [System.Collections.Generic.List[object]]::new()
            InvalidSourceAccessAssignments = [System.Collections.Generic.List[object]]::new()
            MatchingAccessAssignments      = [System.Collections.Generic.List[object]]::new()
        }
    }

    process
    {
        $DifferencesObject.GDAPRelationshipID = $GDAPRelationshipID

        Write-LogMessage -Severity Verbose -Message "Retrieving the existing GDAP Relationship"
        $ExistingGDAPRelationship = Get-GDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -GraphBaseURL $GraphBaseURL

        if ($ExistingGDAPRelationship.status -notin 'active', 'approved', 'activating', 'expiring')
        {
            Write-LogMessage -Severity Warning -Message "The GDAP relationship request with ID $($GDAPRelationshipID)$(if ($ExistingGDAPRelationship.customer.displayName) { " for $($ExistingGDAPRelationship.customer.displayName)" }) is not in an active state." ; break
        }
        else
        {
            Write-LogMessage -Severity Verbose -Message "Retrieving the existing accessAssignments"
            $ExistingGDAPAccessAssignment = Get-GDAPRelationshipAccessAssignment -GDAPRelationshipID $GDAPRelationshipID -Active -GraphBaseURL $GraphBaseURL
        }

        Write-LogMessage -Severity Verbose -Message "Validating the list of role IDs currently in the adminRelationship"

        Write-LogMessage -Severity Verbose -Message "Generating the list of required role IDs from the provided accessAssignments"
        [System.Collections.Generic.List[string]]$RequiredRoleID = $DelegatedAdminAccessAssignment | ForEach-Object {
            $_.accessDetails.unifiedRoles.roleDefinitionId
        } | Select-Object -Unique

        if ($RoleDefinition)
        {
            Write-LogMessage -Severity Verbose -Message "Expected Role Definition list included, adding to required roles list"
            $RoleDefinitionId = (Get-GDAPAccessRolebyNameorId -RoleDefinition $RoleDefinition).RoleDefinitionId

            $RequiredRoleID = $RoleDefinitionId + $RequiredRoleIDs | Select-Object -Unique
        }

        Write-LogMessage -Severity Verbose -Message "Comparing roleDefinitionId(s) from:`n Discovered roles: $($ExistingGDAPRelationship.accessDetails.unifiedRoles.roleDefinitionId -join ", ")`n Provided roles: $($RequiredRoleID -join ", ")"
        $CompareRoles = Compare-Object -ReferenceObject $ExistingGDAPRelationship.accessDetails.unifiedRoles.roleDefinitionId -DifferenceObject $RequiredRoleID
        $CompareRoleswithExisting = ( $CompareRoles | Where-Object -FilterScript { $_.SideIndicator -eq '=>' }).InputObject
        if ($CompareRoleswithExisting.Count -ge 1)
        {
            Write-LogMessage -Severity Verbose -Message "The following required Role IDs were not found in the active GDAP relationship:`n$($CompareRoleswithExisting -join ',')"
            if ($Differences) { $DifferencesObject.MissingRelationshipRoles.Add($CompareRoleswithExisting) }
            $RelationshipMatches = $false
        }
        $CompareRoleswithProvided = ( $CompareRoles | Where-Object -FilterScript { $_.SideIndicator -eq '<=' }).InputObject
        if ($CompareRoleswithProvided.Count -ge 1)
        {
            Write-LogMessage -Severity Verbose -Message "The following Role IDs were not found in the required Role IDs but exist in the active GDAP relationship:`n$($CompareRoleswithProvided -join ',')"
            if ($Differences) { $DifferencesObject.ExtraRelationshipRoles.Add($CompareRoleswithProvided) }
            $RelationshipMatches = $false
        }

        if (((-not $Differences) -and $RelationshipMatches) -or ($Differences))
        {
            Write-LogMessage -Severity Verbose -Message "Comparing the provided accessAssignment objects against the existing accessAssignment objects"
            foreach ($AccessAssignment in $DelegatedAdminAccessAssignment)
            {
                switch (Compare-GDAPAccessAssignment -GDAPRelationshipID $GDAPRelationshipID -DelegatedAdminAccessAssignment $AccessAssignment -Reason:$Differences -GraphBaseURL $GraphBaseURL)
                {
                    "Invalid delegatedAdminAccessAssignment"
                    {
                        $DifferencesObject.InvalidSourceAccessAssignments.Add($AccessAssignment)
                    }

                    "Invalid Group"
                    {
                        $DifferencesObject.InvalidSourceAccessAssignments.Add($AccessAssignment)
                    }

                    "Missing Roles"
                    {
                        $DifferencesObject.IncorrectAccessAssignments.Add($AccessAssignment)
                        $RelationshipMatches = $false
                    }

                    "Extra Roles"
                    {
                        $DifferencesObject.IncorrectAccessAssignments.Add($AccessAssignment)
                        $RelationshipMatches = $false
                    }

                    "Missing Assignment"
                    {
                        $DifferencesObject.MissingAccessAssignments.Add($AccessAssignment)
                        $RelationshipMatches = $false
                    }

                    $false
                    {
                        $RelationshipMatches = $false
                    }

                    $true
                    {
                        $DifferencesObject.MatchingAccessAssignments.Add($AccessAssignment)
                    }
                }
            }

            Write-LogMessage -Severity Verbose -Message "Looking for existing accessAssignment objects that are not in the provided list of accessAssignment objects"
            $ExistingGDAPAccessAssignment | Where-Object { $_.accessContainer.accessContainerId -notin $DifferencesObject.IncorrectAccessAssignments.accessContainer.accessContainerId -and $_.accessContainer.accessContainerId -notin $DifferencesObject.MissingAccessAssignments.accessContainer.accessContainerId -and $_.accessContainer.accessContainerId -notin $DifferencesObject.MatchingAccessAssignments.accessContainer.accessContainerId } | ForEach-Object {
                if ($Differences) { $DifferencesObject.ExtraAccessAssignments.Add($_) }
                $RelationshipMatches = $false
            }
        }
    }

    end
    {
        if ($Differences)
        {
            $DifferencesObject.Matches = $RelationshipMatches
            $PSCmdlet.WriteObject($DifferencesObject, $true)
        }
        else
        {
            $RelationshipMatches
        }
        Write-LogMessage -Severity Verbose -Message "Ending Test-GDAPRelationshipStatus"
    }
} #Test-GDAPRelationshipStatus