plugins/SalesforceSC/Public/setup/install-plugin.ps1
<# Function Install-Plugin { ################################################ # # INPUT # ################################################ #----------------------------------------------- # DEBUG SWITCH #----------------------------------------------- $debug = $true $configMode = $true ################################################ # # NOTES # ################################################ ################################################ # # SCRIPT ROOT # ################################################ if ( $debug ) { # Load scriptpath if ($MyInvocation.MyCommand.CommandType -eq "ExternalScript") { $scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition } else { $scriptPath = Split-Path -Parent -Path ([Environment]::GetCommandLineArgs()[0]) } } else { $scriptPath = "$( $params.scriptPath )" } Set-Location -Path $scriptPath ################################################ # # SETTINGS AND STARTUP # ################################################ # General settings $modulename = "SFCREATESETTINGS" # Load other generic settings like process id, startup timestamp, ... . ".\bin\general_settings.ps1" # Setup the network security like SSL and TLS . ".\bin\load_networksettings.ps1" # Load functions and assemblies . ".\bin\load_functions.ps1" ################################################ # # START # ################################################ #----------------------------------------------- # ASK FOR SETTINGSFILE #----------------------------------------------- # Default file $settingsFileDefault = "$( $scriptPath )\settings.json" # Ask for another path $settingsFile = Read-Host -Prompt "Where do you want the settings file to be saved? Just press Enter for this default [$( $settingsFileDefault )]" # ALTERNATIVE: The file dialog is not working from Visual Studio Code, but is working from PowerShell ISE or "normal" PowerShell Console #$settingsFile = Set-FileName -initialDirectory "$( $scriptPath )" -filter "JSON files (*.json)|*.json" # If prompt is empty, just use default path if ( $settingsFile -eq "" -or $null -eq $settingsFile) { $settingsFile = $settingsFileDefault } # Check if filename is valid if(Test-Path -LiteralPath $settingsFile -IsValid ) { Write-Host "SettingsFile '$( $settingsFile )' is valid" } else { Write-Host "SettingsFile '$( $settingsFile )' contains invalid characters" } #----------------------------------------------- # ASK FOR LOGFILE #----------------------------------------------- # Default file $logfileDefault = "$( $scriptPath )\cr.log" # Ask for another path $logfile = Read-Host -Prompt "Where do you want the log file to be saved? Just press Enter for this default [$( $logfileDefault )]" # ALTERNATIVE: The file dialog is not working from Visual Studio Code, but is working from PowerShell ISE or "normal" PowerShell Console #$settingsFile = Set-FileName -initialDirectory "$( $scriptPath )" -filter "JSON files (*.json)|*.json" # If prompt is empty, just use default path if ( $logfile -eq "" -or $null -eq $logfile) { $logfile = $logfileDefault } # Check if filename is valid if(Test-Path -LiteralPath $logfile -IsValid ) { Write-Host "Logfile '$( $logfile )' is valid" } else { Write-Host "Logfile '$( $logfile )' contains invalid characters" } #----------------------------------------------- # ASK FOR TOKENFILE #----------------------------------------------- # Default file $tokenFileDefault = "$( $scriptPath )\sf.token" # Ask for another path $tokenFile = Read-Host -Prompt "Where do you want the token file to be saved? Just press Enter for this default [$( $tokenFileDefault )]" # ALTERNATIVE: The file dialog is not working from Visual Studio Code, but is working from PowerShell ISE or "normal" PowerShell Console #$settingsFile = Set-FileName -initialDirectory "$( $scriptPath )" -filter "JSON files (*.json)|*.json" # If prompt is empty, just use default path if ( $tokenFile -eq "" -or $null -eq $tokenFile) { $tokenFile = $tokenFileDefault } # Check if filename is valid if(Test-Path -LiteralPath $tokenFile -IsValid ) { Write-Host "SettingsFile '$( $tokenFile )' is valid" } else { Write-Host "SettingsFile '$( $tokenFile )' contains invalid characters" } #----------------------------------------------- # LOAD LOGGING MODULE NOW #----------------------------------------------- $settings = @{ "logfile" = $logfile } # Setup the log and do the initial logging e.g. for input parameters . ".\bin\startup_logging.ps1" #----------------------------------------------- # LOG THE NEW SETTINGS CREATION #----------------------------------------------- Write-Log -message "Creating a new settings file" -severity ( [Logseverity]::WARNING ) ################################################ # # OBTAIN CLEVERREACH TOKEN # ################################################ #----------------------------------------------- # CONFIRM FOR NEXT STEPS #----------------------------------------------- # Confirm you want to proceed $proceed = $Host.UI.PromptForChoice("New Token", "This will create a NEW token. Previous tokens will be invalid immediatly. Please confirm you are sure to proceed?", @('&Yes'; '&No'), 1) # Leave if answer is not yes If ( $proceed -eq 0 ) { Write-Log -message "Asked for confirmation of new token creation. Answer was 'yes'" } else { Write-Log -message "Asked for confirmation of new token creation. Answer was 'No'" Write-Log -message "Leaving the script now" exit 0 } #----------------------------------------------- # SETTINGS FOR THE TOKEN CREATION #----------------------------------------------- $customProtocol = "sftoken" #"apttoken$( Get-RandomString -length 6 -noSpecialChars )" $clientId = "3MVG9I5UQ_0k_hTkyC7dvZDoWszDfra.IGCBVnxubIu4opOza7jDxuLEIFH5ZWZepZLJ.Axbw.uoCtZqF1vUp" # Certified CleverReach App for Apteco $authUrl = [uri]"https://login.salesforce.com/services/oauth2/authorize" $tokenUrl = [uri]"https://login.salesforce.com/services/oauth2/token" $callbackFile = "$( $env:TEMP )\callback.txt" # This path is also used in the callback.ps1 script # Ask APTECO to enter the client secret Write-Log -message "Asking Apteco about the CleverReach App client secret" $clientSecret = Read-Host -AsSecureString "Please ask Apteco to enter the client secret" $clientCred = New-Object PSCredential $clientId,$clientSecret $clientSecret = "" #----------------------------------------------- # PREPARE REGISTRY #----------------------------------------------- # current path - will get back to this at the end $currentLocation = Get-Location # Switch to registry - choose the current user to not need admin rights $root = "Registry::HKEY_CURRENT_USER\Software\Classes" # User registry - needs no elevated rights # $root = "Registry::HKEY_CLASSES_ROOT" # Global registry - needs admin rights Write-Log -message "Putting new registry entries into '$( $root )' with custom protocol '$( $customProtocol )'" Set-Location -Path $root # Remove the registry entries, if already existing If ( Test-Path -path $customProtocol ) { Write-Log -message "Custom protocol folder was already existing. Removing it now." Remove-Item -Path $customProtocol -Force } # Create the base entries now New-Item -Path $customProtocol New-ItemProperty -Path $customProtocol -Name "(Default)" -PropertyType String -Value "URL:$( $customProtocol )" New-ItemProperty -Path $customProtocol -Name "URL Protocol" -PropertyType String -Value "" # Create more keys and properties for sub items Set-Location -Path ".\$( $customProtocol )" New-Item -Path ".\DefaultIcon" New-Item -Path ".\shell\open\command" -force # Creates the items recursively New-ItemProperty -Path ".\shell\open\command" -Name "(Default)" -PropertyType String -Value """powershell.exe"" -File ""$( $scriptPath )\bin\callback.ps1"" ""%1""" # Go back to original path Set-Location -path $currentLocation.Path Write-Log -message "Created the registry entries" #----------------------------------------------- # CLEVERREACH OAUTHv2 PROCESS - STEP 1 #----------------------------------------------- # Prepare redirect URI $redirectUri = "$( $customProtocol )://localhost" # The www.apteco.de is only there for cleverreach, otherwise the url would be invalid and not accepted # STEP 1: Prepare the first call to let the user log into cleverreach # SOURCE: https://powershellmagazine.com/2019/06/14/pstip-a-better-way-to-generate-http-query-strings-in-powershell/ $nvCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) $nvCollection.Add('response_type','code') $nvCollection.Add('client_id',$clientId) $nvCollection.Add('grant',"basic") $nvCollection.Add('redirect_uri', $redirectUri) # a dummy url like apteco.de is needed # Create the url $uriRequest = [System.UriBuilder]$authUrl $uriRequest.Query = $nvCollection.ToString() # Remove callback file if it exists If ( Test-Path -Path $callbackFile ) { "Removing callback file '$( $callbackFile )'" Remove-Item $callbackFile -Force } # Open the default browser with the generated url Write-Log -message "Opening the browser now to allow the CleverReach Apteco APP access to the account" Write-Log -message "Please finish the process in your browser now" Write-Log -message "NOTE:" Write-Log -message " APTECO WILL NOT GET ACCESS TO YOUR DATA THROUGH THE APP!" Write-Log -message " ONLY THIS LOCAL GENERATED TOKEN CAN BE USED FOR ACCESS!" Start-Process $uriRequest.Uri.OriginalString # Wait Write-Log -message "Waiting for the callback file '$( $callbackFile )'" Do { Write-Host "." -NoNewline Start-Sleep -Milliseconds 500 } Until ( Test-Path -Path $callbackFile ) Write-Log -message "Callback file found '$( $callbackFile )'" # Read and parse callback file $callback = Get-Content -Path $callbackFile -Encoding utf8 $callbackUri = [uri]$callback $callbackUriSegments = [System.Web.HttpUtility]::ParseQueryString($callbackUri.Query) $code = $callbackUriSegments["code"] # Remove callback file Write-Log -message "Removing callback file now" Remove-Item $callbackFile -Force #----------------------------------------------- # CLEVERREACH OAUTHv2 PROCESS - STEP 2 #----------------------------------------------- # Prepare the second call to exchange the code quickly for a token $postParams = [Hashtable]@{ Method = "Post" Uri = $tokenUrl Body = [Hashtable]@{ "client_id" = $clientCred.UserName "client_secret" = $clientCred.GetNetworkCredential().Password "redirect_uri" = $redirectUri "grant_type" = "authorization_code" "code" = $code } Verbose = $true } $response = Invoke-RestMethod @postParams Write-Log -message "Got a token with scope '$( $response.scope )'" # Trying an API call # try { # $headers = @{ # "Authorization" = "Bearer $( $response.access_token )" # } # $ttl = Invoke-RestMethod -Uri "https://rest.cleverreach.com/v3/debug/ttl.json" -Method Get -ContentType "application/json; charset=utf-8" -Headers $headers # Write-Log -message "Used token for API call successfully. Token expires at '$( $ttl.date.toString() )'" # } catch { # Write-Log -message "API call was not successful. Aborting the whole script now!" -severity ( [Logseverity]::WARNING ) # throw $_.Exception # } # Clear the variables straight away $clientCred = $null #----------------------------------------------- # HOUSEKEEPING OF REGISTRY #----------------------------------------------- Write-Log -message "Removing temporary registry entries" # Switch to root path of registry Set-Location -Path $root # Remove item now Remove-Item $customProtocol -Recurse # Go back to original path Set-Location -path $currentLocation.Path ################################################ # # SETTINGS # ################################################ $login = @{ "accesstoken" = Get-PlaintextToSecure $response.access_token "refreshtoken" = Get-PlaintextToSecure $response.refresh_token "refreshTokenAutomatically" = $true "refreshTtl" = 604800 # seconds; refresh one week before expiration } #----------------------------------------------- # MAIL SETTINGS #----------------------------------------------- $smtpPass = Read-Host -AsSecureString "Please enter the SMTP password" $smtpPassEncrypted = Get-PlaintextToSecure ((New-Object PSCredential "dummy",$smtpPass).GetNetworkCredential().Password) $mailSettings = @{ smptServer = "smtp.example.com" port = 587 from = "admin@example.com" username = "admin@example.com" password = $smtpPassEncrypted deactivateServerCertificateValidation = $true # $true|$false useSsl = $true # $true|$false useCredentials = $true # $true|$false -> sometimes you have mailservers without user/pass } #----------------------------------------------- # ALL SETTINGS #----------------------------------------------- $settings = @{ # general "base" = "https://rest.cleverreach.com/v3/" "connectionTestUrl" = "https://rest.cleverreach.com/v3/debug/validate.json" "providername" = "CleverReach" "logfile" = $logfile "contentType" = "application/json; charset=utf-8" # Token specific "tokenfile" = "$( $scriptPath )\cr.token" "sendMailOnCheck" = $true "sendMailOnSuccess" = $true "sendMailOnFailure" = $true "notificationReceiver" = "admin@example.com" # Windows scheduled task settings "taskDefaultName" = "Apteco CleverReach Token Refresher" "powershellExePath" = "powershell.exe" # e.g. use pwsh.exe for PowerShell7 "dailyTaskSchedule" = 6 # runs every day at 6 local time in the morning # Mail settings for notification "mail" = $mailSettings # authentication "login" = $login # network "changeTLS" = $true } ################################################ # # PACK TOGETHER SETTINGS AND SAVE AS JSON # ################################################ # rename settings file if it already exists If ( Test-Path -Path $settingsFile ) { $backupPath = "$( $settingsFile ).$( $timestamp.ToString("yyyyMMddHHmmss") )" Write-Log -message "Moving previous settings file to $( $backupPath )" -severity ( [Logseverity]::WARNING ) Move-Item -Path $settingsFile -Destination $backupPath } else { Write-Log -message "There was no settings file existing yet" } # create json object $json = $settings | ConvertTo-Json -Depth 99 # -compress # print settings to console $json # save settings to file $json | Set-Content -path $settingsFile -Encoding UTF8 ################################################ # # EXPORT THE TOKEN # ################################################ # save token to file Write-Log -message "Saving token to '$( $settings.tokenfile )'" Get-SecureToPlaintext -String $settings.login.accesstoken | Set-Content -path "$( $settings.tokenfile )" -Encoding UTF8 -Force ################################################ # # CREATE WINDOWS TASK # ################################################ # Confirm you want a scheduled task $createTask = $Host.UI.PromptForChoice("Confirmation", "Do you want to create a scheduled task for the check and refreshment?", @('&Yes'; '&No'), 0) If ( $createTask -eq "0" ) { # Means yes and proceed Write-Log -message "Creating a scheduled task to check the token daily" # Default file $taskNameDefault = $settings.taskDefaultName # Replace task? $replaceTask = $Host.UI.PromptForChoice("Replace Task", "Do you want to replace the existing task if it exists?", @('&Yes'; '&No'), 0) If ( $replaceTask -eq 0 ) { # Check if the task already exists $matchingTasks = Get-ScheduledTask | where { $_.TaskName -eq $taskName } If ( $matchingTasks.count -ge 1 ) { Write-Log -message "Removing the previous scheduled task for recreation" # To replace the task, remove it without confirmation Unregister-ScheduledTask -TaskName $taskNameDefault -Confirm:$false } # Set the task name to default $taskName = $taskNameDefault } else { # Ask for task name or use default value $taskName = Read-Host -Prompt "Which name should the task have? [$( $taskNameDefault )]" if ( $taskName -eq "" -or $null -eq $taskName) { $taskName = $taskNameDefault } } Write-Log -message "Using name '$( $taskName )' for the task" # TODO [ ] Find a reliable method for credentials testing # TODO [ ] Check if a user has BatchJobrights ##[System.Security.Principal.WindowsIdentity]::GrantUserLogonAsBatchJob # Enter username and password $taskCred = Get-Credential # Parameters for scheduled task $taskParams = [Hashtable]@{ TaskPath = "\Apteco\" TaskName = $taskname Description = "Refreshes the token for CleverReach because it is only valid for 30 days" Action = New-ScheduledTaskAction -Execute "$( $settings.powershellExePath )" -Argument "-ExecutionPolicy Bypass -File ""$( $scriptPath )\cleverreach__05__check_token.ps1""" #Principal = New-ScheduledTaskPrincipal -UserId $taskCred.Name -LogonType "ServiceAccount" # Using this one is always interactive mode and NOT running in the background Trigger = New-ScheduledTaskTrigger -at ([Datetime]::Today.AddDays(1).AddHours($settings.dailyTaskSchedule)) -Daily # Starting tomorrow at six in the morning Settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 3) -MultipleInstances "Parallel" # Max runtime of 3 minutes User = $taskCred.UserName Password = $taskCred.GetNetworkCredential().Password #AsJob = $true } # Create the scheduled task try { Write-Log -message "Creating the scheduled task now" $newTask = Register-ScheduledTask @taskParams #T1 -InputObject $task } catch { Write-Log -message "Creation of task failed or is not completed, please check your scheduled tasks and try again" throw $_.Exception } # Check the scheduled task $task = $newTask #Get-ScheduledTask | where { $_.TaskName -eq $taskName } $taskInfo = $task | Get-ScheduledTaskInfo Write-Host "Task with name '$( $task.TaskName )' in '$( $task.TaskPath )' was created" Write-Host "Next run '$( $taskInfo.NextRunTime.ToLocalTime().ToString() )' local time" # The task will only be created if valid. Make sure it was created successfully } Write-Log -message "Done with settings creation" ################################################ # # WAIT FOR KEY # ################################################ Write-Host -NoNewLine 'Press any key to continue...'; $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); exit 0 } #> |