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"


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

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

        [Parameter()]
        [switch]$Detailed,

        [Parameter(Mandatory)]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

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

    process
    {
        try
        {
            Write-Verbose -Message "Processing accessAssignment $($AccessAssignment.id)"
            # Add detailed object information if the detailed flag is present
            if ($Detailed)
            {
                Write-Verbose -Message "Detailed flag found, updating delegatedAdminAccessAssignment object with delegatedAdminRelationshipId, accessContainerDisplayName, and roleDefinition details"
                Add-Member -InputObject $AccessAssignment -Name "delegatedAdminRelationshipId" -MemberType NoteProperty -Value $GDAPRelationshipID
                Add-Member -InputObject $AccessAssignment.accessContainer -Name "accessContainerDisplayName" -MemberType NoteProperty -Value (Get-EntraGroupbyNameorId -Group $AccessAssignment.accessContainer.accessContainerId -GraphBaseURL $GraphBaseURL).displayName
                $AccessAssignment.accessDetails.unifiedRoles = Get-GDAPAccessRolebyNameorId -RoleDefinition $AccessAssignment.accessDetails.unifiedRoles.roleDefinitionId
            }
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
    end
    {
        $PSCmdlet.WriteObject($AccessAssignment, $true)
    }
} #Format-AccessAssignment

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

        [Parameter()]
        [switch]$Detailed
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            Write-Verbose -Message "Processing adminRelationship $($AdminRelationship.id)"
            # Add detailed object information if the detailed flag is present
            if ($Detailed)
            {
                Write-Verbose -Message "Detailed flag found, updating delegatedAdminRelationship object with roleDefinition details"
                $AdminRelationship.accessDetails.unifiedRoles = Get-GDAPAccessRolebyNameorId -RoleDefinition $AdminRelationship.accessDetails.unifiedRoles.roleDefinitionId
            }
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
    end
    {
        $PSCmdlet.WriteObject($AdminRelationship, $true)
    }
} #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'
            '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-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-Error -Message $_.Exception.Message
        }
    }
    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

        if (-not (Get-MgContext))
        {
            Write-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/$($Group.Trim())?`$select=id,displayName,groupTypes,mailEnabled,securityEnabled"
            }
            else
            {
                $GroupLookupURL = "$($GraphBaseURL)groups?`$filter=displayName eq ('$($Group.Trim())')&`$select=id,displayName,groupTypes,mailEnabled,securityEnabled"
            }

            Write-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-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-Error -Message "HTTP error when retrieving Graph Groups, status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
    end
    {
        $PSCmdlet.WriteObject($GroupObject, $true)
    }
} #Get-EntraGroupbyNameorId

Function Get-ExistingCustomer
{
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory)]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        if (-not (Get-MgContext))
        {
            Write-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-Verbose -Message "Getting existing customers from Graph API: 'Invoke-MgGraphRequest -Method GET -Uri $ExistingCustomersURL -OutputType PSObject'"
            $ExistingCustomersObjects = (Invoke-MgGraphRequest -Method GET -Uri $ExistingCustomersURL -OutputType PSObject).value
            Write-Debug -Message "Result:`n $($ExistingCustomersObjects | ConvertTo-Json -Depth 5)"
            Write-Verbose -Message "Generating and displaying GridView for customer selection"
            $SelectedCustomerObject = $ExistingCustomersObjects | Select-Object -Property displayName, tenantId | Sort-Object -Property displayName | Out-GridView -Title 'Select Customer' -PassThru
            Write-Debug -Message "Result:`n $($SelectedCustomerObject | ConvertTo-Json -Depth 5)"
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "Unable to retrieve existing customers from $($ExistingCustomersURL), status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
    end
    {
        if ($null -ne $SelectedCustomerObject)
        {
            return $SelectedCustomerObject
        }
        else
        {
            throw "No existing customer object selected, please run the script again or remove the `"-SelectFromExistingCustomers`" flag."
        }
    }
} #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
    }
    process
    {
        try
        {
            Write-Verbose -Message "Attempting to download JSON file from $($JSONFileURL) using Invoke-RestMethod"
            $JSONObject = Invoke-RestMethod -Method Get -Uri $JSONFileURL
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "Unable to download JSON file from $($JSONFileURL), status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
    end
    {
        return $JSONObject
    }
} #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
    }

    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-Verbose "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-Verbose "Current page value count: $($Page.'@odata.count')"
            }

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

    end {}
}

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
        if (-not (Get-MgContext))
        {
            Write-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='$($TenantID)')"

            Write-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-Debug -Message "Result:`n $($TenantInformation | ConvertTo-Json -Depth 5)"

            if (-not ([string]::IsNullOrEmpty($TenantInformation.displayName)))
            {
                $PSCmdlet.WriteObject($TenantInformation.displayName, $true)
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when generating GDAP relationship request, status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #Get-TenantDisplayNamebyId

Function New-GDAPRelationshipRequest
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([string])]
    param (
        [Parameter(Mandatory)]
        [string]$GDAPRelationshipID,
        [Parameter(Mandatory)]
        [string]$GraphBaseURL
    )
    begin
    {
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $DelegatedAdminCreateRequestURL = "$($GraphBaseURL)tenantRelationships/delegatedAdminRelationships/$($GDAPRelationshipID)/requests"
        $DelegatedAdminRelationshipBody = @{
            action = "lockForApproval"
        }
    }
    process
    {
        try
        {
            Write-Verbose -Message "Creating new GDAP request from Graph API: 'Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminCreateRequestURL -Body (`$DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 5) -OutputType PSObject'"
            Write-Debug -Message "Graph Request Body value for new request:`n$($RoleAccessContainerObjects | ConvertTo-Json -Depth 5)"
            if ($PSCmdlet.ShouldProcess(("Request body: `n$($RoleAccessContainerObjects | ConvertTo-Json -Depth 5)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminCreateRequestURL")))
            {
                $GDAPRelationShipRequest = Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminCreateRequestURL -Body ($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 5) -StatusCodeVariable IMGRStatusCode -OutputType PSObject
                Write-Debug -Message "Result:`n $($GDAPRelationShipRequest | ConvertTo-Json -Depth 5)"

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

<#
.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

<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

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

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

        Write-Verbose "Retrieving the list of GDAP Roles"
        $GDAPRoles = Get-GDAPRoleList
    }

    process
    {
        $GDAPRolesObject = $RoleDefinition | ForEach-Object {
            $RoleDefinitionId = $false
            $RoleDefinitionString = $null
            $GDAPRoleMatch = $null
            $RoleDefinitionString = $_.Trim()
            Write-Verbose "Checking if the RoleDefinition was provided in GUID Id format"
            if (Test-Guid $RoleDefinitionString)
            {
                Write-Debug "GUID detected, Setting `$RoleDefinitionID to true"
                $RoleDefinitionId = $true
            }

            if ($RoleDefinitionId)
            {
                Write-Verbose "Looking up GDAP Role by RoleDefinitionId"
                Write-Debug "Parsing `$GDAPRoles for a role with RoleDefinitionId value of $RoleDefinitionString"
                $GDAPRoleMatch = $GDAPRoles | Where-Object { $_.RoleDefinitionId -ieq $RoleDefinitionString } | Select-Object -First 1
                if ($GDAPRoleMatch)
                {
                    Write-Debug "Found role match name for $RoleDefinitionString of $($GDAPRoleMatch.Name)"
                    $GDAPRoleMatch
                }
                else
                {
                    Write-Warning "Unable to find a matching role by GUID for $RoleDefinitionString."
                }
            }
            else
            {
                Write-Verbose "Looking up GDAP Role by Name"
                Write-Debug "Parsing `$GDAPRoles for a role with Name value of $RoleDefinitionString"
                $GDAPRoleMatch = $GDAPRoles | Where-Object { $_.Name -ieq $RoleDefinitionString } | Select-Object -First 1
                if ($GDAPRoleMatch)
                {
                    Write-Debug "Found role match ID for $RoleDefinitionString of $($GDAPRoleMatch.RoleDefinitionId)"
                    $GDAPRoleMatch
                }
                else
                {
                    Write-Warning "Unable to find a matching role by Name for $RoleDefinitionString."
                }
            }
        }
    }

    end
    {
        Write-Debug "Found matches for $($GDAPRolesObject.Count) of $($RoleDefinition.Count) RoleDefinition(s)."
        $PSCmdlet.WriteObject($GDAPRolesObject, $true)
    }
} #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",
            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",
            HelpMessage = "Filter used to search relationships based on a specific value, uses OData query parameters")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { $_ -inotlike "`$filter=*" })]
        [string]$Filter,

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

        [Parameter(Mandatory = $false,
            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

        if (-not (Get-MgContext))
        {
            Write-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=$($Filter)&`$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-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-Debug -Message "Result:`n$($DelegatedAdminRelationships | ConvertTo-Json -Depth 10)"

            Write-Verbose "Processing returned array of objects"

            $DelegatedAdminRelationships | ForEach-Object {
                $DelegatedAdminRelationship = $null
                $DelegatedAdminRelationship = Format-AdminRelationship -AdminRelationship $_ -Detailed:$Detailed
                $DelegatedAdminRelationshipsObjects.Add($DelegatedAdminRelationship) | Out-Null
            }

            if ($DelegatedAdminRelationshipsObjects.Count -ge 1)
            {
                $PSCmdlet.WriteObject($DelegatedAdminRelationshipsObjects, $true)
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when retrieving GDAP relationships, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #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,
            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 = $false, ParameterSetName = "ByGroup",
            HelpMessage = "Filters the accessAssignments to specific groups")]
        [ValidateNotNullOrEmpty()]
        [string[]]$Group,

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

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

        [Parameter(Mandatory = $false,
            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

        if (-not (Get-MgContext))
        {
            Write-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)
            {
                "ByGroup"
                {
                    $Group = $Group | ForEach-Object {
                        Write-Verbose "Checking if the Group was provided in GUID ObjectId format"
                        if (Test-Guid $_)
                        {
                            Write-Debug "GUID detected, returning group as is"
                            $_
                        }
                        else
                        {
                            Write-Debug "Performing group lookup"
                            (Get-EntraGroupbyNameorId -Group $_ -GraphBaseURL $GraphBaseURL).id
                        }
                    }
                    $DelegatedAdminAccessAssignmentURL += "?`$filter=accessContainer/accessContainerId in ('$($Group -join "', '")')&`$count=true"
                }
                "ByFilter"
                {
                    $DelegatedAdminAccessAssignmentURL += "?`$filter=$($Filter)&`$count=true"
                }
                default
                {
                    $DelegatedAdminAccessAssignmentURL += "?`$count=true"
                }
            }

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

            # Submit the Graph API request and receive the delegatedAdminRelationship object
            Write-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-Debug -Message "Result:`n$($DelegatedAdminRelationshipAccessItems | ConvertTo-Json -Depth 5)"

            Write-Verbose "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-Debug -Message "Result:`n$($DelegatedAdminRelationshipAccessObjects | ConvertTo-Json -Depth 5)"

            if ($DelegatedAdminRelationshipAccessObjects.Count -ge 1)
            {
                $PSCmdlet.WriteObject($DelegatedAdminRelationshipAccessObjects, $true)
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when getting GDAP adminAccessAssignments, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #Get-GDAPRelationshipAccessAssignment


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-GDAPRelationshipRequestLink
{

    [CmdletBinding(DefaultParameterSetName = "NoEmail")]
    [OutputType([object])]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Email',
            HelpMessage = "The GDAP relationship ID provided during the GDAP relationship request creation process")]
        [Parameter(Mandatory, ParameterSetName = 'NoEmail',
            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',
            HelpMessage = "String containing the link that should be included as an indirect reseller link")]
        [Parameter(Mandatory = $false, ParameterSetName = 'NoEmail',
            HelpMessage = "String containing the link that should be included as an indirect reseller link")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$IndirectResellerLink,

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

        [Parameter(Mandatory = $false, ParameterSetName = 'Email',
            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

        if ($PsCmdlet.ParameterSetName -eq 'Email' -and $GenerateEmailText)
        {
            if (-not (Get-MgContext))
            {
                Write-Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API"; break
            }
            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-Warning "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-Error -Message $_.Exception.Message
            }
        }
    }
    process
    {
        # 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 ($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 ([System.DateTime]$GDAPRelationship.endDateTime))

Requested Entra ID roles:
"@


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

                }
            }
            # Build and return the output object
            Write-Verbose -Message "Generating GDAPInvitationLink value."
            $GDAPRelationshipRequest = @{
                GDAPInvitationLink = $GDAPInvitationLink
            }
            if ($IndirectResellerLink)
            {
                Write-Verbose -Message "Adding IndirectResellerLink value."
                $GDAPRelationshipRequest.Add('IndirectResellerLink', $IndirectResellerLink)
            }
            if ($EmailText)
            {
                Write-Verbose -Message "Generating EmailText value."
                $GDAPRelationshipRequest.Add('EmailText', $EmailText)
            }
            $PSCmdlet.WriteObject($GDAPRelationshipRequest)
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #Get-GDAPRelationshipRequestLinks


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-GDAPRoleList
{
    [CmdletBinding()]
    [OutputType([object])]
    param()

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

    try
    {
        Write-Verbose -Message 'Getting content of GDAPRoles.json.'
        $RoleExpression = @{n = "RoleDefinitionId"; e = { $_.ObjectId } }
        $RoleList = (Get-Content -Path (Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath 'Resources/GDAPRoles.json') -ErrorAction 'Stop' | ConvertFrom-Json) | Select-Object -Property Name, $RoleExpression, Description
        $PSCmdlet.WriteObject($RoleList, $true)
    }
    catch
    {
        throw "Can't retrieve the JSON GDAPRoles file. Verify the GDAPRelationships module is correctly installed."
    }
} #Get-GDAPRoleList


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function New-GDAPRelationship
{
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Generated")]
    [OutputType([object])]
    param (
        [Parameter(Mandatory = $false,
            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 = 'Named',
            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 = 'Generated',
            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 = 'Named',
            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,
            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,
            HelpMessage = "Should the relationship be set to auto extend using the allowed `"P180D`" parameter?")]
        [switch]$AutoExtendRelationship,

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

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

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

        [Parameter(Mandatory = $false,
            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

        if (-not (Get-MgContext))
        {
            Write-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(8, $RelationshipPrefix.Length)) -replace '[\s_-]+$'
            }

            # Build and normalize the GDAP Relationship displayName
            Write-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)
                })

            # Ensure the GDAP Relationship displayName is unique amongst all GDAP relationships, update with random characters until it is unique
            Write-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-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-Verbose -Message "GDAP Relationship Dispay Name: $($RelationshipDisplayName)"

            # Perform role definition lookups
            $RoleDefinition = (Get-GDAPAccessRolebyNameorId -RoleDefinition $RoleDefinition).RoleDefinitionId

            # Build the Graph API Message Body with available variables
            $DelegatedAdminRelationshipBody = [PSCustomObject]@{
                displayName   = $RelationshipDisplayName
                duration      = "P$($RelationshipExpirationInDays.ToString())D"
                accessDetails = @{
                    unifiedRoles = @(
                        $RoleDefinition | ForEach-Object {
                            @{ roleDefinitionId = $_ }
                        }
                    )
                }
            }
            if ($AutoExtendRelationship)
            {
                Add-Member -InputObject $DelegatedAdminRelationshipBody -MemberType NoteProperty -Name 'autoExtendDuration' -Value 'P180D'
            }
            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-Verbose -Message "Creating new GDAP relationship from Graph API: 'Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL -Body (`$DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 10) -StatusCodeVariable IMGRStatusCode -OutputType PSObject'"
            Write-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)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL -StatusCodeVariable IMGRStatusCode -OutputType PSObject")))
            {
                $CreateDelegatedAdminRelationship = Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL -Body ($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 10) -StatusCodeVariable IMGRStatusCode -OutputType PSObject
                Write-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')
                {
                    $FormattedDelegatedAdminRelationship = Format-AdminRelationship -AdminRelationship $CreateDelegatedAdminRelationship -Detailed:$Detailed
                    $PSCmdlet.WriteObject($FormattedDelegatedAdminRelationship, $true)
                }
                else
                {
                    throw "No valid admin relationship created."
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when created GDAP relationship, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #New-GDAPRelationship


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function New-GDAPRelationshipAccessAssignment
{

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([object])]
    param (
        [Parameter(Mandatory,
            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,
            HelpMessage = "Entra ID Group Object ID Guid or Display Name to assign specific Entra ID roles.")]
        [ValidateNotNullOrEmpty()]
        [string]$Group,

        [Parameter(Mandatory,
            HelpMessage = "Array of Entra ID role Guids or role Names to be assigned to the security group in the GDAP Relationship")]
        [ValidateNotNullOrEmpty()]
        [string[]]$RoleDefinition,

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

        [Parameter(Mandatory = $false,
            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

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

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

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

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

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

            # Perform role definition lookups
            Write-Verbose "Looking up the provided RoleDefinition(s)"
            $RoleDefinitionId = (Get-GDAPAccessRolebyNameorId -RoleDefinition $RoleDefinition).roleDefinitionId

            # Build the RoleAccessContainer object with updated/provided values

            Write-Verbose "Building the RoleAccessContainer object"
            Write-Debug "RoleAccesContainer input parameters:`n GroupId: $($Group)`n roleDefinitionId: $($RoleDefinitionId -join ", ")"
            $RoleAccessContainer = @{
                accessContainer = @{
                    accessContainerId   = $Group
                    accessContainerType = "securityGroup"
                }
                accessDetails   = @{
                    unifiedRoles = @(
                        $RoleDefinitionId | ForEach-Object {
                            @{ roleDefinitionId = $_ }
                        }
                    )
                }
            }
            Write-Debug "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-Verbose -Message "Comparing specified roles with allowed role IDs in the GDAP relationship."
            Write-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-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-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-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-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
                    $PSCmdlet.WriteObject($FormattedAccessAssignment, $true)
                }
                else
                {
                    throw "No valid admin access assignment created for group ID $($RoleAccessContainer.accessContainer.accessContainerId)."
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when creating GDAP access assignments, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #New-GDAPRelationshipAccessAssignment


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Remove-GDAPRelationship
{
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "ByObject")]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory, ParameterSetName = "ByObject", ValueFromPipeline,
            HelpMessage = "Object containing details from type delegatedAdminRelationship")]
        [ValidateScript({
                if ([string]::IsNullOrEmpty($_.id) -or [string]::IsNullOrEmpty($_."@odata.etag") -or [string]::IsNullOrEmpty($_.status))
                {
                    throw "Passed GDAPRelationshipObject is not a valid delegatedAdminRelationship object or is missing required properties"
                }
                else
                {
                    $true
                }
            })]
        [object]$GDAPRelationshipObject,

        [Parameter(Mandatory, ParameterSetName = "ByID",
            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,
            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

        if (-not (Get-MgContext))
        {
            Write-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
                }
            }

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

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

            # Validate that the GDAP Relationship is in a valid state
            if ($GDAPRelationshipObject.status -notin "active", "expiring")
            {
                throw "Existing GDAP Relationship not in a terminatable state, current state $($GDAPRelationshipObject.status), skipping"
            }
            else
            {
                Write-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(($GDAPRelationshipObject), ("Invoke-MgGraphRequest -Method DELETE -Uri $DelegatedAdminRelationshipURL -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-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-Verbose "Successfully terminated existing GDAP relationship $($GDAPRelationshipObject.id)"
                        $true
                    }
                    else
                    {
                        Write-Warning -Message "Failed to terminate existing GDAP relationship $($GDAPRelationshipObject.id)"
                        $false
                    }
                }
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when trying to terminate GDAP relationship, status code $($_.Exception.Response.StatusCode) ($($_.Exception.Response.StatusCode.value__))`n$($_.Exception.Response.StatusDescription)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #Remove-GDAPRelationship