Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7288f39f5d | |||
| f32fd57874 |
@@ -94,8 +94,7 @@ End-to-end chain working from a pure managed .NET 10 client: `Hist.GetV → Hist
|
|||||||
Smaller, isolated items — none block the production read surface:
|
Smaller, isolated items — none block the production read surface:
|
||||||
|
|
||||||
- Remote TCP transports (`RemoteTcpIntegrated`, `RemoteTcpCertificate`) untested against an actual remote Historian (tests skip without `HISTORIAN_REMOTE_TCP_HOST`).
|
- Remote TCP transports (`RemoteTcpIntegrated`, `RemoteTcpCertificate`) untested against an actual remote Historian (tests skip without `HISTORIAN_REMOTE_TCP_HOST`).
|
||||||
- Explicit username/password tag-metadata path (`HistorianWcfTagClient` line ~357) throws — only integrated security wired for that op.
|
- Explicit username/password tag-metadata path is wired (validator only blocks no-auth-at-all), but live-verification requires `HISTORIAN_USER`+`HISTORIAN_PASSWORD` set; gated test `GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian` skips otherwise.
|
||||||
- `Historian2020ProtocolDialect.GetTagInfoByName/GetTagInfos` throws — currently dead code; `GetTagMetadataAsync` works through the WCF tag client instead.
|
|
||||||
- Per-row trailing ~24 bytes of `GetNextQueryResultBuffer` are not decoded (likely per-sample value/source/state metadata).
|
- Per-row trailing ~24 bytes of `GetNextQueryResultBuffer` are not decoded (likely per-sample value/source/state metadata).
|
||||||
- `EnsureTagAsync` distinct `MinRaw`/`MaxRaw` persistence requires `ApplyScaling=true` + a follow-up `UpdateTags` call — not yet wired (no API user has asked).
|
- `EnsureTagAsync` distinct `MinRaw`/`MaxRaw` persistence requires `ApplyScaling=true` + a follow-up `UpdateTags` call — not yet wired (no API user has asked).
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
@@ -87,17 +87,6 @@ internal sealed class Historian2020ProtocolDialect
|
|||||||
#pragma warning restore CA1416
|
#pragma warning restore CA1416
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAsyncEnumerable<string> BrowseTagNamesAsync(string filter, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Missing<string>("StartLikeTagNameSearch/GetLikeTagnames", cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<HistorianTagMetadata?> GetTagMetadataAsync(string tag, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
throw new ProtocolEvidenceMissingException("GetTagInfoByName/GetTagInfos");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<HistorianConnectionStatus> GetConnectionStatusAsync(CancellationToken cancellationToken)
|
public Task<HistorianConnectionStatus> GetConnectionStatusAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|||||||
@@ -351,10 +351,18 @@ internal static class HistorianWcfTagClient
|
|||||||
|
|
||||||
private static void ValidateSupportedAuth(HistorianClientOptions options)
|
private static void ValidateSupportedAuth(HistorianClientOptions options)
|
||||||
{
|
{
|
||||||
|
// Three valid auth shapes:
|
||||||
|
// 1. IntegratedSecurity=true (current Windows identity, no UserName/Password)
|
||||||
|
// 2. IntegratedSecurity=false + UserName + Password (NTLM/Kerberos with explicit creds)
|
||||||
|
// 3. IntegratedSecurity=true + UserName + Password (impersonation/explicit override)
|
||||||
|
// The fourth combination — IntegratedSecurity=false with no UserName/Password — has
|
||||||
|
// no way to authenticate against the /Hist-Integrated endpoint and is rejected.
|
||||||
if (!options.IntegratedSecurity
|
if (!options.IntegratedSecurity
|
||||||
&& (!string.IsNullOrEmpty(options.UserName) || !string.IsNullOrEmpty(options.Password)))
|
&& string.IsNullOrEmpty(options.UserName)
|
||||||
|
&& string.IsNullOrEmpty(options.Password))
|
||||||
{
|
{
|
||||||
throw new ProtocolEvidenceMissingException("Open2 explicit username/password tag browse");
|
throw new ProtocolEvidenceMissingException(
|
||||||
|
"Tag browse / metadata requires either IntegratedSecurity=true OR an explicit UserName + Password.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,6 +313,55 @@ public sealed class HistorianClientIntegrationTests
|
|||||||
Assert.Equal(host, status.ServerName);
|
Assert.Equal(host, status.ServerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The validator inside HistorianWcfTagClient now allows IntegratedSecurity=false WHEN
|
||||||
|
// explicit UserName + Password are provided (NTLM/Kerberos with non-current-user creds).
|
||||||
|
// It still rejects the no-credentials-at-all case since there's no way to authenticate
|
||||||
|
// against /Hist-Integrated.
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTagMetadataAsync_NoAuthAndNoCredentials_Throws()
|
||||||
|
{
|
||||||
|
HistorianClient client = new(new HistorianClientOptions
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
IntegratedSecurity = false,
|
||||||
|
UserName = string.Empty,
|
||||||
|
Password = string.Empty,
|
||||||
|
});
|
||||||
|
await Assert.ThrowsAsync<ProtocolEvidenceMissingException>(
|
||||||
|
() => client.GetTagMetadataAsync("anytag", CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian()
|
||||||
|
{
|
||||||
|
// Live verification of the explicit-creds tag-metadata path. Gated on
|
||||||
|
// HISTORIAN_USER + HISTORIAN_PASSWORD being set; skips cleanly otherwise. The path
|
||||||
|
// routes through WCF Windows transport security with Credentials.Windows.ClientCredential.
|
||||||
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
||||||
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
||||||
|
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
||||||
|
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
||||||
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)
|
||||||
|
|| string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(password)
|
||||||
|
|| !OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HistorianClient client = new(new HistorianClientOptions
|
||||||
|
{
|
||||||
|
Host = host,
|
||||||
|
IntegratedSecurity = false,
|
||||||
|
UserName = user,
|
||||||
|
Password = password,
|
||||||
|
});
|
||||||
|
|
||||||
|
AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata =
|
||||||
|
await client.GetTagMetadataAsync(testTag, CancellationToken.None);
|
||||||
|
Assert.NotNull(metadata);
|
||||||
|
Assert.Equal(testTag, metadata.Name);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetTagMetadataAsync_ReturnsConfiguredTestTagMetadata()
|
public async Task GetTagMetadataAsync_ReturnsConfiguredTestTagMetadata()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user