IdentityCommand.AccessRequest.psm1

function ConvertTo-ARFilterString {
    <#
    .SYNOPSIS
    Assembles an Access Requests filter expression.
 
    .DESCRIPTION
    Formats filter clauses into the expression syntax accepted by the Access Requests service, which
    requires every expression to be complete within parentheses. Each clause is parenthesised, and
    clauses are combined pairwise from the left, each combination parenthesised in turn:
 
        1 clause (a eq 1)
        2 clauses ((a eq 1) and (b eq 2))
        3 clauses (((a eq 1) and (b eq 2)) and (c eq 3))
 
    Values are formatted by ConvertTo-FilterClause and quoted with single quotes, as the service
    documentation renders them. The expression is not url encoded - Add-QueryString encodes the
    query string as a whole.
 
    .PARAMETER Filter
    The clauses to assemble, each a hashtable with a Field key, an Operator key, and - for operators
    which take one - a Value key. Operators taking no value (is_null, is_not_null, is_true,
    is_false) are given without a Value.
 
    .PARAMETER LogicalOperator
    The operator to combine clauses with. Defaults to 'and'.
 
    .EXAMPLE
    ConvertTo-ARFilterString -Filter @{ Field = 'requestState'; Operator = 'eq'; Value = 'finished' }
 
    Outputs: (requestState eq finished)
 
    .EXAMPLE
    ConvertTo-ARFilterString -Filter @(
        @{ Field = 'requestState'; Operator = 'eq'; Value = 'finished' }
        @{ Field = 'priority'; Operator = 'gt'; Value = 5 }
        @{ Field = 'createdBy'; Operator = 'eq'; Value = 'John.Doe@cyberark.com' }
    )
 
    Outputs: (((requestState eq finished) and (priority gt 5)) and (createdBy eq 'John.Doe@cyberark.com'))
 
    .EXAMPLE
    ConvertTo-ARFilterString -Filter @{ Field = 'finalizationReason'; Operator = 'is_null' }
 
    Outputs: (finalizationReason is_null)
 
    .OUTPUTS
    String
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true
        )]
        [hashtable[]]$Filter,

        [parameter(Mandatory = $false)]
        [ValidateSet('and', 'or')]
        [string]$LogicalOperator = 'and'
    )

    begin {

        #Operators which compare against no value
        $ValuelessOperator = @('is_null', 'is_not_null', 'is_true', 'is_false')

        $ValidOperator = $ValuelessOperator + @(
            'eq', 'neq', 'contains', 'not_contains', 'sw', 'ew', 'gt', 'lt', 'ge', 'le', 'in'
        )

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

        #The service renders anything but a bare word quoted - an email address, a date, a timestamp
        $RequiresQuoting = { param($Item) ($Item -is [string]) -and ($Item -notmatch '^[A-Za-z0-9_]+$') }

    }

    process {

        foreach ($Clause in $Filter) {

            if (-not $Clause.ContainsKey('Field')) {
                throw 'Each filter clause requires a Field'
            }

            if (-not $Clause.ContainsKey('Operator')) {
                throw "Filter clause for field '$($Clause['Field'])' requires an Operator"
            }

            $Operator = $Clause['Operator']

            if ($Operator -notin $ValidOperator) {
                throw "'$Operator' is not a filter operator supported by the Access Requests service. Valid operators are: $($ValidOperator -join ', ')"
            }

            if ($Operator -in $ValuelessOperator) {

                $Clauses.Add($(ConvertTo-FilterClause -Field $Clause['Field'] -Operator $Operator))

            } elseif ($Operator -eq 'in') {

                #TODO: the service documents an 'in' operator but not how it delimits the list.
                #Verify against a tenant before relying on this rendering.
                $Values = @($Clause['Value']) | ForEach-Object {
                    ConvertTo-FilterClause -Value $_ -ValueOnly -QuoteCharacter "'" -QuoteValue:(& $RequiresQuoting $_)
                }

                $Clauses.Add("$($Clause['Field']) in ($($Values -join ', '))")

            } else {

                $Value = $Clause['Value']

                $Clauses.Add($(ConvertTo-FilterClause -Field $Clause['Field'] -Operator $Operator -Value $Value -QuoteCharacter "'" -QuoteValue:(& $RequiresQuoting $Value)))

            }

        }

    }

    end {

        if ($Clauses.Count -eq 0) {

            return

        }

        $Expression = "($($Clauses[0]))"

        for ($i = 1; $i -lt $Clauses.Count; $i++) {

            $Expression = "($Expression $LogicalOperator ($($Clauses[$i])))"

        }

        $Expression

    }

}

# .ExternalHelp IdentityCommand.AccessRequest-help.xml
function Approve-ARRequest {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$requestId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 4096)]
        [String]$finalizationReason
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/workflows/requests/$requestId/finalize"

        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove requestId
        $boundParameters['result'] = 'APPROVED'

        $body = $boundParameters | ConvertTo-Json -Depth 8

        if ($PSCmdlet.ShouldProcess($requestId, 'Approve Access Request')) {

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

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.AccessRequest-help.xml
function Connect-ARTenant {

    [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('ars_url','uar_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 ars -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 ars -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.AccessRequest 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.AccessRequest-help.xml
function Deny-ARRequest {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$requestId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 4096)]
        [String]$finalizationReason
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/workflows/requests/$requestId/finalize"

        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove requestId
        $boundParameters['result'] = 'REJECTED'

        $body = $boundParameters | ConvertTo-Json -Depth 8

        if ($PSCmdlet.ShouldProcess($requestId, 'Reject Access Request')) {

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

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.AccessRequest-help.xml
function Get-ARModuleData {

    [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.AccessRequest-help.xml
function Get-ARRequest {
    [CmdletBinding(DefaultParameterSetName = 'byQuery')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byId'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$requestId,

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

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateSet('STARTING', 'RUNNING', 'PENDING', 'FINISHED', 'EXPIRED')]
        [String]$requestState,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateSet('APPROVED', 'REJECTED', 'CANCELED', 'FAILED', 'UNKNOWN')]
        [String]$requestResult,

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

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

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [datetime]$createdAfter,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [datetime]$createdBefore,

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

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

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

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byQuery'
        )]
        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'byFilterCriteria'
        )]
        [ValidateSet('CREATOR', 'APPROVER')]
        [String]$requestRole,

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

    begin { }#begin

    process {

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

            $URI = "$($ISPSSSession.tenant_url)/api/workflows/requests/$requestId"

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

        } else {

            $URI = "$($ISPSSSession.tenant_url)/api/workflows/requests"

            $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove requestState, requestResult, createdBy, updatedBy, createdAfter, createdBefore

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

                $FilterClause = @()

                foreach ($Field in 'requestState', 'requestResult', 'createdBy', 'updatedBy') {
                    if ($PSBoundParameters.ContainsKey($Field)) {
                        $FilterClause += @{ Field = $Field; Operator = 'eq'; Value = $PSBoundParameters[$Field] }
                    }
                }

                #The service renders a filtered timestamp as 'yyyy-MM-dd HH:mm:ss'
                if ($PSBoundParameters.ContainsKey('createdAfter')) {
                    $FilterClause += @{ Field = 'createdAt'; Operator = 'ge'; Value = $createdAfter.ToString('yyyy-MM-dd HH:mm:ss') }
                }

                if ($PSBoundParameters.ContainsKey('createdBefore')) {
                    $FilterClause += @{ Field = 'createdAt'; Operator = 'le'; Value = $createdBefore.ToString('yyyy-MM-dd HH:mm:ss') }
                }

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

            }

            $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 Offset -ResultProperty 'items' -TotalResponseKey 'totalCount'

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.AccessRequest-help.xml
function Get-ARRequestForm {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('CLOUD_CONSOLE')]
        [String]$targetCategory,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('ON_DEMAND', 'DUAL_CONTROL')]
        [String]$requestType
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/workflows/request-forms"

        $URI = Add-QueryString -URI $URI -Parameter ($PSBoundParameters | Get-Parameter)

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

        if ($null -ne $result) {

            $result.requestForms

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.AccessRequest-help.xml
function New-ARRequest {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('CLOUD_CONSOLE')]
        [String]$targetCategory,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('ON_DEMAND', 'DUAL_CONTROL')]
        [String]$requestType,

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

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/workflows/requests"

        $body = $PSBoundParameters | Get-Parameter | ConvertTo-Json -Depth 8

        if ($PSCmdlet.ShouldProcess($targetCategory, 'Create Access Request')) {

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

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.AccessRequest-help.xml
function Stop-ARRequest {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('id')]
        [String]$requestId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 4096)]
        [String]$cancelReason
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/workflows/requests/$requestId/cancel"

        $body = $PSBoundParameters | Get-Parameter -ParametersToRemove requestId | ConvertTo-Json -Depth 8

        if ($PSCmdlet.ShouldProcess($requestId, 'Cancel Access Request')) {

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

        }

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

#Requests are identified by an opaque uuid, so label the completion with the target category and
#state rather than leaving the caller to recognise the id.
Register-ArgumentCompleter -ParameterName 'requestId' -ScriptBlock (
    Get-ArgumentCompleter -RetrievalCommand 'Get-ARRequest' -ValueProperty 'requestId' -LabelProperty 'requestState'
) -CommandName @(
    'Approve-ARRequest'
    'Deny-ARRequest'
    'Get-ARRequest'
    'Stop-ARRequest'
)

#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