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
@@ -21,9 +21,27 @@ public sealed class SonicConnection
/// </summary> /// </summary>
public required string ConnectionUrl { get; init; } public required string ConnectionUrl { get; init; }
/// <summary>Sonic SMC / Domain Manager Login (nicht für WinRM).</summary>
public required string Username { get; init; } public required string Username { get; init; }
/// <summary>Sonic SMC / Domain Manager Passwort (nicht für WinRM).</summary>
public required string Password { get; init; } public required string Password { get; init; }
/// <summary>
/// Windows-Konto für WinRM (<c>Invoke-Command -Credential</c>).
/// Leer = aktueller Prozess-Benutzer ohne explizite Credentials.
/// Nicht mit <see cref="Username"/>/<see cref="Password"/> (Sonic SMC) verwechseln.
/// </summary>
public string WinRmUsername { get; init; } = string.Empty;
/// <summary>Windows-Passwort für WinRM; nur relevant wenn <see cref="WinRmUsername"/> gesetzt ist.</summary>
public string WinRmPassword { get; init; } = string.Empty;
/// <summary>
/// WinRm = remote via PowerShell Remoting;
/// LocalCmd = lokal auf dem Sonic-Server (kein WinRM, App muss dort laufen);
/// HttpApi = REST.
/// </summary>
public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.WinRm; public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.WinRm;
/// <summary>Sonic-Installationsroot, z.B. "C:\Sonic\MQ10.0".</summary> /// <summary>Sonic-Installationsroot, z.B. "C:\Sonic\MQ10.0".</summary>
@@ -11,9 +11,11 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
/// ///
/// Modus = WinRm (Standard): /// Modus = WinRm (Standard):
/// PowerShell Remoting (Invoke-Command) → CMD stop/startcontainer auf dem Sonic-Server. /// 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: /// 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: /// Modus = HttpApi:
/// HTTP REST API mit automatischer Pfad-Erkennung. /// HTTP REST API mit automatischer Pfad-Erkennung.
@@ -21,6 +21,9 @@ public sealed class WinRmExecutor
_connection = connection; _connection = connection;
} }
private bool HasExplicitWinRmCredentials
=> !string.IsNullOrWhiteSpace(_connection.WinRmUsername);
public async Task<(bool Success, string? Error)> TestConnectionAsync( public async Task<(bool Success, string? Error)> TestConnectionAsync(
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
@@ -56,7 +59,7 @@ public sealed class WinRmExecutor
return sessionOk return sessionOk
? (true, null) ? (true, null)
: (false, $"WinRM-Verbindung fehlgeschlagen: {sessionError}"); : (false, sessionError ?? "WinRM-Verbindung fehlgeschlagen.");
} }
public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync( 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 (true, stdout, stderr.Length > 0 ? stderr : null);
} }
return (false, stdout.Length > 0 ? stdout : null, string rawError = stderr.Length > 0
stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}"); ? stderr
: $"PowerShell ExitCode={process.ExitCode}";
string error = _connection.ManagementMode == SonicManagementMode.WinRm
? FormatWinRmFailure(rawError)
: Truncate(rawError);
return (false, stdout.Length > 0 ? stdout : null, error);
} }
finally finally
{ {
@@ -134,24 +144,99 @@ public sealed class WinRmExecutor
/// <summary> /// <summary>
/// Remote-Payload als Base64 einbetten und per [scriptblock]::Create ausführen. /// Remote-Payload als Base64 einbetten und per [scriptblock]::Create ausführen.
/// Vermeidet verschachteltes ScriptBlock-Brace-Nesting und lokale $-Expansion. /// Vermeidet verschachteltes ScriptBlock-Brace-Nesting und lokale $-Expansion.
/// WinRM-Credentials: nur <see cref="SonicConnection.WinRmUsername"/> / WinRmPassword.
/// Leer = aktueller Windows-Benutzer (ohne -Credential).
/// </summary> /// </summary>
private string BuildRemoteScript(string host, string scriptBlock) private string BuildRemoteScript(string host, string scriptBlock)
{ {
string escapedPwd = _connection.Password.Replace("'", "''");
string escapedUser = _connection.Username.Replace("'", "''");
string escapedHost = host.Replace("'", "''"); string escapedHost = host.Replace("'", "''");
string remoteB64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptBlock)); 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 return
"$ErrorActionPreference = 'Stop'\n" + "WinRM-Authentifizierung fehlgeschlagen: Windows-Anmeldedaten falsch oder fehlend.\n" +
$"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n" + "Hinweis: Username/Password in appsettings sind Sonic-SMC-/Domain-Manager-Logins " +
$"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n" + "NICHT für WinRM/Windows.\n" +
$"$remoteB64 = '{remoteB64}'\n" + authHint + "\n" +
"$remoteScript = [System.Text.Encoding]::Unicode.GetString(" + "Alternativen:\n" +
"[System.Convert]::FromBase64String($remoteB64))\n" + " • ManagementMode=LocalCmd setzen und die App direkt auf dem Sonic-Server starten (kein WinRM).\n" +
"$sb = [scriptblock]::Create($remoteScript)\n" + " • WinRmUsername/WinRmPassword mit gültigem Windows-Konto befüllen.\n" +
$"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} " + $"Details: {truncated}";
"-Credential $cred -ScriptBlock $sb -ErrorAction Stop"; }
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( public static string ApplyScriptTemplate(
@@ -10,8 +10,13 @@
"Name": "DE-Test", "Name": "DE-Test",
"DomainName": "proalpha-test", "DomainName": "proalpha-test",
"ConnectionUrl": "tcp://dekun-painwbdet:13070", "ConnectionUrl": "tcp://dekun-painwbdet:13070",
// Sonic SMC / Domain Manager (NICHT für WinRM):
"Username": "Administrator", "Username": "Administrator",
"Password": "Administrator", "Password": "Administrator",
// Windows für WinRM: leer = aktueller Benutzer (z.B. gisler). Nie Sonic-SMC-Logins hier eintragen.
// Lokal auf dem Sonic-Server testen: ManagementMode auf "LocalCmd" setzen (kein WinRM nötig).
"WinRmUsername": "",
"WinRmPassword": "",
"ManagementMode": "WinRm", "ManagementMode": "WinRm",
"SonicHome": "C:\\Sonic\\MQ10.0", "SonicHome": "C:\\Sonic\\MQ10.0",
"KnownContainers": [ "KnownContainers": [