From 7288f39f5d5fe8e0ee0228d9f609ee99a5da04fb Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 4 May 2026 15:22:34 -0400 Subject: [PATCH] Add Set-HistorianCredentials.ps1 for DPAPI-encrypted credential persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops a small helper for the gated live-integration tests that need HISTORIAN_USER + HISTORIAN_PASSWORD set in the process environment (currently GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian). Three modes: - Default: prompts for username (default \) and password (silent), saves to %USERPROFILE%\.histsdk\credentials.xml via Export-Clixml. The SecureString inside the PSCredential is DPAPI-encrypted and decryptable only by the same Windows user account on the same machine. - -Load: reads the saved credential and exports HISTORIAN_USER + HISTORIAN_PASSWORD into the current PowerShell session's environment. - -Clear: deletes the saved credential file. Also accepts -Path to override the storage location (e.g. for keeping multiple credential sets side by side) and -UserName to skip the username prompt for password-only re-saves. Stored under the user profile, never inside the repo, so it cannot be committed accidentally. The file format is plain Export-Clixml — no custom encoding shenanigans. Live-verified locally: -Load + dotnet test passes the previously-skipped GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian test against the local Historian with IntegratedSecurity=false. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/Set-HistorianCredentials.ps1 | 117 +++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 scripts/Set-HistorianCredentials.ps1 diff --git a/scripts/Set-HistorianCredentials.ps1 b/scripts/Set-HistorianCredentials.ps1 new file mode 100644 index 0000000..fae789d --- /dev/null +++ b/scripts/Set-HistorianCredentials.ps1 @@ -0,0 +1,117 @@ +<# +.SYNOPSIS +Prompts for Historian credentials and persists them to an encrypted file under the +current user's profile, or loads previously-saved credentials into the current +session's HISTORIAN_USER and HISTORIAN_PASSWORD environment variables. + +.DESCRIPTION +Persistence uses Windows DPAPI via Export-Clixml. The resulting XML can only be +decrypted by the same user account on the same machine. The file is saved +outside the repository (default: $env:USERPROFILE\.histsdk\credentials.xml) so +it cannot be accidentally committed. + +The integration tests in this repo read HISTORIAN_USER and HISTORIAN_PASSWORD +from the process environment. Run this script with -Load before launching +`dotnet test` to inject the saved credentials into the current session. + +.PARAMETER Load +Read previously-saved credentials and set $env:HISTORIAN_USER + $env:HISTORIAN_PASSWORD +in the current PowerShell session. The variables persist for the lifetime of +this session only - re-run with -Load in a new shell. + +.PARAMETER Clear +Delete the saved credentials file. Subsequent -Load calls will fail until +credentials are re-saved. + +.PARAMETER Path +Override the default storage path. Useful for keeping multiple credential sets +side-by-side (e.g. local-vs-remote). + +.PARAMETER UserName +Skip the username prompt and use the supplied value. Password is still prompted +interactively. Convenient for scripted re-saves where only the password rotates. + +.EXAMPLE +PS> .\scripts\Set-HistorianCredentials.ps1 +Prompts for username + password, saves both to %USERPROFILE%\.histsdk\credentials.xml. + +.EXAMPLE +PS> .\scripts\Set-HistorianCredentials.ps1 -Load +Loads the saved credentials into HISTORIAN_USER and HISTORIAN_PASSWORD for this session. + +.EXAMPLE +PS> .\scripts\Set-HistorianCredentials.ps1 -Clear +Deletes the saved credentials file. +#> +[CmdletBinding(DefaultParameterSetName = 'Save')] +param( + [Parameter(ParameterSetName = 'Load')] + [switch]$Load, + + [Parameter(ParameterSetName = 'Clear')] + [switch]$Clear, + + [string]$Path = (Join-Path $env:USERPROFILE '.histsdk\credentials.xml'), + + [Parameter(ParameterSetName = 'Save')] + [string]$UserName +) + +$ErrorActionPreference = 'Stop' + +function ConvertTo-PlainText { + param([Parameter(Mandatory)][securestring]$SecureString) + $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + try { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } + finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } +} + +if ($Clear) { + if (Test-Path $Path) { + Remove-Item -LiteralPath $Path -Force + Write-Host "Deleted $Path" + } else { + Write-Host "Nothing to clear (file does not exist): $Path" + } + return +} + +if ($Load) { + if (-not (Test-Path $Path)) { + throw "No saved credentials at $Path. Run this script without -Load to save them first." + } + $cred = Import-Clixml -LiteralPath $Path + if ($cred -isnot [System.Management.Automation.PSCredential]) { + throw "File at $Path is not a PSCredential - re-save with this script." + } + $env:HISTORIAN_USER = $cred.UserName + $env:HISTORIAN_PASSWORD = ConvertTo-PlainText $cred.Password + Write-Host "Loaded credentials for '$($cred.UserName)' from $Path." + Write-Host "HISTORIAN_USER and HISTORIAN_PASSWORD are set for this PowerShell session." + return +} + +# Save mode (default). +$dir = Split-Path -Parent $Path +if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Force -Path $dir | Out-Null +} + +if ([string]::IsNullOrWhiteSpace($UserName)) { + $suggested = "$env:COMPUTERNAME\$env:USERNAME" + $UserName = Read-Host "Historian user [$suggested]" + if ([string]::IsNullOrWhiteSpace($UserName)) { + $UserName = $suggested + } +} + +$securePassword = Read-Host "Historian password for '$UserName'" -AsSecureString +$cred = New-Object System.Management.Automation.PSCredential($UserName, $securePassword) +$cred | Export-Clixml -LiteralPath $Path + +Write-Host "" +Write-Host "Saved credentials for '$UserName' to $Path" +Write-Host "(DPAPI-encrypted; decryptable only by '$env:USERNAME' on '$env:COMPUTERNAME'.)" +Write-Host "" +Write-Host "Run '.\scripts\Set-HistorianCredentials.ps1 -Load' in any new session to inject" +Write-Host "the credentials into HISTORIAN_USER and HISTORIAN_PASSWORD."