Files
histsdk/scripts/Set-HistorianCredentials.ps1
T
Joseph Doherty 7288f39f5d Add Set-HistorianCredentials.ps1 for DPAPI-encrypted credential persistence
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 <COMPUTERNAME>\<USERNAME>) 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) <noreply@anthropic.com>
2026-05-04 15:22:34 -04:00

118 lines
4.2 KiB
PowerShell

<#
.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."