Public/Set-AtwsModuleConfiguration.ps1

<#
    .COPYRIGHT
    Copyright (c) Hugo Klemmestad. All rights reserved. Licensed under the MIT license.
    See https://github.com/ecitsolutions/Autotask/blob/master/LICENSE.md for license information.

#>


Function Set-AtwsModuleConfiguration {
    <#
        .SYNOPSIS
            This function updates the runtime configuration of the module.
        .DESCRIPTION
            This function updates the runtime configuration of the module. Values that can be changed while the module is loaded
            are:
            - Credentials (both username and password may be changed separately)
            - API key
            - Whether parameters with picklist values should show labels in place of numbers (ConvertPicklistIdsToLabel)
            - Debug preference
            - Verbose preference
            - Errorlimit - how many errors should Set-Atws* or New-Atws* functions accept before the operation is aborted

            The parameters Prefix and RefreshCache does not have any effect on the current connection. They must be saved and loaded
            as part of a later connection to have any effect.
        .INPUTS
            Nothing.
        .OUTPUTS
            Nothing.
        .EXAMPLE
            Set-AtwsModuleConfiguration -Credential $Credential -ApiTrackingIdentifier $string
        .NOTES
            NAME: Set-AtwsModuleConfiguration

            Related commands:
            Get-AtwsModuleConfiguration
            New-AtwsModuleConfiguration
            Remove-AtwsModuleConfiguration
            Save-AtwsModuleConfiguration

    #>

    
    [cmdletbinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Medium',
        DefaultParameterSetName = 'Username_and_password'
    )]
    [Alias('Set-AtwsProfile')]
    Param
    ( 
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateNotNullOrEmpty()] 
        [pscredential]
        # An API user to Autotask. Optional.
        $Credential,

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [ValidateNotNullOrEmpty()] 
        [string]
        # A new username to use for the connection. Optional.
        $Username,
    
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [ValidateNotNullOrEmpty()]
        [securestring]
        # A new password for this connection. Must be encrypted as SecureString. Optional.
        $SecurePassword,
    
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateNotNullOrEmpty()]
        [securestring]
        # The API identifier from your resource in Autotask. Must be encrypted as SecureString. Optional.
        $SecureTrackingIdentifier, 
        
        [Parameter(
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        # The API identifier from your resource in Autotask. Must be encrypted as SecureString. Optional.
        $TrackingIdentifier,
    
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [switch]
        # Please ignore. It is only here for backwards compatibility. Use -PicklistConversion
        $ConvertPicklistIdToLabel,
    
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateScript( {
                # It can be empty, but if it isn't it should be max 8 characters and only letters and numbers
                if ($_.length -eq 0 -or ($_ -match '[a-zA-Z0-9]' -and $_.length -gt 0 -and $_.length -le 8)) {
                    $true
                }
                else {
                    $false
                }
            })]
        [string]
        # Please ignore. It is only here for backwards functionality. Will be removed soon.
        $Prefix,

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [switch]
        # Please ignore. It is only here for backwards compatibility. Will be removed soon.
        $RefreshCache,

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateSet('Stop', 'Inquire', 'Continue', 'SilentlyContinue')]
        [string]
        # You may save a default debug preference so you may have a separate profile for debugging.
        $DebugPref,

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateSet('Stop', 'Inquire', 'Continue', 'SilentlyContinue')]
        [string]
        # You may save a default verbose preference so you may have a separate profile for debugging.
        $VerbosePref,

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateRange(0, 100)]
        [int]
        # For bulk operations. When you post 100+ objects with changes to the API it is annoying if the operation
        # fails on all of them just because 1 of them could not be updated. How many such errors can you live with
        # before the whole operation should fail? Default = 10. Optional.
        $ErrorLimit,

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ArgumentCompleter( {
                param($Cmd, $Param, $Word, $Ast, $FakeBound)
                $(Get-ChildItem -Path $Global:AtwsModuleConfigurationPath -Filter "*.clixml").FullName
            })]
        [ValidateScript( { 
                Test-Path $_
            })]
        [IO.FileInfo]
        [alias('ProfilePath')]
        # Full path to an alternate configuration file you want the profile to be saved to. Optional.
        $Path = $(Join-Path -Path $Global:AtwsModuleConfigurationPath -ChildPath AtwsConfig.clixml),

        # Use this parameter to save to another configuration name.
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ArgumentCompleter( {
                param($Cmd, $Param, $Word, $Ast, $FakeBound)
                if ($FakeBound.Path) {
                    [IO.FileInfo]$filepath = $FakeBound.Path
                }
                else {
                    [IO.FileInfo]$filepath = $(Join-Path -Path $Global:AtwsModuleConfigurationPath -ChildPath AtwsConfig.clixml)
                }
                $tempsettings = Import-Clixml -Path $filepath.Fullname
                if ($tempsettings -is [hashtable]) {
                    foreach ($item in ($tempsettings.keys | Sort-Object)) {
                        "'{0}'" -F $($item -replace "'", "''")
                    }
                }
            })]
        [ValidateNotNullOrEmpty()] 
        [String]
        [alias('ProfileName')]
        # The name you want to use on the connection profile. Default name is 'Default'.
        $Name = 'Default',

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateSet('Disabled', 'Inline', 'LabelField')]
        [string]
        # How do you want picklist items to be expanded: Not at all (Disabled), have the text label
        # replace the index value (Inline) or a separate property with "Label" as suffix (LabelField)
        $PickListExpansion = 'LabelField',

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ValidateSet('Disabled', 'Inline', 'Hashtable')]
        [string]
        # How do you want UDFs to be expanded: Not at all (Disabled), as new properties with
        # a hashtag as prefix (Inline) or as a hashtable on a single property .UDF ()
        $UdfExpansion = 'Inline',

        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'Username_and_password'
        )]
        [Parameter(
            ParameterSetName = 'Credentials'
        )]
        [ArgumentCompleter( {
                param($Cmd, $Param, $Word, $Ast, $FakeBound)
                'Disabled'
                'Local'
                (Get-TimeZone -ListAvailable).Id | Sort-Object
            })]
        [ValidateScript( {
                # Allow disabled and local before testing timezone conversion
                if ($_ -in 'Disabled', 'Local') { return $true }
                # Allow any valid TimeZone on current system
                try { $null = [System.Timezoneinfo]::FindSystemTimeZoneById($_) }
                catch { return $false }
                return $true
            })]
        [string]
        # The Autotask API always uses Eastern Standard Time for all DateTime objects. This option
        # controls which timezone DateTime objects will be converted to when they are retrieved. The
        # default setting is 'Local', which imply that all DateTime objects will be converted to
        # the current, local timezone setting on the system where the code runs. Other options are
        # 'Disabled' - do not perform any timezone conversion at all; and 'specific/timezone', i.e.
        # any timezone your local system supports. Useful if your companys locations span multiple
        # timezones.
        $DateConversion = 'Local', 

        [switch]
        # Return the changed configuration as a new configuration object. Useful if you want to change
        # an option (or more) in the current running configuration and save it to a new profile name
        # in one go.
        $PassThru
    )
    
    begin { 
    
        # Enable modern -Debug behavior
        if ($PSCmdlet.MyInvocation.BoundParameters['Debug'].IsPresent) {
            $DebugPreference = 'Continue' 
        }
        else {
            # Respect configured preference
            $DebugPreference = $Script:Atws.Configuration.DebugPref
        }
    
        Write-Debug ('{0}: Begin of function' -F $MyInvocation.MyCommand.Name)

        if (!($PSCmdlet.MyInvocation.BoundParameters['Verbose'].IsPresent)) {
            # No local override of central preference. Load central preference
            $VerbosePreference = $Script:Atws.Configuration.VerbosePref
        }
    
    }
  
    process {

        # Read existing configuration from disk
        if (Test-Path $Path) {
            Try { 
                # Try to save to the path
                $settings = Import-Clixml -Path $Path.Fullname
            }
            catch {
                $message = "{0}`nStacktrace:`n{1}" -f $_, $_.ScriptStackTrace
                throw (New-Object System.Configuration.Provider.ProviderException $message)
        
                return
            }
        }

        # Create an empty setting table
        if (-not ($settings -is [hashtable])) {
            $settings = @{}
        }

        # Get current configuration
        if ($Script:Atws.integrationsValue) {
            # We are connected. Use active configuration.
            $configuration = $Script:Atws.Configuration
        }
        # Not connected. Do we have an existing configuration from disk with this name?
        elseIf ($settings.containskey($Name)) {
            # Use saved configuration
            $configuration = $settings[$Name]
        }
        else {
            $message = "Not connected and no configuration by name '{0}' exists. Create a new configuration with New-AtwsModuleConfiguration. You may save it using Save-AtwsModuleConfiuguration." -f $Name
            throw (New-Object System.Configuration.Provider.ProviderException $message)
        
            return           
        }

        # Only run code if parameter has been used
        foreach ($parameter in $PSBoundParameters.GetEnumerator()) { 

            $caption = $MyInvocation.MyCommand.Name
            $verboseDescription = '{0}: Setting {1} to {2} as requested.' -F $caption, $parameter.key, $parameter.value
            $verboseWarning = '{0}: About to set {1} to {2}. Do you want to continue?' -F $caption, $parameter.key, $parameter.value

            if ($PSCmdlet.ShouldProcess($verboseDescription, $verboseWarning, $caption)) {
                # Only run code if parameter has been used
                switch ($parameter.key) { 
                    'Credential' {
                        $configuration.Username = $Credential.UserName
                        $configuration.SecurePassword = $Credential.Password
                    }
                    'Username' {
                        $configuration.Username = $UserName
                    }
                    'SecurePassword' {
                        $configuration.SecurePassword = $SecurePassword
                    }
                    'SecureTrackingIdentifier' { 
                        $configuration.SecureTrackingIdentifier = $SecureTrackingIdentifier
                    }
                    'TrackingIdentifier' { 
                        $configuration.SecureTrackingIdentifier = ConvertTo-SecureString $TrackingIdentifier -AsPlainText -Force
                    }
                    'ConvertPicklistIdToLabel' {
                        $configuration.ConvertPicklistIdToLabel = $ConvertPicklistIdToLabel.IsPresent
                        if ($ConvertPicklistIdToLabel.IsPresent) {
                            Add-Member -InputObject $configuration -MemberType NoteProperty -Name PicklistExpansion -Value 'Inline' -Force
                        }
                    }
                    'RefreshCache' { 
                        $configuration.RefreshCache = $RefreshCache.IsPresent
                    }
                    'DebugPref' { 
                        $configuration.DebugPref = $DebugPref
                    }
                    'VerbosePref' {
                        $configuration.VerbosePref = $VerbosePref
                    }
                    'ErrorLimit' {
                        $configuration.ErrorLimit = $ErrorLimit
                    }
                    'PicklistExpansion' {
                        Add-Member -InputObject $configuration -MemberType NoteProperty -Name PicklistExpansion -Value $PicklistExpansion -Force
                    }
                    'UdfExpansion' {
                        Add-Member -InputObject $configuration -MemberType NoteProperty -Name UdfExpansion -Value $UdfExpansion -Force
                    }
                    'DateConversion' {
                        Add-Member -InputObject $configuration -MemberType NoteProperty -Name DateConversion -Value $DateConversion -Force
                    }
                }
            }
        }

        # Are there any required properties where we have default values that does not have a value?
        $requiredProperties = @(
            'DebugPref', 'VerbosePref', 'ErrorLimit', 'PicklistExpansion', 'UdfExpansion', 'DateConversion'
        )
        foreach ($p in $requiredProperties) {
            if ($configuration.PSObject.Properties.name -notcontains $p) {
                $value = Get-Variable -Name $p -ValueOnly
                Add-Member -InputObject $configuration -MemberType NoteProperty -Name $p -Value $value -Force
            }
        }

        # Are we connected? Update current settings
        if ($Script:Atws.integrationsValue) {
            $Script:Atws.Configuration = $configuration

            # Prepare securestring password to be converted to plaintext
            $SecurePasswordString = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($configuration.SecurePassword)
            
            $Script:Atws.ClientCredentials.UserName | Add-Member -Force -NotePropertyName UserName -NotePropertyValue $configuration.Username
            
            $BSTRstring = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($SecurePasswordString)
            $Script:Atws.ClientCredentials.UserName | Add-Member -Force -NotePropertyName Password -NotePropertyValue $BSTRstring

            # Set the integrationcode property to the API tracking identifier provided by the user
            $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Configuration.SecureTrackingIdentifier)
            $Script:Atws.IntegrationsValue.IntegrationCode = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR)
        }
        else {
            # We loaded these settings from disk. Save to disk again.
            Save-AtwsModuleConfiguration -Name $Name -Configuration $configuration
        }
    }
  
    end {
        Write-Debug ('{0}: End of function' -F $MyInvocation.MyCommand.Name)
        #TODO: Introduce PipelineSupport from Get-, -Set, -New, and Save-AtwsModuleConfiguration. Not doing this for now as it works as it is, jsut requires a few more lines.
        if ($PassThru.IsPresent) { 
            return $configuration
        }
    }
 
}