IdentityCommand.UAP.psm1

function ConvertTo-UAPFilterString {
    <#
    .SYNOPSIS
    Assembles an Access Control Policies filter expression.
 
    .DESCRIPTION
    Formats filter criteria into the expression syntax the Access Control Policies service accepts.
    Values for the same field are combined with `or` inside their own parentheses, and the resulting
    groups are combined with `and`:
 
        (((policyTags eq 'a') or (policyTags eq 'b')) and (targetCategory eq 'VM'))
 
    Every field compares with `eq`, apart from `identities`, which supports only `contains`.
 
    Values are single quoted, as the service documents them. The expression is not url encoded -
    Add-QueryString encodes the query string as a whole.
 
    .PARAMETER Filter
    A hashtable of field names to the values to match. A field with several values matches any of
    them. Supported fields are policyTags, identities, targetCategory, status and locationType.
 
    .EXAMPLE
    ConvertTo-UAPFilterString -Filter @{ targetCategory = 'VM' }
 
    Outputs: (targetCategory eq 'VM')
 
    .EXAMPLE
    ConvertTo-UAPFilterString -Filter ([ordered]@{ policyTags = @('a', 'b'); targetCategory = 'VM' })
 
    Outputs: (((policyTags eq 'a') or (policyTags eq 'b')) and (targetCategory eq 'VM'))
 
    .OUTPUTS
    String
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0
        )]
        [ValidateNotNull()]
        [System.Collections.IDictionary]$Filter
    )

    $SupportedField = @('policyTags', 'identities', 'targetCategory', 'status', 'locationType')

    #Every field compares with eq; identities is documented as supporting only contains
    $FieldOperator = @{ identities = 'contains' }

    $Groups = [System.Collections.Generic.List[string]]::new()

    foreach ($Field in $Filter.Keys) {

        if ($Field -notin $SupportedField) {
            throw "'$Field' is not a filterable field of the Access Control Policies service. Valid fields are: $($SupportedField -join ', ')"
        }

        $Values = @($Filter[$Field]) | Where-Object { -not [string]::IsNullOrEmpty("$_") }

        if ($Values.Count -eq 0) {
            continue
        }

        $Operator = if ($FieldOperator.ContainsKey($Field)) { $FieldOperator[$Field] } else { 'eq' }

        $Clauses = @($Values | ForEach-Object {
                "($(ConvertTo-FilterClause -Field $Field -Operator $Operator -Value $_ -QuoteValue -QuoteCharacter "'"))"
            })

        $Groups.Add($(if ($Clauses.Count -gt 1) { "($($Clauses -join ' or '))" } else { $Clauses[0] }))

    }

    if ($Groups.Count -eq 0) {
        return
    }

    if ($Groups.Count -eq 1) {
        return $Groups[0]
    }

    "($($Groups -join ' and '))"

}

function Remove-UAPReadOnlyProperty {
    <#
    .SYNOPSIS
    Strips the read-only properties from a retrieved access policy.
 
    .DESCRIPTION
    A policy retrieved with GET carries properties the service populates and marks read-only - who
    created it, the display names resolved for each target, the status code behind its status, and the
    invalid resources behind an error. Set-UAPPolicy re-sends the retrieved policy as the fallback for
    whatever the caller did not supply, so those properties are removed first rather than echoed back.
 
    The policy's status is kept, since a policy is suspended and reactivated through it; the read-only
    detail alongside it is not.
 
    .PARAMETER Policy
    The policy as returned by Get-UAPPolicy.
 
    .EXAMPLE
    $Existing = Get-UAPPolicy -policyId $policyId | Remove-UAPReadOnlyProperty
 
    .OUTPUTS
    The policy, without its read-only properties.
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function returns a filtered copy of its input and does not change state')]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipeline = $true
        )]
        [psobject]$Policy
    )

    begin {

        #Read-only on the policy itself. delegationClassification is marked read-only by the schema,
        #though the service's own edit examples send it back.
        $PolicyProperty = @('delegationClassification', 'invalidResources')

        #Read-only on the metadata. The examples name the update stamp inconsistently, so both spellings go.
        $MetadataProperty = @('createdBy', 'updatedBy', 'updatedOn')

        #Display names and types the service resolves for each target from the ids given
        $TargetProperty = @(
            'roleName'
            'roleType'
            'rolePackage'
            'workspaceName'
            'clusterName'
            'namespaceName'
            'groupName'
            'groupType'
            'directoryName'
            'description'
        )

        $Strip = {
            param($Object, $Names)

            if ($null -eq $Object) { return $null }

            $Kept = [ordered]@{ }

            foreach ($Property in $Object.PSObject.Properties) {
                if ($Property.Name -notin $Names) {
                    $Kept[$Property.Name] = $Property.Value
                }
            }

            [pscustomobject]$Kept

        }

    }#begin

    process {

        $Clean = & $Strip $Policy $PolicyProperty

        if ($null -ne $Clean.metadata) {

            $Metadata = & $Strip $Clean.metadata $MetadataProperty

            if ($null -ne $Metadata.status) {
                $Metadata.status = [pscustomobject]@{ status = $Metadata.status.status }
            }

            $Clean.metadata = $Metadata

        }

        if ($null -ne $Clean.targets) {

            #Cloud console policies nest their targets, clusters and groups hold a bare array, and
            #VM/DB targets are keyed objects whose entries are not per-target records
            if ($Clean.targets -is [System.Collections.IEnumerable] -and $Clean.targets -isnot [string]) {

                $Clean.targets = @($Clean.targets | ForEach-Object { & $Strip $_ $TargetProperty })

            } elseif ($null -ne $Clean.targets.targets) {

                $Clean.targets = [pscustomobject]@{
                    targets = @($Clean.targets.targets | ForEach-Object { & $Strip $_ $TargetProperty })
                }

            }

        }

        $Clean

    }#process

    end { }#end

}

function Resolve-UAPTargetShape {
    <#
    .SYNOPSIS
    Places policy targets in the shape their target category expects.
 
    .DESCRIPTION
    The service carries targets differently for each target category, so the builders emit plain
    target entries and this helper wraps them once, in one place:
 
        Cloud Console { "targets": [ ... ] }
        Clusters [ ... ]
        Groups [ ... ]
        DB { "FQDN/IP": { "instances": [ ... ] } }
        VM { "AWS": { ... } } - already shaped by New-UAPVirtualMachineTargetDefinition
 
    Targets which are already in their final shape are returned unaltered, so a hand-built payload
    passes through untouched.
 
    .PARAMETER targetCategory
    The policy's target category.
 
    .PARAMETER targets
    The targets to shape, as returned by the New-UAP*TargetDefinition builders.
 
    .EXAMPLE
    Resolve-UAPTargetShape -targetCategory 'Cloud Console' -targets $Targets
 
    .OUTPUTS
    The targets, wrapped as the target category requires.
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory = $true)]
        [string]$targetCategory,

        [parameter(Mandatory = $true)]
        [AllowNull()]
        $targets
    )

    switch ($targetCategory) {

        'Cloud Console' {

            #Every documented cloud console example nests the array under a targets property, which
            #the schema does not show. The examples are consistent across create, edit and get.
            if ($targets.PSObject.Properties.Name -contains 'targets') {
                $targets
            } else {
                [ordered]@{ targets = @($targets) }
            }

            break

        }

        'DB' {

            if ($targets.PSObject.Properties.Name -contains 'FQDN/IP') {
                $targets
            } else {
                [ordered]@{ 'FQDN/IP' = [ordered]@{ instances = @($targets) } }
            }

            break

        }

        { $_ -in 'Clusters', 'Groups' } {

            @($targets)
            break

        }

        default {

            #VM targets are keyed by location, and the builder emits that object whole
            $targets

        }

    }

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function Connect-UAPTenant {

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Subdomain')]
    param(

        #subdomain
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Subdomain')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'SubdomainCredential')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'SubdomainSAML')]
        [ValidateNotNullOrEmpty()]
        [Alias('subdomain')]
        [String]$tenant_subdomain,

        #tenant_url
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'URL')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'URLCredential')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'URLSAML')]
        [ValidateNotNullOrEmpty()]
        [Alias('uap_url')]
        [String]$tenant_url,

        #Credential used to authenticate to CyberArk Identity. Authentication is performed even if an active IdentityCommand session is found, replacing it
        [parameter(Mandatory = $true, ParameterSetName = 'SubdomainCredential')]
        [parameter(Mandatory = $true, ParameterSetName = 'URLCredential')]
        [ValidateNotNullOrEmpty()]
        [PSCredential]$Credential,

        #Authenticate as a service user via New-IDPlatformToken (OAuth client_credentials) instead of the interactive New-IDSession
        [parameter(ParameterSetName = 'SubdomainCredential')]
        [parameter(ParameterSetName = 'URLCredential')]
        [Switch]$PlatformToken,

        #SAML assertion used to authenticate to CyberArk Identity. Authentication is performed even if an active IdentityCommand session is found, replacing it
        [parameter(Mandatory = $true, ParameterSetName = 'SubdomainSAML')]
        [parameter(Mandatory = $true, ParameterSetName = 'URLSAML')]
        [ValidateNotNullOrEmpty()]
        [String]$SAMLResponse

    )

    begin {

        $IDSession = Get-IDSession
        $HaveSession = $null -ne $IDSession.tenant_url
        $AuthRequested = $PSBoundParameters.ContainsKey('Credential') -or $PSBoundParameters.ContainsKey('SAMLResponse')

        if ($HaveSession -and $AuthRequested) {
            Write-Verbose 'Authentication parameters were supplied; authenticating and replacing the existing IdentityCommand session'
        }

    }#begin

    process {

        $UsingSubdomain = $PSCmdlet.ParameterSetName -like 'Subdomain*'

        #Resolve service URLs from platform discovery when a subdomain was supplied, or when
        #authentication is required and the CyberArk Identity URL must be discovered.
        $ServiceUrl = $null

        if ($UsingSubdomain) {

            $ServiceUrl = Resolve-ServiceUrl -Service uap -Subdomain $tenant_subdomain
            $tenant_url = $ServiceUrl.ServiceUrl

        } else {

            #Ensure URL is in expected format - remove trailing slash if provided in Url
            $tenant_url = $tenant_url -replace '/$', ''

            if ($AuthRequested) {
                $ServiceUrl = Resolve-ServiceUrl -Service uap -Url $tenant_url
            }

        }

        if ($AuthRequested -or (-not $HaveSession)) {

            if (-not $AuthRequested) {
                throw 'Authenticate with New-IDSession or New-IDPlatformToken, or supply -Credential, and try again'
            }

            $IdentityUrl = $ServiceUrl.IdentityUrl

            if ($PSCmdlet.ShouldProcess($IdentityUrl, 'Authenticate to CyberArk Identity')) {

                if ($PSCmdlet.ParameterSetName -like '*SAML') {
                    $null = New-IDSession -tenant_url $IdentityUrl -SAMLResponse $SAMLResponse
                } elseif ($PlatformToken) {
                    $null = New-IDPlatformToken -tenant_url $IdentityUrl -Credential $Credential
                } else {
                    $null = New-IDSession -tenant_url $IdentityUrl -Credential $Credential
                }

                $IDSession = Get-IDSession

            }

        }

        #Make the CyberArk Identity Session available in the IdentityCommand.UAP scope
        foreach ($key in $IDSession.keys) {
            if ($null -ne $IDSession[$key]) {
                $ISPSSSession[$key] = $IDSession[$key]
            }
        }

        #Set the Access Requests URL in the session data
        $ISPSSSession.tenant_url = $tenant_url

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function Get-UAPModuleData {

    [CmdletBinding()]
    param()

    begin { }#begin

    process {

        #Calculate the time elapsed since the start of the session and include in return data
        if ($null -ne $ISPSSSession.StartTime) {
            $ISPSSSession.ElapsedTime = '{0:HH:mm:ss}' -f ([datetime]$($(Get-Date) - $($ISPSSSession.StartTime)).Ticks)
        } else { $ISPSSSession.ElapsedTime = $null }

        #Deep Copy the $ISPSSSession session object and return as IdCmd Session type.
        Get-SessionClone -InputObject $ISPSSSession | Add-CustomType -Type IdCmd.Session

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function Get-UAPPolicy {
    [CmdletBinding(DefaultParameterSetName = 'byFilterCriteria')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byId'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$policyId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byQuery'
        )]
        [ValidateNotNullOrEmpty()]
        [String]$filter,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateNotNullOrEmpty()]
        [String[]]$policyTags,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateNotNullOrEmpty()]
        [String[]]$identities,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateSet('Cloud Console', 'VM', 'DB', 'Clusters', 'Groups')]
        [String[]]$targetCategory,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateSet('Active', 'Suspended', 'Expired', 'Validating', 'Error', 'Warning')]
        [String[]]$status,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateSet('AWS', 'Azure', 'GCP', 'FQDN/IP')]
        [String[]]$locationType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byQuery'
        )]
        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateRange(0, 50)]
        [int]$limit,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byQuery'
        )]
        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateSet('createdBy')]
        [String]$sort,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byQuery'
        )]
        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateLength(1, 200)]
        [String]$q
    )

    begin {

        $FilterField = @('policyTags', 'identities', 'targetCategory', 'status', 'locationType')

    }#begin

    process {

        if ($PSCmdlet.ParameterSetName -eq 'byId') {

            $URI = "$($ISPSSSession.tenant_url)/api/policies/$policyId"

            #Send Request
            Invoke-IDRestMethod -Uri $URI -Method GET

        } else {

            $URI = "$($ISPSSSession.tenant_url)/api/policies"

            $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove $FilterField

            if ($PSCmdlet.ParameterSetName -eq 'byFilterCriteria') {

                $Criteria = [ordered]@{ }

                foreach ($Field in $FilterField) {
                    if ($PSBoundParameters.ContainsKey($Field)) {
                        $Criteria[$Field] = $PSBoundParameters[$Field]
                    }
                }

                if ($Criteria.Count -gt 0) {
                    $boundParameters['filter'] = ConvertTo-UAPFilterString -Filter $Criteria
                }

            }

            $URI = Add-QueryString -URI $URI -Parameter $boundParameters

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method GET

            if ($null -ne $result) {

                Get-PagedResult -InitialResult $result -URI $URI -Style Cursor -ResultProperty 'results' -CursorRequestKey 'nextToken' -CursorResponseKey 'nextToken'

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPCloudConsoleTargetDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding()]
    [OutputType('IdCmd.UAP.Definition.CloudConsoleTarget')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$roleId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$workspaceId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$orgId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$workspaceType,

        [parameter(Mandatory = $false)]
        [psobject[]]$TargetDefinition
    )

    begin { }#begin

    process {

        $Target = $PSBoundParameters | Get-Parameter -ParametersToRemove TargetDefinition

        $Output = @($TargetDefinition) + @([pscustomobject]$Target) | Where-Object { $null -ne $_ }

        $Output | Add-CustomType -Type 'IdCmd.UAP.Definition.CloudConsoleTarget'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPClusterTargetDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding()]
    [OutputType('IdCmd.UAP.Definition.ClusterTarget')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$roleId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$workspaceId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$clusterId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('cluster', 'namespace')]
        [String]$scope,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$namespaceId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$orgId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$workspaceType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$region,

        [parameter(Mandatory = $false)]
        [psobject[]]$TargetDefinition
    )

    begin { }#begin

    process {

        if (($scope -eq 'namespace') -and (-not $PSBoundParameters.ContainsKey('namespaceId'))) {
            throw 'namespaceId is required when scope is namespace'
        }

        $Target = $PSBoundParameters | Get-Parameter -ParametersToRemove TargetDefinition

        $Output = @($TargetDefinition) + @([pscustomobject]$Target) | Where-Object { $null -ne $_ }

        $Output | Add-CustomType -Type 'IdCmd.UAP.Definition.ClusterTarget'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPConditionDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding()]
    [OutputType('IdCmd.UAP.Definition.Conditions')]
    param(
        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateRange(0, 6)]
        [int[]]$daysOfTheWeek,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$fromHour,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$toHour,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateRange(1, 24)]
        [int]$maxSessionDuration,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateRange(1, 120)]
        [int]$idleTime,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [switch]$accessApprovalRequired,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateCount(1, 5)]
        [psobject[]]$approvers
    )

    begin { }#begin

    process {

        $Conditions = [ordered]@{ }

        if ($PSBoundParameters.ContainsKey('daysOfTheWeek') -or $PSBoundParameters.ContainsKey('fromHour') -or $PSBoundParameters.ContainsKey('toHour')) {

            if ($accessApprovalRequired) {
                throw 'Omit the access window when access approval is required - the service rejects a payload carrying both'
            }

            $AccessWindow = [ordered]@{ }

            if ($PSBoundParameters.ContainsKey('daysOfTheWeek')) { $AccessWindow['daysOfTheWeek'] = @($daysOfTheWeek) }
            if ($PSBoundParameters.ContainsKey('fromHour')) { $AccessWindow['fromHour'] = $fromHour }
            if ($PSBoundParameters.ContainsKey('toHour')) { $AccessWindow['toHour'] = $toHour }

            $Conditions['accessWindow'] = $AccessWindow

        }

        if ($PSBoundParameters.ContainsKey('maxSessionDuration')) { $Conditions['maxSessionDuration'] = $maxSessionDuration }
        if ($PSBoundParameters.ContainsKey('idleTime')) { $Conditions['idleTime'] = $idleTime }

        if ($PSBoundParameters.ContainsKey('accessApprovalRequired')) {

            $AccessApproval = [ordered]@{ required = $accessApprovalRequired.IsPresent }

            if ($PSBoundParameters.ContainsKey('approvers')) { $AccessApproval['approvers'] = @($approvers) }

            $Conditions['accessApproval'] = $AccessApproval

        }

        [pscustomobject]$Conditions | Add-CustomType -Type 'IdCmd.UAP.Definition.Conditions'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPDatabaseTargetDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding()]
    [OutputType('IdCmd.UAP.Definition.DatabaseTarget')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 256)]
        [String]$instanceName,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 32)]
        [String]$instanceType,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 36)]
        [String]$instanceId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('ldap_auth', 'db_auth', 'oracle_auth', 'mongo_auth', 'sqlserver_auth', 'rds_iam_user_auth')]
        [String]$authenticationMethod,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [hashtable]$profile,

        [parameter(Mandatory = $false)]
        [psobject[]]$TargetDefinition
    )

    begin { }#begin

    process {

        $Target = $PSBoundParameters | Get-Parameter -ParametersToRemove TargetDefinition

        $Output = @($TargetDefinition) + @([pscustomobject]$Target) | Where-Object { $null -ne $_ }

        $Output | Add-CustomType -Type 'IdCmd.UAP.Definition.DatabaseTarget'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPGroupTargetDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding()]
    [OutputType('IdCmd.UAP.Definition.GroupTarget')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$groupId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$directoryId,

        [parameter(Mandatory = $false)]
        [psobject[]]$TargetDefinition
    )

    begin { }#begin

    process {

        $Target = $PSBoundParameters | Get-Parameter -ParametersToRemove TargetDefinition

        $Output = @($TargetDefinition) + @([pscustomobject]$Target) | Where-Object { $null -ne $_ }

        $Output | Add-CustomType -Type 'IdCmd.UAP.Definition.GroupTarget'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 200)]
        [String]$name,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 200)]
        [String]$description,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('Cloud Console', 'VM', 'DB', 'Clusters', 'Groups')]
        [String]$targetCategory,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'Azure', 'GCP', 'FQDN/IP')]
        [String]$locationType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('Recurring', 'OnDemand')]
        [String]$policyType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateCount(1, 20)]
        [String[]]$policyTags,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 50)]
        [String]$timeZone,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$fromTime,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$toTime,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('Active', 'Suspended')]
        [String]$status,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject[]]$principals,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject]$conditions,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject]$targets,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject]$behavior,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('direct', 'proxy')]
        [String]$connectionMethod
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/policies"

        $Metadata = [ordered]@{ name = $name }

        if ($PSBoundParameters.ContainsKey('description')) { $Metadata['description'] = $description }
        if ($PSBoundParameters.ContainsKey('status')) { $Metadata['status'] = @{ status = $status } }
        if ($PSBoundParameters.ContainsKey('policyTags')) { $Metadata['policyTags'] = @($policyTags) }

        if ($PSBoundParameters.ContainsKey('fromTime') -or $PSBoundParameters.ContainsKey('toTime')) {

            $TimeFrame = [ordered]@{ }

            #The service documents the timeframe as yyyy-MM-ddTHH:mm:ss
            if ($PSBoundParameters.ContainsKey('fromTime')) { $TimeFrame['fromTime'] = $fromTime.ToString('yyyy-MM-ddTHH:mm:ss') }
            if ($PSBoundParameters.ContainsKey('toTime')) { $TimeFrame['toTime'] = $toTime.ToString('yyyy-MM-ddTHH:mm:ss') }

            $Metadata['timeFrame'] = $TimeFrame

        }

        if ($PSBoundParameters.ContainsKey('timeZone')) { $Metadata['timeZone'] = $timeZone }

        $Entitlement = [ordered]@{ targetCategory = $targetCategory; locationType = $locationType }
        if ($PSBoundParameters.ContainsKey('policyType')) { $Entitlement['policyType'] = $policyType }

        $Metadata['policyEntitlement'] = $Entitlement

        $Policy = [ordered]@{
            metadata   = $Metadata
            principals = @($principals)
            targets    = $(Resolve-UAPTargetShape -targetCategory $targetCategory -targets $targets)
        }

        if ($PSBoundParameters.ContainsKey('conditions')) { $Policy['conditions'] = $conditions }
        if ($PSBoundParameters.ContainsKey('behavior')) { $Policy['behavior'] = $behavior }
        if ($PSBoundParameters.ContainsKey('connectionMethod')) { $Policy['connectionMethod'] = $connectionMethod }

        $body = $Policy | ConvertTo-Json -Depth 12

        if ($PSCmdlet.ShouldProcess($name, 'Create Access Policy')) {

            #Send Request
            Invoke-IDRestMethod -Uri $URI -Method POST -Body $body

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPPrincipalDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding()]
    [OutputType('IdCmd.UAP.Definition.Principal')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 40)]
        [String]$id,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 512)]
        [String]$name,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('USER', 'ROLE', 'GROUP')]
        [String]$type,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 256)]
        [String]$sourceDirectoryName,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$sourceDirectoryId,

        [parameter(Mandatory = $false)]
        [psobject[]]$PrincipalDefinition
    )

    begin { }#begin

    process {

        if (($type -ne 'ROLE') -and (-not ($PSBoundParameters.ContainsKey('sourceDirectoryName') -and $PSBoundParameters.ContainsKey('sourceDirectoryId')))) {
            throw 'sourceDirectoryName and sourceDirectoryId are required unless the principal type is ROLE'
        }

        $Principal = $PSBoundParameters | Get-Parameter -ParametersToRemove PrincipalDefinition

        $Output = @($PrincipalDefinition) + @([pscustomobject]$Principal) | Where-Object { $null -ne $_ }

        $Output | Add-CustomType -Type 'IdCmd.UAP.Definition.Principal'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPVirtualMachineBehaviorDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding()]
    [OutputType('IdCmd.UAP.Definition.VirtualMachineBehavior')]
    param(
        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String]$sshUsername,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [String[]]$rdpAssignGroups,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [switch]$enableEphemeralUserReconnect
    )

    begin { }#begin

    process {

        $ConnectAs = [ordered]@{ }

        if ($PSBoundParameters.ContainsKey('sshUsername')) {
            $ConnectAs['ssh'] = @{ username = $sshUsername }
        }

        if ($PSBoundParameters.ContainsKey('rdpAssignGroups') -or $PSBoundParameters.ContainsKey('enableEphemeralUserReconnect')) {

            $EphemeralUser = [ordered]@{ }

            if ($PSBoundParameters.ContainsKey('rdpAssignGroups')) { $EphemeralUser['assignGroups'] = @($rdpAssignGroups) }

            $EphemeralUser['enableEphemeralUserReconnect'] = $enableEphemeralUserReconnect.IsPresent

            $ConnectAs['rdp'] = @{ localEphemeralUser = $EphemeralUser }

        }

        [pscustomobject]@{ connectAs = $ConnectAs } | Add-CustomType -Type 'IdCmd.UAP.Definition.VirtualMachineBehavior'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function New-UAPVirtualMachineTargetDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function builds a definition object and does not change state')]
    [CmdletBinding(DefaultParameterSetName = 'AWS')]
    [OutputType('IdCmd.UAP.Definition.VirtualMachineTarget')]
    param(
        [parameter(Mandatory = $true, ParameterSetName = 'AWS')]
        [switch]$AWS,

        [parameter(Mandatory = $true, ParameterSetName = 'Azure')]
        [switch]$Azure,

        [parameter(Mandatory = $true, ParameterSetName = 'GCP')]
        [switch]$GCP,

        [parameter(Mandatory = $true, ParameterSetName = 'FQDNIP')]
        [switch]$FQDNIP,

        [parameter(Mandatory = $false, ParameterSetName = 'AWS')]
        [parameter(Mandatory = $false, ParameterSetName = 'Azure')]
        [parameter(Mandatory = $false, ParameterSetName = 'GCP')]
        [String[]]$regions,

        [parameter(Mandatory = $false, ParameterSetName = 'AWS')]
        [parameter(Mandatory = $false, ParameterSetName = 'Azure')]
        [hashtable[]]$tags,

        [parameter(Mandatory = $false, ParameterSetName = 'GCP')]
        [hashtable[]]$labels,

        [parameter(Mandatory = $false, ParameterSetName = 'AWS')]
        [parameter(Mandatory = $false, ParameterSetName = 'GCP')]
        [String[]]$vpcIds,

        [parameter(Mandatory = $false, ParameterSetName = 'AWS')]
        [String[]]$accountIds,

        [parameter(Mandatory = $false, ParameterSetName = 'Azure')]
        [String[]]$vnetIds,

        [parameter(Mandatory = $false, ParameterSetName = 'Azure')]
        [String[]]$resourceGroups,

        [parameter(Mandatory = $false, ParameterSetName = 'Azure')]
        [String[]]$subscriptions,

        [parameter(Mandatory = $false, ParameterSetName = 'GCP')]
        [String[]]$projects,

        [parameter(Mandatory = $false, ParameterSetName = 'FQDNIP')]
        [hashtable[]]$fqdnRules,

        [parameter(Mandatory = $false, ParameterSetName = 'FQDNIP')]
        [hashtable[]]$ipRules,

        [parameter(Mandatory = $false)]
        [psobject]$TargetDefinition
    )

    begin { }#begin

    process {

        $Location = if ($PSCmdlet.ParameterSetName -eq 'FQDNIP') { 'FQDN/IP' } else { $PSCmdlet.ParameterSetName }

        $Target = $PSBoundParameters | Get-Parameter -ParametersToRemove AWS, Azure, GCP, FQDNIP, TargetDefinition

        #Each location is a property of a single targets object, so an existing definition is
        #carried forward rather than replaced
        $Output = [ordered]@{ }

        if ($PSBoundParameters.ContainsKey('TargetDefinition')) {
            foreach ($Property in $TargetDefinition.PSObject.Properties) {
                $Output[$Property.Name] = $Property.Value
            }
        }

        $Output[$Location] = $Target

        [pscustomobject]$Output | Add-CustomType -Type 'IdCmd.UAP.Definition.VirtualMachineTarget'

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function Remove-UAPPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$policyId
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/policies/$policyId"

        if ($PSCmdlet.ShouldProcess($policyId, 'Delete Access Policy')) {

            #Send Request
            Invoke-IDRestMethod -Uri $URI -Method DELETE

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function Set-UAPPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$policyId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 200)]
        [String]$name,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 200)]
        [String]$description,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('Cloud Console', 'VM', 'DB', 'Clusters', 'Groups')]
        [String]$targetCategory,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'Azure', 'GCP', 'FQDN/IP')]
        [String]$locationType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('Recurring', 'OnDemand')]
        [String]$policyType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateCount(1, 20)]
        [String[]]$policyTags,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 50)]
        [String]$timeZone,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$fromTime,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$toTime,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('Active', 'Suspended')]
        [String]$status,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject[]]$principals,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject]$conditions,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject]$targets,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [psobject]$behavior,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('direct', 'proxy')]
        [String]$connectionMethod
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/policies/$policyId"

        #The service replaces the policy with the payload sent, so the current policy provides
        #whatever the caller did not supply. Its read-only properties are dropped rather than echoed.
        $Existing = Get-UAPPolicy -policyId $policyId | Remove-UAPReadOnlyProperty

        if ($null -eq $Existing) {
            throw "Policy '$policyId' was not found, so there is no policy to update"
        }

        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove policyId

        #Metadata: the flat properties merge directly against the current policy's metadata
        $Metadata = Merge-Parameter -Template ([ordered]@{
                policyId   = $policyId
                name       = $null
                description = $null
                policyTags = $null
                timeZone   = $null
            }) -BoundParameter $boundParameters -Fallback $Existing.metadata

        $Metadata['policyId'] = $policyId

        #Status travels as an object, and only its status value is writable
        $CurrentStatus = if ($PSBoundParameters.ContainsKey('status')) { $status } else { $Existing.metadata.status.status }
        if (-not [string]::IsNullOrEmpty($CurrentStatus)) {
            $Metadata['status'] = [ordered]@{ status = $CurrentStatus }
        }

        #The timeframe is a nested object, so each end falls back to the current policy's own value
        $TimeFrame = [ordered]@{ }

        foreach ($Bound in @{ Name = 'fromTime'; Value = $fromTime }, @{ Name = 'toTime'; Value = $toTime }) {

            if ($PSBoundParameters.ContainsKey($Bound.Name)) {
                #The service documents the timeframe as yyyy-MM-ddTHH:mm:ss
                $TimeFrame[$Bound.Name] = $Bound.Value.ToString('yyyy-MM-ddTHH:mm:ss')
            } elseif ($null -ne $Existing.metadata.timeFrame.$($Bound.Name)) {
                $TimeFrame[$Bound.Name] = $Existing.metadata.timeFrame.$($Bound.Name)
            }

        }

        if ($TimeFrame.Count -gt 0) {
            $Metadata['timeFrame'] = $TimeFrame
        }

        $Metadata['policyEntitlement'] = Merge-Parameter -Template ([ordered]@{
                targetCategory = $null
                locationType   = $null
                policyType     = $null
            }) -BoundParameter $boundParameters -Fallback $Existing.metadata.policyEntitlement

        #Top level properties merge against the policy itself
        $Policy = Merge-Parameter -Template ([ordered]@{
                principals       = $null
                conditions       = $null
                behavior         = $null
                connectionMethod = $null
            }) -BoundParameter $boundParameters -Fallback $Existing

        $Policy.Insert(0, 'metadata', $Metadata)

        #Targets retrieved from the service are already in their final shape; targets supplied by the
        #caller come from a builder and are shaped here
        $Category = $Metadata['policyEntitlement']['targetCategory']
        $PolicyTargets = if ($PSBoundParameters.ContainsKey('targets')) { $targets } else { $Existing.targets }

        $Policy['targets'] = Resolve-UAPTargetShape -targetCategory $Category -targets $PolicyTargets

        #Drop anything neither supplied nor held by the current policy
        foreach ($Key in @($Policy.Keys)) {
            if ($null -eq $Policy[$Key]) {
                $Policy.Remove($Key)
            }
        }

        foreach ($Key in @($Metadata.Keys)) {
            if ($null -eq $Metadata[$Key]) {
                $Metadata.Remove($Key)
            }
        }

        $body = $Policy | ConvertTo-Json -Depth 12

        if ($PSCmdlet.ShouldProcess($policyId, 'Update Access Policy')) {

            #Send Request
            Invoke-IDRestMethod -Uri $URI -Method PUT -Body $body

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.UAP-help.xml
function Test-UAPPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$policyId
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/policies/$policyId/validate"

        if ($PSCmdlet.ShouldProcess($policyId, 'Validate Access Policy')) {

            #Send Request
            Invoke-IDRestMethod -Uri $URI -Method POST

        }

    }#process

    end { }#end

}

#Copy IdentityCommand's private helpers into this module: this module's functions call them, and
#the argument completer registrations below do so at import time.
#Each copy is created from the function definition, so it runs in this module's scope and uses this
#module's $ISPSSSession, whether IdentityCommand loaded from source or from its combined psm1.
#Resolve a single IdentityCommand module: with more than one version loaded, Get-Module returns
#an array.
$Module = Get-Module -Name IdentityCommand | Sort-Object Version -Descending | Select-Object -First 1

if ($null -eq $Module) {
    throw 'The IdentityCommand module is not loaded. Import IdentityCommand and try again.'
}

& $Module { Get-ChildItem -Path Function: } |

    Where-Object { $_.ModuleName -eq $Module.Name -and -not $Module.ExportedFunctions.ContainsKey($_.Name) } |

    ForEach-Object {

        . ([scriptblock]::Create("function $($_.Name) {$($_.Definition)}"))

    }

#region Registration

#Policies are identified by an opaque id, so label the completion with the policy name.
Register-ArgumentCompleter -ParameterName 'policyId' -ScriptBlock (
    Get-ArgumentCompleter -RetrievalCommand 'Get-UAPPolicy' -ValueProperty 'policyId', 'id' -LabelProperty 'name'
) -CommandName @(
    'Get-UAPPolicy'
    'Remove-UAPPolicy'
    'Set-UAPPolicy'
    'Test-UAPPolicy'
)

#Tags are free text, so offer those already in use on the tenant rather than a fixed set.
Register-ArgumentCompleter -ParameterName 'policyTags' -ScriptBlock (
    Get-ArgumentCompleter -RetrievalCommand 'Get-UAPPolicy' -ValueProperty 'policyTags'
) -CommandName @(
    'Get-UAPPolicy'
    'New-UAPPolicy'
    'Set-UAPPolicy'
)

#endregion Registration

# Script scope session object for session data
$ISPSSSession = [ordered]@{
    tenant_url         = $null
    User               = $null
    TenantId           = $null
    SessionId          = $null
    WebSession         = $null
    StartTime          = $null
    ElapsedTime        = $null
    LastCommand        = $null
    LastCommandTime    = $null
    LastCommandResults = $null
    LastError          = $null
    LastErrorTime      = $null
} | Add-CustomType -Type IdCmd.Session

New-Variable -Name ISPSSSession -Value $ISPSSSession -Scope Script -Force