DSCResources/MSFT_xPSSessionConfiguration/MSFT_xPSSessionConfiguration.psm1

# Localized messages
data LocalizedData
{
    # culture="en-US"
    ConvertFrom-StringData @'
CheckEndpointMessage = Checking if session configuration {0} exists ...
EndpointNameMessage = Session configuration {0} is {1}
 
CheckPropertyMessage = Checking if session configuration {0} is {1} ...
NotDesiredPropertyMessage = Session configuration {0} is NOT {1}, but {2}
DesiredPropertyMessage = Session configuration {0} is {1}
SetPropertyMessage = Session configuration {0} is now {1}
 
WhitespacedStringMessage = The session configuration {0} should not be white-spaced string
StartupPathNotFoundMessage = Startup path {0} not found
EmptyCredentialMessage = The value of RunAsCredential can not be an empty credential
WrongStartupScriptExtensionMessage = The startup script should have a 'ps1' extension, and not '{0}'
'@

}

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String]$Name
    )

    # Try getting the specified endpoint
    $endpoint = Get-PSSessionConfiguration -Name $Name -ErrorAction SilentlyContinue -Verbose:$false

    # If endpoint is null, it is absent
    if($endpoint -eq $null)
    {
        $ensure = 'Absent'
    }

    # If endpoint is present, check other properties
    else 
    {
        $ensure = 'Present'

        # If runAsUser is specified, return only the username in the credential property
        if($($endpoint.RunAsUser))
        {
            $convertToCimCredential = New-CimInstance -ClassName MSFT_Credential -Property @{Username = [String]$($endpoint.RunAsUser); Password = [String]$null} `
                                                        -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly
        }
        $accessMode = Get-EndpointAccessMode -Endpoint $endpoint
    }

    @{
        Name                   = $Name
        RunAsCredential        = [CimInstance]$convertToCimCredential
        SecurityDescriptorSDDL = $endpoint.Permission
        StartupScript          = $endpoint.StartupScript
        AccessMode             = $accessMode
        Ensure                 = $ensure
    }
}

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,

        [AllowEmptyString()]
        [String]$StartupScript,

        [PSCredential]$RunAsCredential,

        [String]$SecurityDescriptorSDDL,

        [ValidateSet('Local','Remote', 'Disabled')]
        [String]$AccessMode = 'Remote',

        [ValidateSet('Present','Absent')]
        [String]$Ensure = 'Present'
    )

    #Check if the session configuration exists
    $queryNameMessage = $($LocalizedData.CheckEndpointMessage) -f $Name
    Write-Verbose -Message $queryNameMessage
    
    # Try to get a named session configuration
    $endpoint = Get-PSSessionConfiguration -Name $Name -ErrorAction SilentlyContinue -Verbose:$false
    
    # If endpoint is present, set ensure correctly
    if($endpoint)
    {
        $namePresentMessage = $($LocalizedData.EndpointNameMessage) -f $Name,'present'
        Write-Verbose -Message $namePresentMessage

        # If the endpoint should be absent, delete the endpoint
        if($Ensure -eq 'Absent')
        {
            try
            {
                # Set the following preference so the functions inside Unregister-PSSessionConfig doesn't get these settings
                $oldDebugPrefernce = $DebugPreference; $oldVerbosePreference = $VerbosePreference
                $DebugPreference = $VerbosePreference = "SilentlyContinue"

                $null = Unregister-PSSessionConfiguration -Name $Name -Force -NoServiceRestart -ErrorAction Stop

                # Reset the following preference to older values
                $DebugPreference = $oldDebugPrefernce; $VerbosePreference = $oldVerbosePreference

                $namePresentMessage = $($LocalizedData.EndpointNameMessage) -f $Name,'absent'
                Write-Verbose -Message $namePresentMessage 

                $restartNeeded = $true
            }
            catch
            {
                New-TerminatingError -errorId 'UnregisterPSSessionConfigurationFailed' -errorMessage $_.Exception -errorCategory InvalidOperation
            }

        }

        # else validate endpoint properties and return the result
        else
        {
            # Remove Name and Ensure from the bound parameters for splatting
            if($PSBoundParameters.ContainsKey('Name')){$null = $PSBoundParameters.Remove('Name')}
            if($PSBoundParameters.ContainsKey('Ensure')){$null = $PSBoundParameters.Remove('Ensure')}

            [Hashtable]$parameters = (Validate-ResourceProperties -Endpoint $endpoint @PSBoundParameters -Apply)
            $null = $parameters.Add('Name',$Name)

            # If the $parameters contain more than 1 key, something needs to be changed
            if($parameters.count -gt 1)
            { 
                try
                {
                    $null = Set-PSSessionConfiguration @parameters -Force -NoServiceRestart -Verbose:$false
                    $restartNeeded = $true

                    # Write verbose message for all the properties, except Name, that are changing
                    Write-EndpointMessage -parameters $parameters -keysToSkip 'Name'
                }
                catch
                {
                    New-TerminatingError -errorId 'SetPSSessionConfigurationFailed' -errorMessage $_.Exception -errorCategory InvalidOperation
                }
            }
        }
    }
    else
    {
        # Named session configuration is absent
        $nameAbsentMessage = $($LocalizedData.EndpointNameMessage) -f $Name,'absent'
        Write-Verbose -Message $nameAbsentMessage

        # If the endpoint should have been present, create it
        if($Ensure -eq 'Present')
        {
            # Remove Ensure,Verbose,Debug from the bound parameters for splatting
            if($PSBoundParameters.ContainsKey('Ensure')){$null = $PSBoundParameters.Remove('Ensure')}
            if($PSBoundParameters.ContainsKey('Verbose')){$null = $PSBoundParameters.Remove('Verbose')}
            if($PSBoundParameters.ContainsKey('Debug')){$null = $PSBoundParameters.Remove('Debug')}

            # Register the endpoint with specified properties
            try
            {
                # Set the following preference so the functions inside Unregister-PSSessionConfig doesn't get these settings
                $oldDebugPrefernce = $DebugPreference; $oldVerbosePreference = $VerbosePreference
                $DebugPreference = $VerbosePreference = "SilentlyContinue"

                $null = Register-PSSessionConfiguration @PSBoundParameters -Force -NoServiceRestart
           
                # Reset the following preference to older values
                $DebugPreference = $oldDebugPrefernce; $VerbosePreference = $oldVerbosePreference

                # If access mode is specified, set it on the endpoint
                if($PSBoundParameters.ContainsKey('AccessMode') -and $AccessMode -ne 'Remote')
                {              
                        $null = Set-PSSessionConfiguration -Name $Name -AccessMode $AccessMode -Force -NoServiceRestart -Verbose:$false
                }

                $restartNeeded = $true

                # If the $parameters contain more than 1 key, something needs to be changed
                if($parameters.count -gt 1)
                { 
                    Write-EndpointMessage -parameters $parameters -keysToSkip 'Name'
                }

                $namePresentMessage = $($LocalizedData.EndpointNameMessage) -f $Name,'present'
                Write-Verbose -Message $namePresentMessage
            }
            catch
            {
                New-TerminatingError -errorId 'RegisterOrSetPSSessionConfigurationFailed' -errorMessage $_.Exception -errorCategory InvalidOperation
            }            
        }
    }

    # Any change to existing endpoint or creating new endpoint requires WinRM restart.
    # Since DSC(CIM) uses WSMan as well it will stop responding.
    # Hence telling the DSC Engine to restart the machine
    if($restartNeeded) {$global:DscMachineStatus = 1}
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,

        [AllowEmptyString()]
        [String]$StartupScript,

        [PSCredential]$RunAsCredential,

        [String]$SecurityDescriptorSDDL,

        [ValidateSet('Local','Remote', 'Disabled')]
        [String]$AccessMode = 'Remote',

        [ValidateSet('Present','Absent')]
        [String]$Ensure = 'Present'
    )

#region Input Validation
    # Check if the endpoint name is blank/whitespaced string
    if([String]::IsNullOrWhiteSpace($Name))
    {
        $whitespacedEndpointNameMessage = $($LocalizedData.WhitespacedStringMessage) -f 'name'
        New-TerminatingError -errorId 'BlankString' -errorMessage $whitespacedEndpointNameMessage -errorCategory SyntaxError
    }

    # Check for Startup script path and extension
    if($PSBoundParameters.ContainsKey('StartupScript'))
    {
        # Check if startup script path is valid
        if(!(Test-Path $StartupScript))
        {
            $startupPathAbsentMessage = $($LocalizedData.StartupPathNotFoundMessage) -f $StartupScript
            New-TerminatingError -errorId 'PathNotFound' -errorMessage $startupPathAbsentMessage -errorCategory ObjectNotFound
        }

        # Check the startup script extension
        $startupScriptFileExtension = $StartupScript.Split('.')[-1]
        if( $startupScriptFileExtension -ne 'ps1')
        {
            $wrongExtensionMessage = $($LocalizedData.WrongStartupScriptExtensionMessage) -f $startupScriptFileExtension
            New-TerminatingError -errorId 'WrongFileExtension' -errorMessage $wrongExtensionMessage -errorCategory InvalidData
        }
    }

    # Check if SecurityDescriptorSDDL is whitespaced
    if($PSBoundParameters.ContainsKey('SecurityDescriptorSDDL') -and [String]::IsNullOrWhiteSpace($SecurityDescriptorSDDL))
    {
        $whitespacedSDDLMessage = $($LocalizedData.WhitespacedStringMessage) -f 'securityDescriptorSddl'
        New-TerminatingError -errorId 'BlankString' -errorMessage $whitespacedSDDLMessage -errorCategory SyntaxError
    }

    # Check if the RunAsCredential is not empty
    if($PSBoundParameters.ContainsKey('RunAsCredential') -and ($RunAsCredential -eq [PSCredential]::Empty))
    {
        $emptyCredentialMessage = $($LocalizedData.EmptyCredentialMessage)
        New-TerminatingError -errorId 'EmptyCredential' -errorMessage $emptyCredentialMessage -errorCategory InvalidArgument
    }
#endregion

    #Check if the session configuration exists
    $queryNameMessage = $($LocalizedData.CheckEndpointMessage) -f $Name
    Write-Verbose -Message $queryNameMessage

    try
    {
        # Try to get a named session configuration
        $endpoint = Get-PSSessionConfiguration -Name $Name -ErrorAction Stop -Verbose:$false
    
        $namePresentMessage = $($LocalizedData.EndpointNameMessage) -f $Name,'present'
        Write-Verbose -Message $namePresentMessage

        # If the endpoint shouldn't be present, return false
        if($Ensure -eq 'Absent')
        {
            return $false
        }
        # else validate endpoint properties and return the result
        else
        {
            # Remove Name and Ensure from the bound parameters for splatting
            if($PSBoundParameters.ContainsKey('Name')){$null = $PSBoundParameters.Remove('Name')}
            if($PSBoundParameters.ContainsKey('Ensure')){$null = $PSBoundParameters.Remove('Ensure')}

            return (Validate-ResourceProperties -Endpoint $endpoint @PSBoundParameters)
        }
    }
    catch [Microsoft.PowerShell.Commands.WriteErrorException]
    {
        $nameAbsentMessage = $($LocalizedData.EndpointNameMessage) -f $Name,'absent'
        Write-Verbose -Message $nameAbsentMessage

        return ($Ensure -eq 'Absent')
    }
}

# Internal function to translate the endpoint's accessmode to the "Disabled","Local","Remote" values
function Get-EndpointAccessMode
{
    param
    (
        [Parameter(Mandatory)]
        $Endpoint
    )
    if($($endpoint.Enabled) -eq $false) {return 'Disabled'}
    elseif($endpoint.Permission -and ($endpoint.Permission).contains('NT AUTHORITY\NETWORK AccessDenied')){return 'Local'}
    else{return 'Remote'}
}

# Internal function to write set verbose messages for collection of properties
function Write-EndpointMessage
{
    param
    (
        [Parameter(Mandatory)]
        [Hashtable]$parameters,

        [Parameter(Mandatory)]
        [String[]]$keysToSkip
    )

    foreach($key in $parameters.keys)
    {
        if($keysToSkip -notcontains $key)
        {
            $setPropertyMessage = $($LocalizedData.SetPropertyMessage) -f $key,$($parameters[$key])
            Write-Verbose -Message $setPropertyMessage
        }
    }
}

# Internal function to validate an endpoint's properties
function Validate-ResourceProperties
{
    param
    (
        [Parameter(Mandatory)]
        $Endpoint,

        [String]$StartupScript,

        [PSCredential]$RunAsCredential,

        [String]$SecurityDescriptorSDDL,

        [ValidateSet('Local','Remote','Disabled')]
        [String]$AccessMode,

        [Switch]$Apply
    )

    if($Apply)
    {
        $parameters = @{}
    }

    # Check if the SDDL is same as specified
    if($PSBoundParameters.ContainsKey('SecurityDescriptorSDDL'))
    { 
            $querySDDLMessage = $($LocalizedData.CheckPropertyMessage) -f 'SDDL',$SecurityDescriptorSDDL  
            Write-Verbose -Message $querySDDLMessage

            # If endpoint SDDL is not same as specified
            if($endpoint.SecurityDescriptorSddl -and ($endpoint.SecurityDescriptorSddl -ne $SecurityDescriptorSDDL))
            {
                $notDesiredSDDLMessage = $($LocalizedData.NotDesiredPropertyMessage) -f 'SDDL',$SecurityDescriptorSDDL,$($endpoint.SecurityDescriptorSddl)
                Write-Verbose -Message $notDesiredSDDLMessage

                if($Apply)
                {
                    $parameters['SecurityDescriptorSddl'] = $SecurityDescriptorSDDL
                }
                else
                {
                    return $false
                }
            }
            # If endpoint SDDL is same as specified
            else
            {
                $desiredSDDLMessage = $($LocalizedData.DesiredPropertyMessage) -f 'SDDL',$SecurityDescriptorSDDL
                Write-Verbose -Message $desiredSDDLMessage
            }
        }

    # Check the RunAs user is same as specified
    if($PSBoundParameters.ContainsKey('RunAsCredential'))
    {
            $queryRunAsMessage = $($LocalizedData.CheckPropertyMessage) -f 'RunAs user',$($RunAsCredential.UserName) 
            Write-Verbose -Message $queryRunAsMessage

            # If endpoint RunAsUser is not same as specified
            if($endpoint.RunAsUser -ne $RunAsCredential.UserName)
            {
                $notDesiredRunAsMessage = $($LocalizedData.NotDesiredPropertyMessage) -f 'RunAs user',$($RunAsCredential.UserName),$($endpoint.RunAsUser)
                Write-Verbose -Message $notDesiredRunAsMessage
                
                if($Apply)
                {
                    $parameters['RunAsCredential'] = $RunAsCredential
                }
                else
                {
                    return $false
                }
            }
            # If endpoint RunAsUser is same as specified
            else
            {
                $desiredRunAsMessage = $($LocalizedData.DesiredPropertyMessage) -f 'RunAs user',$($RunAsCredential.UserName)
                Write-Verbose -Message $desiredRunAsMessage
            }
        }

    # Check if the StartupScript is same as specified
    if($PSBoundParameters.ContainsKey('StartupScript'))
    {
        $queryStartupScriptMessage = $($LocalizedData.CheckPropertyMessage) -f 'startup script',$StartupScript
        Write-Verbose -Message $queryStartupScriptMessage

        # If endpoint StartupScript is not same as specified
        if($endpoint.StartupScript -ne $StartupScript)
        {
            $notDesiredStartupScriptMessage = $($LocalizedData.NotDesiredPropertyMessage) -f 'startup script',$StartupScript,$($endpoint.StartupScript)
            Write-Verbose -Message $notDesiredStartupScriptMessage

            if($Apply)
            {   
                $parameters['StartupScript'] = $StartupScript
            }
            else
            {
                return $false
            }
        }
        # If endpoint StartupScript is same as specified
        else
        {
            $desiredStartupScriptMessage = $($LocalizedData.DesiredPropertyMessage) -f 'startup script',$StartupScript
            Write-Verbose -Message $desiredStartupScriptMessage
        }
    }

    # Check if AccessMode is same as specified
    if($PSBoundParameters.ContainsKey('AccessMode'))
    {
        $queryAccessModeMessage = $($LocalizedData.CheckPropertyMessage) -f 'acess mode',$AccessMode
        Write-Verbose -Message $queryAccessModeMessage

        $curAccessMode = Get-EndpointAccessMode -Endpoint $Endpoint
                
        # If endpoint access mode is not same as specified
        if($curAccessMode -ne $AccessMode)
        {            
            $notDesiredAccessModeMessage = $($LocalizedData.NotDesiredPropertyMessage) -f 'access mode',$AccessMode,$curAccessMode
            Write-Verbose -Message $notDesiredAccessModeMessage

            if($Apply)
            {   
                $parameters['AccessMode'] = $AccessMode
            }
            else
            {
                return $false
            }
        }
        # If endpoint access mode is same as specified
        else
        {
            $desiredAccessModeMessage = $($LocalizedData.DesiredPropertyMessage) -f 'access mode',$AccessMode
            Write-Verbose -Message $desiredAccessModeMessage
        }
    }

    if($Apply)
    {
        return $parameters
    }
    else
    {
        return ($Ensure -eq 'Present')
    }
}

# Internal function to throw terminating error with specified errroCategory, errorId and errorMessage
function New-TerminatingError
{
    param
    (
        [Parameter(Mandatory)]
        [String]$errorId,
        
        [Parameter(Mandatory)]
        [String]$errorMessage,

        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorCategory]$errorCategory
    )
    
    $exception = New-Object System.InvalidOperationException $errorMessage 
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $null
    throw $errorRecord
}

Export-ModuleMember -Function *-TargetResource