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"


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-ExistingGDAPRelationship
{
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[object]])]
    param (
        [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, 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, ParameterSetName = "All",
            HelpMessage = "Specifies that all existing GDAP relationships should be retrieved")]
        [switch]$All,

        [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
    {
        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=300"
                }
            }

            $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
            if ($DelegatedAdminRelationships.value)
            {
                $DelegatedAdminRelationships.value | ForEach-Object { $DelegatedAdminRelationshipsObjects.Add($_) | Out-Null }
            }
            elseif ($DelegatedAdminRelationships.id)
            {
                $DelegatedAdminRelationshipsObjects.Add($DelegatedAdminRelationships) | Out-Null
            }
            if ($All -and $DelegatedAdminRelationships."@odata.nextLink")
            {
                do
                {
                    $DelegatedAdminRelationships = Invoke-MgGraphRequest -Method GET -Uri $DelegatedAdminRelationships."@odata.nextLink" -OutputType PSObject
                    $DelegatedAdminRelationships.value | ForEach-Object { $DelegatedAdminRelationshipsObjects.Add($_) | Out-Null }
                } until ([string]::IsNullOrEmpty($DelegatedAdminRelationships."@odata.nextLink"))
            }
            Write-Debug -Message "Result:`n$($DelegatedAdminRelationshipsObjects | ConvertTo-Json -Depth 5)"

            if ($DelegatedAdminRelationshipsObjects.Count -ge 1)
            {
                return $DelegatedAdminRelationshipsObjects
            }
            else
            {
                throw "No admin relationships returned"
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when created GDAP relationship, status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #Get-ExistingGDAPRelationships


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function Get-GDAPAccessRolebyNameorId
{
    [CmdletBinding(DefaultParameterSetName = "FromPipe")]
    [OutputType([System.Object])]
    param (
        [Parameter(Mandatory, ParameterSetName = "ByName",
            HelpMessage = "The Role Definition Name in GUID format to use to find the associated Role Defition ID GUID")]
        [ValidateNotNullOrEmpty()]
        [string]$RoleDefinitionName,

        [Parameter(Mandatory, ParameterSetName = "ByID",
            HelpMessage = "The Role Definition ID in GUID format to use to find the associated Role Name")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Guid -InputObject $_ })]
        [string]$RoleDefinitionId,

        [Parameter(Mandatory, ParameterSetName = "FromPipe", ValueFromPipeline = $true, Position = 0,
            HelpMessage = "The Role Definition string to find its matching Role Name or Role ID GUID")]
        [ValidateNotNullOrEmpty()]
        [string]$RoleDefinition
    )

    begin
    {
        if ($PsCmdlet.ParameterSetName -eq 'FromPipe')
        {
            if (Test-Guid $RoleDefinition)
            {
                $RoleDefinitionId = $RoleDefinition
            }
            else
            {
                $RoleDefinitionName = $RoleDefinition
            }
        }

        $GDAPRoles = Get-GDAPRoleList
    }

    process
    {
        if ($RoleDefinitionId)
        {
            try
            {
                $GDAPRoles | Where-Object { $_.ObjectId -ieq $RoleDefinitionId } | Select-Object -First 1
            }
            catch
            {
                throw "Unable to find a matching role with the given RoleDefinitionId."
            }
        }
        elseif ($RoleDefinitionName)
        {
            try
            {
                $GDAPRoles | Where-Object { $_.Name -ieq $RoleDefinitionName } | Select-Object -First 1
            }
            catch
            {
                throw "Unable to find a matching role with the given RoleDefinitionName."
            }
        }
    }
} #Get-GDAPAccessRolebyNameorId


<#
.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
    {
        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-ExistingGDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -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 = "$($ClientTenantName) - $($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 | ForEach-Object {
                    Get-GDAPAccessRolebyNameorId -RoleDefinitionId $_
                }

                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 Azure AD 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)
            }
            return $GDAPRelationshipRequest
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #Get-GDAPRelationshipRequestLinks


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

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

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


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function New-GDAPAccessAssignment
{

    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Separated")]
    [OutputType([string])]
    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, ParameterSetName = "Joined",
            HelpMessage = "Object containing Security Group Object ID with a list of Entra ID roles to assign")]
        [ValidateScript( {
                if ((Test-Guid -InputObject $_.Id -and (($_.RoleDefinitionId | ForEach-Object { Test-Guid -InputObject $_ }) -notcontains $false)))
                {
                    return $true
                }
                else
                {
                    throw "RoleAccessContainer value is invalid, must be an object containing a [string] Property `"Id`" with a value of the Object ID Guid of an Entra ID group`
                     and an array of [string] Properties `"Roles`" with a list of Entra ID role Guids."

                }

            })]
        [object]$RoleAccessContainer,

        [Parameter(Mandatory, ParameterSetName = "Separated",
            HelpMessage = "Security Group Object ID")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Guid -InputObject $_ })]
        [string]$GroupID,

        [Parameter(Mandatory, ParameterSetName = "Separated",
            HelpMessage = "Array of Entra ID role Guids to be assigned to the security group in the GDAP Relationship")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { ($_ | ForEach-Object { Test-Guid -InputObject $_ }) -notcontains $false })]
        [string[]]$RoleDefinitionId,

        [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
    {
        if (-not (Get-MgContext))
        {
            Write-Warning -Message "No Graph API login found, use Connect-MgGraph to login to the Graph API"; break
        }

        $GDAPRelationshipStatus = Get-ExistingGDAPRelationship -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"

            # Check which parameter set is used and build the RoleAccessContainer object with those values
            switch ($PsCmdlet.ParameterSetName)
            {
                "Joined"
                {
                    $RoleAccessContainer = @{
                        accessContainer = @{
                            accessContainerId   = $_.ID
                            accessContainerType = "securityGroup"
                        }
                        accessDetails   = @{
                            unifiedRoles = @(
                                $_.RoleDefinitionID | ForEach-Object {
                                    @{ roleDefinitionId = $_ }
                                }
                            )
                        }
                    }
                }
                "Separated"
                {
                    $RoleAccessContainer = @{
                        accessContainer = @{
                            accessContainerId   = $GroupID
                            accessContainerType = "securityGroup"
                        }
                        accessDetails   = @{
                            unifiedRoles = @(
                                $RoleDefinitionId | ForEach-Object {
                                    @{ roleDefinitionId = $_ }
                                }
                            )
                        }
                    }
                }
            }

            # 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."
            $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) -OutputType PSObject'"
            Write-Debug -Message "Graph Request Body value for access assigments:`n$($RoleAccessContainer | ConvertTo-Json -Depth 5))"
            if ($PSCmdlet.ShouldProcess(("Request body: `n$($RoleAccessContainer | ConvertTo-Json -Depth 5)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminAccessAssignmentURL")))
            {
                $CreateDelegatedAdminRelationshipAccessAssignment = Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminAccessAssignmentURL -Body ($RoleAccessContainer | ConvertTo-Json -Depth 5) -OutputType PSObject
                Write-Debug -Message "Result:`n $($CreateDelegatedAdminRelationshipAccessAssignment | ConvertTo-Json -Depth 5)"
                if (-not ([string]::IsNullOrEmpty($CreateDelegatedAdminRelationshipAccessAssignment.id)) -and ($CreateDelegatedAdminRelationshipAccessAssignment.status -ne 'error'))
                {
                    return $CreateDelegatedAdminRelationshipAccessAssignment
                }
                else
                {
                    throw "No valid admin access assignment created for group ID $($RoleAccessContainer.accessContainer.accessContainerId)."
                }
            }
            else
            {
                return $null
            }
        }
        catch [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpResponseException]
        {
            Write-Error -Message "HTTP error when setting GDAP access assignments, status code $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #New-GDAPAccessAssignment


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

Function New-GDAPRelationship
{
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Generated")]
    [OutputType([string])]
    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 of Entra ID roles to be used in the GDAP Relationship")]
        [ValidateScript( { $_ | ForEach-Object { Test-Guid -InputObject $_ } })]
        [string[]]$RoleDefinitionId,

        [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
    {
        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"

            # Get existing GDAP relationships to validate used displayName is unique
            $ExistingGDAPRelationshipNames = (Get-ExistingGDAPRelationship -All -GraphBaseURL $GraphBaseURL).displayName

            # If used, normalize the Relationship Prefix to match and fit
            if ([string]::IsNullOrEmpty($RelationshipPrefix) -and [string]::IsNullOrEmpty($GDAPRelationshipName))
            {
                [string]$RelationshipPrefix = Get-TenantDisplayNamebyId -TenantID ((Get-MgContext).TenantId) -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 ($ClientTenantID)
                {
                    "{0}_{1}_{2}" -f $RelationshipPrefix, $(Get-Date -Format yyyy), $ClientTenantID
                }
                else
                {
                    "{0}_{1}_{2}" -f $RelationshipPrefix, $(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 ($RelationshipDisplayName -in $ExistingGDAPRelationshipNames)
            {
                $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 ($RelationshipDisplayName -notin $ExistingGDAPRelationshipNames)
            }
            Write-Verbose -Message "GDAP Relationship Dispay Name: $($RelationshipDisplayName)"

            # Build the Graph API Message Body with available variables
            $DelegatedAdminRelationshipBody = [PSCustomObject]@{
                displayName   = $RelationshipDisplayName
                duration      = "P$($RelationshipExpirationInDays.ToString())D"
                accessDetails = @{
                    unifiedRoles = @(
                        $RoleDefinitionId | 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 5) -OutputType PSObject'"
            Write-Debug -Message "Graph Request Body value for new Relationship: `n$($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 5)"
            if ($PSCmdlet.ShouldProcess(("Request body: `n$($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 5)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL")))
            {
                $CreateDelegatedAdminRelationships = Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL -Body ($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 5) -OutputType PSObject
                Write-Debug -Message "Result:`n $($CreateDelegatedAdminRelationships | ConvertTo-Json -Depth 5)"
                # Validate the relationship request ID is valid before returning
                if (-not ([string]::IsNullOrEmpty($CreateDelegatedAdminRelationships.id)))
                {
                    return $CreateDelegatedAdminRelationships
                }
                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 $($Error[0].Exception.Response.StatusCode.value__),`n$($Error[0].Exception.Response.StatusDescription.value__)"
        }
        catch
        {
            Write-Error -Message $_.Exception.Message
        }
    }
} #New-GDAPRelationship


<#
.EXTERNALHELP GDAPRelationships-help.xml
#>

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

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

        Write-Information "Attempting to remove existing GDAP relationship $($GDAPRelationshipObject.id)"
        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)
            {
                "Lookup"
                {
                    $GDAPRelationshipObject = Get-ExistingGDAPRelationship -GDAPRelationshipID $GDAPRelationshipID -GraphBaseURL $GraphBaseURL
                }
            }

            # 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")) } -OutputType PSObject'"
                if ($PSCmdlet.ShouldProcess(("Request body: `n$($DelegatedAdminRelationshipBody | ConvertTo-Json -Depth 5)"), ("Invoke-MgGraphRequest -Method POST -Uri $DelegatedAdminRelationshipURL")))
                {
                    # 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 5)"

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


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 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)))
        {
            return $CompletionCommand
        }
        else
        {
            throw "Unable to create completion command."
        }
    }
} #Get-CompletionCommand

Function Get-ExistingCustomer
{
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory)]
        [string]$GraphBaseURL
    )
    begin
    {
        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
    )

    $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
    )
    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-TenantDisplayNamebyId
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory)]
        [ValidateScript( { Test-Guid -InputObject $_ })]
        [string]$TenantID,

        [Parameter(Mandatory)]
        [ValidateScript( { Test-Url -Url $_ })]
        [string]$GraphBaseURL
    )
    begin
    {
        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)))
            {
                return $TenantInformation.displayName
            }
            else
            {
                return $null
            }
        }
        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
    {
        $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
    )
    process
    {
        return [guid]::TryParse($InputObject, $([ref][guid]::Empty))
    }
} #Test-Guid

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

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