Separate WinRM Windows credentials from Sonic SMC logins.

WinRM no longer reuses Administrator/Administrator; empty WinRm* uses the current Windows user, with clearer auth errors and LocalCmd guidance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 08:47:35 +02:00
co-authored by Cursor
parent cfbf80ecbf
commit d71d6990ad
4 changed files with 125 additions and 15 deletions
@@ -11,9 +11,11 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
///
/// Modus = WinRm (Standard):
/// PowerShell Remoting (Invoke-Command) → CMD stop/startcontainer auf dem Sonic-Server.
/// WinRM nutzt WinRmUsername/WinRmPassword (Windows); leer = aktueller Benutzer.
/// Username/Password bleiben Sonic-SMC-/Domain-Manager-Logins.
///
/// Modus = LocalCmd:
/// Dieselbe CMD/PowerShell-Logik lokal (Test direkt auf dem Sonic-PC).
/// Dieselbe CMD/PowerShell-Logik lokal (App muss auf dem Sonic-PC laufen; kein WinRM).
///
/// Modus = HttpApi:
/// HTTP REST API mit automatischer Pfad-Erkennung.
@@ -21,6 +21,9 @@ public sealed class WinRmExecutor
_connection = connection;
}
private bool HasExplicitWinRmCredentials
=> !string.IsNullOrWhiteSpace(_connection.WinRmUsername);
public async Task<(bool Success, string? Error)> TestConnectionAsync(
CancellationToken cancellationToken = default)
{
@@ -56,7 +59,7 @@ public sealed class WinRmExecutor
return sessionOk
? (true, null)
: (false, $"WinRM-Verbindung fehlgeschlagen: {sessionError}");
: (false, sessionError ?? "WinRM-Verbindung fehlgeschlagen.");
}
public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync(
@@ -119,8 +122,15 @@ public sealed class WinRmExecutor
return (true, stdout, stderr.Length > 0 ? stderr : null);
}
return (false, stdout.Length > 0 ? stdout : null,
stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}");
string rawError = stderr.Length > 0
? stderr
: $"PowerShell ExitCode={process.ExitCode}";
string error = _connection.ManagementMode == SonicManagementMode.WinRm
? FormatWinRmFailure(rawError)
: Truncate(rawError);
return (false, stdout.Length > 0 ? stdout : null, error);
}
finally
{
@@ -134,24 +144,99 @@ public sealed class WinRmExecutor
/// <summary>
/// Remote-Payload als Base64 einbetten und per [scriptblock]::Create ausführen.
/// Vermeidet verschachteltes ScriptBlock-Brace-Nesting und lokale $-Expansion.
/// WinRM-Credentials: nur <see cref="SonicConnection.WinRmUsername"/> / WinRmPassword.
/// Leer = aktueller Windows-Benutzer (ohne -Credential).
/// </summary>
private string BuildRemoteScript(string host, string scriptBlock)
{
string escapedPwd = _connection.Password.Replace("'", "''");
string escapedUser = _connection.Username.Replace("'", "''");
string escapedHost = host.Replace("'", "''");
string remoteB64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptBlock));
StringBuilder sb = new();
sb.Append("$ErrorActionPreference = 'Stop'\n");
if (HasExplicitWinRmCredentials)
{
string escapedPwd = (_connection.WinRmPassword ?? string.Empty).Replace("'", "''");
string escapedUser = _connection.WinRmUsername.Replace("'", "''");
sb.Append($"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n");
sb.Append($"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n");
}
sb.Append($"$remoteB64 = '{remoteB64}'\n");
sb.Append("$remoteScript = [System.Text.Encoding]::Unicode.GetString(");
sb.Append("[System.Convert]::FromBase64String($remoteB64))\n");
sb.Append("$sb = [scriptblock]::Create($remoteScript)\n");
sb.Append($"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} ");
if (HasExplicitWinRmCredentials)
{
sb.Append("-Credential $cred ");
}
sb.Append("-ScriptBlock $sb -ErrorAction Stop");
return sb.ToString();
}
/// <summary>
/// Erkennt typische WinRM-/Windows-Auth-Fehler und liefert eine klare Handlungsanweisung.
/// Username/Password in appsettings sind Sonic-SMC nicht Windows/WinRM.
/// </summary>
private string FormatWinRmFailure(string rawError)
{
string truncated = Truncate(rawError);
if (!LooksLikeWinRmAuthFailure(rawError))
{
return truncated;
}
string authHint = HasExplicitWinRmCredentials
? "WinRmUsername/WinRmPassword prüfen (Windows-Konto mit WinRM-Rechten auf dem Zielrechner)."
: "Aktueller Windows-Benutzer hat keine WinRM-Berechtigung auf dem Zielrechner " +
"(oder Kerberos/CredSSP fehlt). WinRmUsername/WinRmPassword setzen " +
"oder App unter einem berechtigten Windows-Konto starten.";
return
"$ErrorActionPreference = 'Stop'\n" +
$"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n" +
$"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n" +
$"$remoteB64 = '{remoteB64}'\n" +
"$remoteScript = [System.Text.Encoding]::Unicode.GetString(" +
"[System.Convert]::FromBase64String($remoteB64))\n" +
"$sb = [scriptblock]::Create($remoteScript)\n" +
$"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} " +
"-Credential $cred -ScriptBlock $sb -ErrorAction Stop";
"WinRM-Authentifizierung fehlgeschlagen: Windows-Anmeldedaten falsch oder fehlend.\n" +
"Hinweis: Username/Password in appsettings sind Sonic-SMC-/Domain-Manager-Logins " +
"NICHT für WinRM/Windows.\n" +
authHint + "\n" +
"Alternativen:\n" +
" • ManagementMode=LocalCmd setzen und die App direkt auf dem Sonic-Server starten (kein WinRM).\n" +
" • WinRmUsername/WinRmPassword mit gültigem Windows-Konto befüllen.\n" +
$"Details: {truncated}";
}
private static bool LooksLikeWinRmAuthFailure(string error)
{
if (string.IsNullOrEmpty(error))
{
return false;
}
ReadOnlySpan<string> markers =
[
"Benutzername oder das Kennwort ist falsch",
"username or password is incorrect",
"Access is denied",
"Zugriff verweigert",
"Logon failure",
"Anmeldefehler",
"PSRemotingTransportException",
"UnauthorizedAccess",
"WinRM cannot process the request",
"der remotecomputer hat den netzwerkdatenverkehr verweigert"
];
foreach (string marker in markers)
{
if (error.Contains(marker, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public static string ApplyScriptTemplate(