diff --git a/ZA.CoreService.ESBCertificateManager/Database/03_Seed.sql b/ZA.CoreService.ESBCertificateManager/Database/03_Seed.sql index 0a52c52..3f9b2ec 100644 --- a/ZA.CoreService.ESBCertificateManager/Database/03_Seed.sql +++ b/ZA.CoreService.ESBCertificateManager/Database/03_Seed.sql @@ -268,12 +268,35 @@ BEGIN 1; END; +/* Demo-Pfad für Zertifikat-Austausch (UNC server.p12) – immer sicherstellen */ +DECLARE @DemoDirectory [nvarchar](500) = + N'\\dekun-painwbdet\e$\proalpha-inwb\test\Containers\proalpha-test.ct-ZADBService\certs'; +DECLARE @DemoFileName [nvarchar](260) = N'server.p12'; + +UPDATE [dbo].[CertificateTarget] +SET + [TargetDirectory] = @DemoDirectory, + [TargetFileName] = @DemoFileName, + [BackupEnabled] = 1, + [BackupDirectoryName] = N'Backup', + [IsActive] = 1, + [ModifiedDateTime] = SYSUTCDATETIME(), + [ModifiedBy] = SUSER_SNAME() +WHERE [SonicContainerId] = @SonicContainerId + AND + ( + [TargetFileName] IN (N'server.crt', N'server.p12') + OR [TargetDirectory] LIKE N'C:\ESB\Certificates%' + ); + IF NOT EXISTS ( SELECT 1 FROM [dbo].[CertificateTarget] AS target WHERE target.[SonicContainerId] = @SonicContainerId - AND target.[TargetFileName] = N'server.crt' + AND target.[TargetDirectory] = @DemoDirectory + AND target.[TargetFileName] = @DemoFileName + AND target.[IsActive] = 1 ) BEGIN INSERT INTO [dbo].[CertificateTarget] @@ -290,8 +313,8 @@ BEGIN VALUES ( @SonicContainerId, - N'C:\ESB\Certificates', - N'server.crt', + @DemoDirectory, + @DemoFileName, 1, N'Backup', 1, diff --git a/ZA.CoreService.ESBCertificateManager/Database/04_EnsureDemoPath.sql b/ZA.CoreService.ESBCertificateManager/Database/04_EnsureDemoPath.sql new file mode 100644 index 0000000..370d71a --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Database/04_EnsureDemoPath.sql @@ -0,0 +1,106 @@ +/* +COMMENT: Stellt den Demo-Zertifikatspfad für DE-Test / ct-ZADBService sicher. + Kann allein gegen eine bestehende EsbZertifikatManager-DB laufen + (ohne Drop/Create). EnvironmentCode muss TEST sein (appsettings). +*/ +USE [EsbZertifikatManager]; +GO + +SET NOCOUNT ON; +SET XACT_ABORT ON; + +DECLARE @SonicContainerId [int]; +DECLARE @TargetDirectory [nvarchar](500) = + N'\\dekun-painwbdet\e$\proalpha-inwb\test\Containers\proalpha-test.ct-ZADBService\certs'; +DECLARE @TargetFileName [nvarchar](260) = N'server.p12'; + +SELECT @SonicContainerId = container.[SonicContainerId] +FROM [dbo].[SonicContainer] AS container +INNER JOIN [dbo].[SonicConnection] AS connection + ON connection.[SonicConnectionId] = container.[SonicConnectionId] +INNER JOIN [dbo].[Environment] AS environment + ON environment.[EnvironmentId] = connection.[EnvironmentId] +WHERE environment.[EnvironmentCode] = N'TEST' + AND connection.[ConnectionName] = N'DE-Test' + AND container.[ContainerName] = N'ct-ZADBService'; + +IF @SonicContainerId IS NULL +BEGIN + THROW 50010, + N'Demo-Pfad: Container ct-ZADBService / DE-Test / TEST nicht gefunden. Zuerst 03_Seed.sql ausführen.', + 1; +END; + +/* Alte Platzhalter am gleichen Container auf Demo-Pfad umbiegen */ +UPDATE [dbo].[CertificateTarget] +SET + [TargetDirectory] = @TargetDirectory, + [TargetFileName] = @TargetFileName, + [BackupEnabled] = 1, + [BackupDirectoryName] = N'Backup', + [IsActive] = 1, + [ModifiedDateTime] = SYSUTCDATETIME(), + [ModifiedBy] = SUSER_SNAME() +WHERE [SonicContainerId] = @SonicContainerId + AND + ( + [TargetFileName] IN (N'server.crt', N'server.p12') + OR [TargetDirectory] LIKE N'C:\ESB\Certificates%' + ); + +IF NOT EXISTS +( + SELECT 1 + FROM [dbo].[CertificateTarget] AS target + WHERE target.[SonicContainerId] = @SonicContainerId + AND target.[TargetDirectory] = @TargetDirectory + AND target.[TargetFileName] = @TargetFileName + AND target.[IsActive] = 1 +) +BEGIN + INSERT INTO [dbo].[CertificateTarget] + ( + [SonicContainerId], + [TargetDirectory], + [TargetFileName], + [BackupEnabled], + [BackupDirectoryName], + [IsActive], + [CreationDateTime], + [CreatedBy] + ) + VALUES + ( + @SonicContainerId, + @TargetDirectory, + @TargetFileName, + 1, + N'Backup', + 1, + SYSUTCDATETIME(), + SUSER_SNAME() + ); +END; + +SELECT + target.[CertificateTargetId], + company.[CompanyCode], + environment.[EnvironmentCode], + connection.[ConnectionName], + container.[ContainerName], + target.[TargetDirectory], + target.[TargetFileName], + target.[IsActive] +FROM [dbo].[CertificateTarget] AS target +INNER JOIN [dbo].[SonicContainer] AS container + ON container.[SonicContainerId] = target.[SonicContainerId] +INNER JOIN [dbo].[SonicConnection] AS connection + ON connection.[SonicConnectionId] = container.[SonicConnectionId] +INNER JOIN [dbo].[Environment] AS environment + ON environment.[EnvironmentId] = connection.[EnvironmentId] +INNER JOIN [dbo].[Company] AS company + ON company.[CompanyId] = container.[CompanyId] +WHERE container.[SonicContainerId] = @SonicContainerId; + +PRINT N'Demo-Pfad ist aktiv: \\dekun-painwbdet\e$\...\certs\server.p12'; +GO diff --git a/ZA.CoreService.ESBCertificateManager/Database/README.md b/ZA.CoreService.ESBCertificateManager/Database/README.md index 43cb0c1..9ba6664 100644 --- a/ZA.CoreService.ESBCertificateManager/Database/README.md +++ b/ZA.CoreService.ESBCertificateManager/Database/README.md @@ -32,9 +32,20 @@ cd Database\AlwaysEncrypted | Environment | TEST2, TEST, PROD | | SonicConnection | DE-Test → `tcp://dekun-painwbdet:13070`, Domain `proalpha-test`, Env TEST | | SonicContainer | ct-ZADBService (Company DE) | -| CertificateTarget | `C:\ESB\Certificates\server.crt` (Platzhalter) | +| CertificateTarget | Demo-UNC `\\dekun-painwbdet\e$\...\certs\server.p12` | | SonicCredential | **nicht** geseedet (Setup-Wizard / AE) | +## Demo-Pfad nachziehen (bestehende DB) + +Wenn der alte Platzhalter noch in SQL liegt: + +```text +04_EnsureDemoPath.sql +``` + +Zusätzlich: in `appsettings.json` ist `"Demo": { "Enabled": true, ... }` – +die App zeigt den Demo-Pfad auch ohne SQL-Update in der Zielliste (Kopieren testen). + ## Always Encrypted | Objekt | Name | diff --git a/ZA.CoreService.ESBCertificateManager/Form1.cs b/ZA.CoreService.ESBCertificateManager/Form1.cs index 013fbc3..4a96ce5 100644 --- a/ZA.CoreService.ESBCertificateManager/Form1.cs +++ b/ZA.CoreService.ESBCertificateManager/Form1.cs @@ -900,7 +900,7 @@ namespace ZA.CoreService.ESBCertificateManager Label hint = new Label { - Text = "Unterstützte Formate: .cer, .crt, .pem, .pfx", + Text = "Unterstützte Formate: .cer, .crt, .pem, .pfx, .p12", ForeColor = MutedTextColor, Font = new Font("Segoe UI", 8.5f), AutoSize = true, @@ -920,11 +920,11 @@ namespace ZA.CoreService.ESBCertificateManager { Title = "Zertifikatsdatei auswählen", Filter = - "Unterstützte Zertifikatsdateien (*.cer;*.crt;*.pem;*.pfx)|*.cer;*.crt;*.pem;*.pfx|" + + "Unterstützte Zertifikatsdateien (*.cer;*.crt;*.pem;*.pfx;*.p12)|*.cer;*.crt;*.pem;*.pfx;*.p12|" + "CER-Zertifikate (*.cer)|*.cer|" + "CRT-Zertifikate (*.crt)|*.crt|" + "PEM-Zertifikate (*.pem)|*.pem|" + - "PFX-Zertifikate (*.pfx)|*.pfx", + "PFX/P12-Zertifikate (*.pfx;*.p12)|*.pfx;*.p12", FilterIndex = 1, Multiselect = false, CheckFileExists = true, @@ -947,7 +947,7 @@ namespace ZA.CoreService.ESBCertificateManager MessageBox.Show( this, "Bitte wähle eine Zertifikatsdatei in einem der folgenden Formate aus:\n" + - ".cer, .crt, .pem oder .pfx", + ".cer, .crt, .pem, .pfx oder .p12", "Nicht unterstütztes Dateiformat", MessageBoxButtons.OK, MessageBoxIcon.Warning); @@ -971,15 +971,17 @@ namespace ZA.CoreService.ESBCertificateManager private CertificateInfo? TryReadSelectedCertificate(string selectedPath) { - bool isPfx = Path.GetExtension(selectedPath) - .Equals(".pfx", StringComparison.OrdinalIgnoreCase); + string extension = Path.GetExtension(selectedPath); + bool needsPassword = + extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".p12", StringComparison.OrdinalIgnoreCase); try { // Zuerst ohne Passwort versuchen return ReadCertificate(selectedPath, null); } - catch (CryptographicException) when (isPfx) + catch (CryptographicException) when (needsPassword) { // Passwort nur abfragen, wenn das Laden ohne Passwort fehlgeschlagen ist string? pfxPassword = PromptForPfxPassword(); @@ -1141,8 +1143,12 @@ namespace ZA.CoreService.ESBCertificateManager string extension = Path.GetExtension(certificatePath); - using X509Certificate2 certificate = + bool passwordProtected = extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".p12", StringComparison.OrdinalIgnoreCase); + + using X509Certificate2 certificate = + passwordProtected ? new X509Certificate2( certificatePath, pfxPassword, @@ -1245,7 +1251,8 @@ namespace ZA.CoreService.ESBCertificateManager ".cer", ".crt", ".pem", - ".pfx" + ".pfx", + ".p12" }; return supportedExtensions.Contains( @@ -1389,9 +1396,9 @@ namespace ZA.CoreService.ESBCertificateManager }); dgvTargets.Columns.Add("Target", "Ziel"); - dgvTargets.Columns.Add("Environment", "Umgebung"); - dgvTargets.Columns.Add("Container", "Container / Service"); - dgvTargets.Columns.Add("Tls", "TLS-Prüfung"); + dgvTargets.Columns.Add("Path", "Zertifikat-Pfad"); + dgvTargets.Columns.Add("Expiry", "Aktuelles Zertifikat"); + dgvTargets.Columns.Add("Container", "Neustart-Container"); dgvTargets.Columns.Add("Status", "Status"); dgvTargets.CurrentCellDirtyStateChanged += (_, _) => @@ -1527,10 +1534,12 @@ namespace ZA.CoreService.ESBCertificateManager "Bereitstellungsziele werden aus SQL geladen ...", isError: false); - _loadedTargets = + IReadOnlyList fromSql = await _deploymentTargetRepository.GetActiveAsync( _settings.EnvironmentCode); + _loadedTargets = EnsureDemoTarget(fromSql); + BindTargetsToGrid(_loadedTargets); if (_loadedTargets.Count == 0) @@ -1542,9 +1551,38 @@ namespace ZA.CoreService.ESBCertificateManager return; } + // Demo: erstes Ziel vorauswählen + if (dgvTargets.Rows.Count > 0) + { + dgvTargets.Rows[0].Cells["Selected"].Value = true; + } + SetStatus( - $"{_loadedTargets.Count} Bereitstellungsziel(e) aus SQL geladen.", + $"{_loadedTargets.Count} Bereitstellungsziel(e) geladen" + + (_settings.Demo.Enabled + ? $" · Demo: {Path.Combine(_settings.Demo.TargetDirectory, _settings.Demo.TargetFileName)}" + : string.Empty), isError: false); + + await ProbeTargetCertificatesAsync(); + } + catch (Exception ex) when (_settings.Demo.Enabled) + { + // Demo trotzdem möglich, wenn SQL kurz nicht erreichbar ist + _loadedTargets = EnsureDemoTarget([]); + BindTargetsToGrid(_loadedTargets); + + if (dgvTargets.Rows.Count > 0) + { + dgvTargets.Rows[0].Cells["Selected"].Value = true; + } + + SetStatus( + $"SQL-Ziele nicht geladen ({ex.Message}). " + + "Demo-Pfad ist trotzdem verfügbar.", + isError: true); + + await ProbeTargetCertificatesAsync(); } catch { @@ -1565,23 +1603,118 @@ namespace ZA.CoreService.ESBCertificateManager foreach (DeploymentTarget target in targets) { - int tlsPort = target.TlsPort is > 0 ? target.TlsPort.Value : 443; - string tls = string.IsNullOrWhiteSpace(target.TlsHost) - ? "—" - : $"{target.TlsHost}:{tlsPort}"; - int rowIndex = dgvTargets.Rows.Add( false, target.Name, - target.Environment, - target.ContainerName, - tls, + target.FullTargetPath, + "wird geprüft …", + string.IsNullOrWhiteSpace(target.ContainerName) + ? "—" + : target.ContainerName, "Bereit"); - dgvTargets.Rows[rowIndex].Tag = target; } } + private IReadOnlyList EnsureDemoTarget( + IReadOnlyList fromSql) + { + if (!_settings.Demo.Enabled) + { + return fromSql; + } + + string directory = _settings.Demo.TargetDirectory.Trim(); + string fileName = _settings.Demo.TargetFileName.Trim(); + + if (string.IsNullOrWhiteSpace(directory) + || string.IsNullOrWhiteSpace(fileName)) + { + return fromSql; + } + + bool alreadyPresent = fromSql.Any(target => + string.Equals( + target.TargetDirectory.TrimEnd('\\', '/'), + directory.TrimEnd('\\', '/'), + StringComparison.OrdinalIgnoreCase) + && string.Equals( + target.CertificateFileName, + fileName, + StringComparison.OrdinalIgnoreCase)); + + if (alreadyPresent) + { + return fromSql; + } + + List merged = [.. fromSql]; + + merged.Insert( + 0, + new DeploymentTarget + { + Id = -1, + Name = _settings.Demo.DisplayName, + Environment = _settings.EnvironmentCode, + IsActive = true, + TargetDirectory = directory, + CertificateFileName = fileName, + BackupEnabled = true, + BackupDirectoryName = "Backup", + ContainerName = _settings.Demo.ContainerName, + RestartType = RestartType.None, + SonicConnectionName = + _settings.Demo.SonicConnectionName, + RestartTimeoutSeconds = 180 + }); + + return merged; + } + + private async Task ProbeTargetCertificatesAsync() + { + CertificateProbeService probe = new(); + + foreach (DataGridViewRow row in dgvTargets.Rows) + { + if (row.Tag is not DeploymentTarget target) + { + continue; + } + + CertificateProbeResult result = await Task.Run( + () => probe.Probe(target.FullTargetPath)); + + if (IsDisposed || !dgvTargets.Columns.Contains("Expiry")) + { + return; + } + + if (result.Success && result.Certificate is not null) + { + string expiry = result.Certificate.ValidUntil + .ToLocalTime() + .ToString("dd.MM.yyyy"); + + row.Cells["Expiry"].Value = result.Certificate.IsCurrentlyValid + ? $"gültig bis {expiry}" + : $"abgelaufen {expiry}"; + continue; + } + + if (result.FileExists) + { + row.Cells["Expiry"].Value = "Datei da · Kennwort nötig"; + continue; + } + + row.Cells["Expiry"].Value = result.Success + ? "Datei fehlt" + : "nicht erreichbar"; + } + } + private List GetSelectedTargets() { List selected = []; diff --git a/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs b/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs index 7d86242..416ab06 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs @@ -11,6 +11,31 @@ public sealed class AppSettings public RuntimeSettings Runtime { get; set; } = new(); public AlwaysEncryptedSettings AlwaysEncrypted { get; set; } = new(); + + /// + /// Wenn aktiv, erscheint der Demo-UNC-Pfad auch ohne SQL-Update in der Zielliste. + /// + public DemoSettings Demo { get; set; } = new(); +} + +public sealed class DemoSettings +{ + public bool Enabled { get; set; } = true; + + public string TargetDirectory { get; set; } = + DemoCertificatePath.Directory; + + public string TargetFileName { get; set; } = + DemoCertificatePath.FileName; + + public string ContainerName { get; set; } = + "ct-ZADBService"; + + public string DisplayName { get; set; } = + "DE / Demo ZADBService"; + + public string SonicConnectionName { get; set; } = + "DE-Test"; } public sealed class DatabaseSettings diff --git a/ZA.CoreService.ESBCertificateManager/Models/DemoCertificatePath.cs b/ZA.CoreService.ESBCertificateManager/Models/DemoCertificatePath.cs new file mode 100644 index 0000000..68652cc --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Models/DemoCertificatePath.cs @@ -0,0 +1,15 @@ +namespace ZA.CoreService.ESBCertificateManager.Models; + +/// +/// Fester Demo-Pfad für den ersten Zertifikat-Austausch-Test (TEST / DE). +/// +public static class DemoCertificatePath +{ + public const string Directory = + @"\\dekun-painwbdet\e$\proalpha-inwb\test\Containers\proalpha-test.ct-ZADBService\certs"; + + public const string FileName = "server.p12"; + + public static string FullPath => + Path.Combine(Directory, FileName); +} diff --git a/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs b/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs index c45637d..9e3dad2 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs @@ -9,6 +9,10 @@ public sealed class DeploymentTarget public required string TargetDirectory { get; init; } public required string CertificateFileName { get; init; } + /// Vollständiger Dateipfad (UNC oder lokal) zur Zieldatei. + public string FullTargetPath => + Path.Combine(TargetDirectory, CertificateFileName); + public bool BackupEnabled { get; init; } = true; public string BackupDirectoryName { get; init; } = "Backup"; diff --git a/ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs b/ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs index 6ce5c2b..e9c0776 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs @@ -63,8 +63,11 @@ public sealed class CertificateTargetOption public required string CompanyCode { get; init; } + public string FullTargetPath => + Path.Combine(TargetDirectory, TargetFileName); + public override string ToString() { - return $"{CompanyCode} / {ContainerName}: {TargetDirectory}\\{TargetFileName}"; + return $"{CompanyCode} / {ContainerName}: {FullTargetPath}"; } } diff --git a/ZA.CoreService.ESBCertificateManager/Services/CertificateProbeService.cs b/ZA.CoreService.ESBCertificateManager/Services/CertificateProbeService.cs new file mode 100644 index 0000000..fe4a94b --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Services/CertificateProbeService.cs @@ -0,0 +1,302 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using ZA.CoreService.ESBCertificateManager.Models; + +namespace ZA.CoreService.ESBCertificateManager.Services; + +public sealed class CertificateProbeService +{ + public static readonly string[] SupportedExtensions = + [ + ".cer", + ".crt", + ".pem", + ".pfx", + ".p12" + ]; + + public CertificateProbeResult Probe( + string certificatePath, + Func? passwordPrompt = null) + { + string? normalized = NormalizePath(certificatePath); + + if (normalized is null) + { + return CertificateProbeResult.Failed( + "Kein gültiger Zertifikatspfad angegeben."); + } + + if (!IsSupportedExtension(normalized)) + { + return CertificateProbeResult.Failed( + "Nicht unterstütztes Format. Erlaubt: " + + string.Join(", ", SupportedExtensions)); + } + + if (!File.Exists(normalized)) + { + return CertificateProbeResult.Missing( + normalized, + "Am Zielpfad liegt noch keine Zertifikatsdatei."); + } + + try + { + CertificateInfo info = ReadCertificate(normalized, password: null); + return CertificateProbeResult.Found(normalized, info); + } + catch (CryptographicException) when (IsPasswordProtected(normalized)) + { + if (passwordPrompt is null) + { + return CertificateProbeResult.PasswordRequired(normalized); + } + + string? password = passwordPrompt.Invoke(); + + if (password is null) + { + return CertificateProbeResult.Failed( + "Zum Lesen der PFX/P12-Datei wird ein Kennwort benötigt."); + } + + try + { + CertificateInfo info = ReadCertificate(normalized, password); + return CertificateProbeResult.Found(normalized, info); + } + catch (CryptographicException) + { + return CertificateProbeResult.Failed( + "Das Kennwort ist falsch oder die Datei ist beschädigt."); + } + } + catch (Exception ex) + { + return CertificateProbeResult.Failed( + $"Zertifikat konnte nicht gelesen werden: {ex.Message}"); + } + } + + public static bool TrySplitTargetPath( + string fullPath, + out string directory, + out string fileName, + out string? error) + { + directory = string.Empty; + fileName = string.Empty; + error = null; + + string? normalized = NormalizePath(fullPath); + + if (normalized is null) + { + error = "Bitte einen vollständigen Pfad zur Zertifikatsdatei angeben."; + return false; + } + + if (!IsSupportedExtension(normalized)) + { + error = + "Dateiendung nicht unterstützt. Erlaubt: " + + string.Join(", ", SupportedExtensions); + return false; + } + + string? dir = Path.GetDirectoryName(normalized); + string file = Path.GetFileName(normalized); + + if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(file)) + { + error = + "Pfad muss Verzeichnis und Dateiname enthalten, z. B.\n" + + @"\\server\e$\...\certs\server.p12"; + return false; + } + + if (Path.GetFileName(file) != file) + { + error = "Dateiname darf keinen weiteren Verzeichnisanteil enthalten."; + return false; + } + + directory = dir; + fileName = file; + return true; + } + + public static string CombineTargetPath( + string directory, + string fileName) + { + return Path.Combine( + directory.Trim().TrimEnd('\\', '/'), + fileName.Trim()); + } + + public static bool IsSupportedExtension(string path) + { + string extension = Path.GetExtension(path); + return SupportedExtensions.Contains( + extension, + StringComparer.OrdinalIgnoreCase); + } + + private static bool IsPasswordProtected(string path) + { + string extension = Path.GetExtension(path); + return extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".p12", StringComparison.OrdinalIgnoreCase); + } + + private static string? NormalizePath(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + string trimmed = path.Trim().Trim('"'); + + if (trimmed.Length == 0) + { + return null; + } + + // UNC und absolute lokale Pfade akzeptieren + if (!Path.IsPathRooted(trimmed) + && !trimmed.StartsWith(@"\\", StringComparison.Ordinal)) + { + return null; + } + + return trimmed; + } + + private static CertificateInfo ReadCertificate( + string certificatePath, + string? password) + { + string extension = Path.GetExtension(certificatePath); + + using X509Certificate2 certificate = + IsPasswordProtected(certificatePath) + ? new X509Certificate2( + certificatePath, + password, + X509KeyStorageFlags.EphemeralKeySet) + : new X509Certificate2(certificatePath); + + string subject = certificate.GetNameInfo( + X509NameType.SimpleName, + forIssuer: false); + + string issuer = certificate.GetNameInfo( + X509NameType.SimpleName, + forIssuer: true); + + if (string.IsNullOrWhiteSpace(subject)) + { + subject = certificate.Subject; + } + + if (string.IsNullOrWhiteSpace(issuer)) + { + issuer = certificate.Issuer; + } + + string fingerprint = certificate.GetCertHashString( + HashAlgorithmName.SHA256); + + fingerprint = string.Join( + ":", + Enumerable.Range(0, fingerprint.Length / 2) + .Select(index => fingerprint.Substring(index * 2, 2))); + + DateTimeOffset validFrom = new(certificate.NotBefore); + DateTimeOffset validUntil = new(certificate.NotAfter); + DateTimeOffset now = DateTimeOffset.Now; + + return new CertificateInfo + { + Subject = subject, + Issuer = issuer, + FingerprintSha256 = fingerprint, + ValidFrom = validFrom, + ValidUntil = validUntil, + IsCurrentlyValid = now >= validFrom && now <= validUntil + }; + } +} + +public sealed class CertificateProbeResult +{ + public bool Success { get; init; } + + public bool FileExists { get; init; } + + public string Path { get; init; } = string.Empty; + + public string Message { get; init; } = string.Empty; + + public CertificateInfo? Certificate { get; init; } + + public static CertificateProbeResult Found( + string path, + CertificateInfo certificate) + { + string expiry = certificate.ValidUntil.ToLocalTime() + .ToString("dd.MM.yyyy HH:mm"); + + string validity = certificate.IsCurrentlyValid + ? "gültig" + : "abgelaufen / ungültig"; + + return new CertificateProbeResult + { + Success = true, + FileExists = true, + Path = path, + Certificate = certificate, + Message = + $"Zertifikat gefunden · Subject: {certificate.Subject} · " + + $"Ablauf: {expiry} ({validity})" + }; + } + + public static CertificateProbeResult Missing( + string path, + string message) + { + return new CertificateProbeResult + { + Success = true, + FileExists = false, + Path = path, + Message = message + }; + } + + public static CertificateProbeResult Failed(string message) + { + return new CertificateProbeResult + { + Success = false, + FileExists = false, + Message = message + }; + } + + public static CertificateProbeResult PasswordRequired(string path) + { + return new CertificateProbeResult + { + Success = false, + FileExists = true, + Path = path, + Message = "PFX/P12 vorhanden – Kennwort nötig zum Lesen des Ablaufs" + }; + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Services/LocalCertificateDeployer.cs b/ZA.CoreService.ESBCertificateManager/Services/LocalCertificateDeployer.cs index 1212692..4ce27ba 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/LocalCertificateDeployer.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/LocalCertificateDeployer.cs @@ -8,7 +8,8 @@ public sealed class LocalCertificateDeployer ".cer", ".crt", ".pem", - ".pfx" + ".pfx", + ".p12" }; public async Task DeployAsync( diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicContainerScanner.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicContainerScanner.cs deleted file mode 100644 index 3f10221..0000000 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicContainerScanner.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System.Diagnostics; -using System.Text; -using ZA.CoreService.ESBCertificateManager.Models; - -namespace ZA.CoreService.ESBCertificateManager.Services; - -public sealed class SonicContainerScanner -{ - private readonly RuntimeSettings _runtimeSettings; - - public SonicContainerScanner(RuntimeSettings runtimeSettings) - { - _runtimeSettings = runtimeSettings - ?? throw new ArgumentNullException(nameof(runtimeSettings)); - } - - public async Task ScanAsync( - SonicSystemOption system, - string userName, - string secret, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(system); - - if (string.IsNullOrWhiteSpace(userName)) - { - return ContainerScanResult.Failed( - "Der Sonic-Benutzername fehlt."); - } - - if (string.IsNullOrEmpty(secret)) - { - return ContainerScanResult.Failed( - "Das Sonic-Kennwort fehlt."); - } - - string javaPath = - PathResolver.ResolvePath( - _runtimeSettings.JavaExecutablePath); - - string libraryPath = - PathResolver.ResolvePath( - _runtimeSettings.SonicClientLibraryPath); - - string? toolsPath = ResolveToolsDirectory(); - - if (!File.Exists(javaPath)) - { - return ContainerScanResult.Failed( - $"Java wurde nicht gefunden: {javaPath}"); - } - - if (!Directory.Exists(libraryPath)) - { - return ContainerScanResult.Failed( - $"Der Sonic-JAR-Ordner wurde nicht gefunden: {libraryPath}"); - } - - if (toolsPath is null) - { - return ContainerScanResult.Failed( - "SonicMfContainerTool.class wurde nicht gefunden."); - } - - string[] jars = - Directory.GetFiles( - libraryPath, - "*.jar", - SearchOption.TopDirectoryOnly); - - if (jars.Length == 0) - { - return ContainerScanResult.Failed( - "Im Sonic-Client-Ordner wurden keine JAR-Dateien gefunden."); - } - - string classpath = - toolsPath - + Path.PathSeparator - + string.Join( - Path.PathSeparator, - jars.OrderBy( - Path.GetFileName, - StringComparer.OrdinalIgnoreCase)); - - ProcessStartInfo startInfo = new() - { - FileName = javaPath, - WorkingDirectory = toolsPath, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8, - CreateNoWindow = true - }; - - startInfo.ArgumentList.Add("-cp"); - startInfo.ArgumentList.Add(classpath); - startInfo.ArgumentList.Add("SonicMfContainerTool"); - startInfo.ArgumentList.Add("list"); - startInfo.ArgumentList.Add("--domain"); - startInfo.ArgumentList.Add(system.DomainName); - startInfo.ArgumentList.Add("--url"); - startInfo.ArgumentList.Add(system.ConnectionUrl); - startInfo.ArgumentList.Add("--user"); - startInfo.ArgumentList.Add(userName.Trim()); - startInfo.ArgumentList.Add("--timeout"); - startInfo.ArgumentList.Add("45"); - - startInfo.Environment["ESB_SONIC_PASSWORD"] = secret; - - using Process process = new() - { - StartInfo = startInfo - }; - - try - { - if (!process.Start()) - { - return ContainerScanResult.Failed( - "Der Java-Prozess konnte nicht gestartet werden."); - } - } - catch (Exception ex) - { - return ContainerScanResult.Failed( - $"Java konnte nicht gestartet werden: {ex.Message}"); - } - - Task standardOutputTask = - process.StandardOutput.ReadToEndAsync(); - - Task standardErrorTask = - process.StandardError.ReadToEndAsync(); - - using CancellationTokenSource timeoutSource = - CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken); - - timeoutSource.CancelAfter(TimeSpan.FromSeconds(50)); - - try - { - await process.WaitForExitAsync(timeoutSource.Token); - } - catch (OperationCanceledException) - when (!cancellationToken.IsCancellationRequested) - { - TryKill(process); - - return ContainerScanResult.Failed( - "Der Container-Scan hat das Zeitlimit überschritten."); - } - catch (OperationCanceledException) - { - TryKill(process); - throw; - } - - string standardOutput = - (await standardOutputTask).Trim(); - - string standardError = - (await standardErrorTask).Trim(); - - string combined = - string.Join( - Environment.NewLine, - new[] { standardOutput, standardError } - .Where(value => !string.IsNullOrWhiteSpace(value))); - - bool success = - process.ExitCode == 0 - && combined.Contains( - "OK:ContainerListEnd", - StringComparison.OrdinalIgnoreCase) - && !combined.Contains( - "ERROR:", - StringComparison.OrdinalIgnoreCase); - - if (!success) - { - return ContainerScanResult.Failed( - ExtractError(combined) - ?? $"Container-Scan fehlgeschlagen, ExitCode={process.ExitCode}."); - } - - List containers = []; - - foreach (string line in standardOutput.Split( - ['\r', '\n'], - StringSplitOptions.RemoveEmptyEntries)) - { - const string prefix = "CONTAINER="; - - if (!line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - string name = line[prefix.Length..].Trim(); - - if (name.Length > 0 - && !containers.Contains(name, StringComparer.OrdinalIgnoreCase)) - { - containers.Add(name); - } - } - - containers.Sort(StringComparer.OrdinalIgnoreCase); - - return ContainerScanResult.Successful(containers); - } - - private static string? ResolveToolsDirectory() - { - string baseDirectory = AppContext.BaseDirectory; - - string[] candidates = - [ - Path.Combine(baseDirectory, "Tools"), - Path.Combine(baseDirectory, "..", "..", "..", "Tools"), - Path.Combine( - Directory.GetCurrentDirectory(), - "Tools") - ]; - - foreach (string candidate in candidates) - { - string fullPath = Path.GetFullPath(candidate); - string classFile = Path.Combine( - fullPath, - "SonicMfContainerTool.class"); - - if (File.Exists(classFile)) - { - return fullPath; - } - } - - return null; - } - - private static string? ExtractError(string output) - { - foreach (string line in output.Split( - ['\r', '\n'], - StringSplitOptions.RemoveEmptyEntries)) - { - if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)) - { - return line["ERROR:".Length..].Trim(); - } - } - - return string.IsNullOrWhiteSpace(output) ? null : output; - } - - private static void TryKill(Process process) - { - try - { - if (!process.HasExited) - { - process.Kill(entireProcessTree: true); - } - } - catch - { - } - } -} - -public sealed class ContainerScanResult -{ - public bool Success { get; init; } - - public string Message { get; init; } = string.Empty; - - public IReadOnlyList Containers { get; init; } = []; - - public static ContainerScanResult Successful( - IReadOnlyList containers) - { - return new ContainerScanResult - { - Success = true, - Message = $"{containers.Count} Container gefunden.", - Containers = containers - }; - } - - public static ContainerScanResult Failed(string message) - { - return new ContainerScanResult - { - Success = false, - Message = message - }; - } -} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs index 6cee57b..565b039 100644 --- a/ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs +++ b/ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs @@ -1,16 +1,22 @@ using ZA.CoreService.ESBCertificateManager.Models; +using ZA.CoreService.ESBCertificateManager.Services; namespace ZA.CoreService.ESBCertificateManager.Setup; public sealed class AddTargetPathForm : Form { private readonly SetupCoordinator _coordinator; + private readonly CertificateProbeService _probeService = new(); + private readonly ComboBox _cmbContainer; - private readonly TextBox _txtDirectory; - private readonly TextBox _txtFileName; + private readonly TextBox _txtFullPath; private readonly TextBox _txtBackupFolder; private readonly CheckBox _chkBackup; + private readonly Label _lblProbeResult; + private readonly Button _btnProbe; private readonly Button _btnSave; + + private CertificateProbeResult? _lastProbe; private bool _running; public int? CreatedCertificateTargetId { get; private set; } @@ -27,15 +33,15 @@ public sealed class AddTargetPathForm : Form MaximizeBox = false; MinimizeBox = false; ShowInTaskbar = false; - ClientSize = new Size(560, 450); + ClientSize = new Size(640, 560); BackColor = SettingsUi.Background; ForeColor = SettingsUi.Text; Font = new Font("Segoe UI", 10f); Label title = new() { - Text = "Neuer Zertifikat-Pfad", - Location = new Point(28, 22), + Text = "Zertifikat-Datei-Pfad", + Location = new Point(28, 18), AutoSize = true, Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold), ForeColor = SettingsUi.Text @@ -44,23 +50,45 @@ public sealed class AddTargetPathForm : Form Label subtitle = new() { Text = - "Zielverzeichnis und Dateiname für die Zertifikatsablage.", - Location = new Point(30, 58), + "UNC- oder lokaler Dateipfad zur .cer/.crt/.pem/.pfx/.p12-Datei. " + + "Der Container ist nur das Neustart-Ziel.", + Location = new Point(30, 54), + MaximumSize = new Size(580, 40), AutoSize = true, ForeColor = SettingsUi.Muted }; Panel card = SettingsUi.CreateCard(); - card.Location = new Point(28, 95); - card.Size = new Size(504, 270); + card.Location = new Point(28, 100); + card.Size = new Size(584, 360); card.Paint += SettingsUi.PaintCardBorder; + _txtFullPath = SettingsUi.CreateTextBox(); + _txtFullPath.Text = DemoCertificatePath.FullPath; + _txtFullPath.PlaceholderText = DemoCertificatePath.FullPath; + + Button browse = SettingsUi.CreateGhostButton("…"); + browse.Size = new Size(40, 32); + browse.Click += (_, _) => BrowseForFile(); + + _btnProbe = SettingsUi.CreateGhostButton("Prüfen"); + _btnProbe.Size = new Size(100, 32); + _btnProbe.Click += (_, _) => RunProbe(); + + _lblProbeResult = new Label + { + AutoSize = false, + Size = new Size(548, 52), + ForeColor = SettingsUi.Muted, + Font = new Font("Segoe UI", 9f), + Text = + "Noch nicht geprüft. Bitte Pfad prüfen – bei vorhandener " + + "Datei wird der Ablauf angezeigt." + }; + _cmbContainer = SettingsUi.CreateComboBox(); - _txtDirectory = SettingsUi.CreateTextBox(); - _txtFileName = SettingsUi.CreateTextBox(); _txtBackupFolder = SettingsUi.CreateTextBox(); _txtBackupFolder.Text = "Backup"; - _txtFileName.Text = "server.crt"; _chkBackup = new CheckBox { @@ -71,21 +99,54 @@ public sealed class AddTargetPathForm : Form Font = new Font("Segoe UI", 9.5f) }; - AddField(card, "Container", _cmbContainer, 18, 18, 460); - AddField(card, "Zielverzeichnis", _txtDirectory, 18, 78, 460); - AddField(card, "Dateiname", _txtFileName, 18, 138, 220); - AddField(card, "Backup-Ordner", _txtBackupFolder, 258, 138, 220); + Label pathLabel = SettingsUi.CreateFieldLabel( + "Vollständiger Pfad zur Zertifikatsdatei"); + pathLabel.Location = new Point(18, 16); + _txtFullPath.Location = new Point(18, 38); + _txtFullPath.Width = 370; + browse.Location = new Point(394, 38); + _btnProbe.Location = new Point(440, 38); - _chkBackup.Location = new Point(18, 210); + _lblProbeResult.Location = new Point(18, 78); + + Label containerLabel = SettingsUi.CreateFieldLabel( + "Neustart-Ziel (Sonic-Container) – nicht der Dateipfad"); + containerLabel.Location = new Point(18, 140); + _cmbContainer.Location = new Point(18, 162); + _cmbContainer.Width = 548; + + Label hint = new() + { + Text = + "Nach dem Austausch wird dieser Container über MfApi " + + "neu gestartet. Der Pfad oben ist unabhängig davon.", + Location = new Point(18, 202), + MaximumSize = new Size(548, 36), + AutoSize = true, + ForeColor = SettingsUi.Muted, + Font = new Font("Segoe UI", 8.5f) + }; + + AddField(card, "Backup-Ordner", _txtBackupFolder, 18, 250, 220); + _chkBackup.Location = new Point(258, 272); card.Controls.Add(_chkBackup); + card.Controls.Add(pathLabel); + card.Controls.Add(_txtFullPath); + card.Controls.Add(browse); + card.Controls.Add(_btnProbe); + card.Controls.Add(_lblProbeResult); + card.Controls.Add(containerLabel); + card.Controls.Add(_cmbContainer); + card.Controls.Add(hint); + _btnSave = SettingsUi.CreatePrimaryButton("Pfad anlegen"); - _btnSave.Location = new Point(312, 385); + _btnSave.Location = new Point(392, 490); _btnSave.Size = new Size(220, 40); _btnSave.Click += async (_, _) => await SaveAsync(); Button cancel = SettingsUi.CreateGhostButton("Abbrechen"); - cancel.Location = new Point(190, 385); + cancel.Location = new Point(270, 490); cancel.Size = new Size(110, 40); cancel.Click += (_, _) => { @@ -103,6 +164,64 @@ public sealed class AddTargetPathForm : Form await LoadContainersAsync(preferredSonicContainerId); } + private void BrowseForFile() + { + using OpenFileDialog dialog = new() + { + Title = "Vorhandene Zertifikatsdatei wählen", + Filter = + "Zertifikate (*.cer;*.crt;*.pem;*.pfx;*.p12)|" + + "*.cer;*.crt;*.pem;*.pfx;*.p12|" + + "Alle Dateien (*.*)|*.*", + CheckFileExists = false, + Multiselect = false + }; + + if (dialog.ShowDialog(this) != DialogResult.OK) + { + return; + } + + _txtFullPath.Text = dialog.FileName; + RunProbe(); + } + + private void RunProbe() + { + _lastProbe = _probeService.Probe( + _txtFullPath.Text, + () => PromptForPassword(this)); + + if (!_lastProbe.Success) + { + _lblProbeResult.ForeColor = Color.FromArgb(220, 100, 100); + _lblProbeResult.Text = _lastProbe.Message; + return; + } + + if (!_lastProbe.FileExists) + { + _lblProbeResult.ForeColor = Color.FromArgb(230, 180, 80); + _lblProbeResult.Text = + _lastProbe.Message + + " Pfad kann trotzdem gespeichert werden " + + "(erstes Deployment)."; + return; + } + + CertificateInfo cert = _lastProbe.Certificate!; + string expiry = cert.ValidUntil.ToLocalTime() + .ToString("dd.MM.yyyy HH:mm"); + + _lblProbeResult.ForeColor = cert.IsCurrentlyValid + ? Color.FromArgb(90, 200, 140) + : Color.FromArgb(220, 100, 100); + + _lblProbeResult.Text = + $"Datei vorhanden · Subject: {cert.Subject}\n" + + $"Ablauf: {expiry} · Fingerprint: {cert.FingerprintSha256}"; + } + private async Task LoadContainersAsync(int? preferredSonicContainerId) { try @@ -151,13 +270,59 @@ public sealed class AddTargetPathForm : Form { MessageBox.Show( this, - "Bitte einen Container wählen.", + "Bitte ein Neustart-Ziel (Sonic-Container) wählen.", "Zertifikat-Pfad", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } + if (!CertificateProbeService.TrySplitTargetPath( + _txtFullPath.Text, + out string directory, + out string fileName, + out string? splitError)) + { + MessageBox.Show( + this, + splitError ?? "Ungültiger Pfad.", + "Zertifikat-Pfad", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + // Vor dem Speichern immer prüfen + RunProbe(); + + if (_lastProbe is null || !_lastProbe.Success) + { + MessageBox.Show( + this, + _lastProbe?.Message + ?? "Pfad konnte nicht geprüft werden.", + "Zertifikat-Pfad", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + if (!_lastProbe.FileExists) + { + DialogResult confirm = MessageBox.Show( + this, + "Am Ziel liegt noch keine Zertifikatsdatei.\n\n" + + "Pfad trotzdem speichern?", + "Datei fehlt", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (confirm != DialogResult.Yes) + { + return; + } + } + _running = true; _btnSave.Enabled = false; Cursor = Cursors.WaitCursor; @@ -167,8 +332,8 @@ public sealed class AddTargetPathForm : Form CreatedCertificateTargetId = await _coordinator.AddCertificateTargetAsync( container.SonicContainerId, - _txtDirectory.Text, - _txtFileName.Text, + directory, + fileName, _chkBackup.Checked, _txtBackupFolder.Text); @@ -192,6 +357,57 @@ public sealed class AddTargetPathForm : Form } } + private static string? PromptForPassword(IWin32Window owner) + { + using Form dialog = new() + { + Text = "PFX/P12-Kennwort", + FormBorderStyle = FormBorderStyle.FixedDialog, + StartPosition = FormStartPosition.CenterParent, + ClientSize = new Size(360, 140), + MaximizeBox = false, + MinimizeBox = false, + ShowInTaskbar = false, + BackColor = SettingsUi.Background, + ForeColor = SettingsUi.Text + }; + + Label label = new() + { + Text = "Kennwort für die Zertifikatsdatei:", + Location = new Point(16, 16), + AutoSize = true + }; + + TextBox txtPwd = new() + { + Location = new Point(16, 44), + Width = 320, + UseSystemPasswordChar = true + }; + + Button btnOk = SettingsUi.CreatePrimaryButton("OK"); + btnOk.Location = new Point(196, 90); + btnOk.Size = new Size(140, 32); + btnOk.DialogResult = DialogResult.OK; + + Button btnCancel = SettingsUi.CreateGhostButton("Abbrechen"); + btnCancel.Location = new Point(100, 90); + btnCancel.Size = new Size(90, 32); + btnCancel.DialogResult = DialogResult.Cancel; + + dialog.Controls.Add(label); + dialog.Controls.Add(txtPwd); + dialog.Controls.Add(btnOk); + dialog.Controls.Add(btnCancel); + dialog.AcceptButton = btnOk; + dialog.CancelButton = btnCancel; + + return dialog.ShowDialog(owner) == DialogResult.OK + ? txtPwd.Text + : null; + } + private static void AddField( Control parent, string label, diff --git a/ZA.CoreService.ESBCertificateManager/Setup/SelectContainersForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/SelectContainersForm.cs deleted file mode 100644 index 0c03cea..0000000 --- a/ZA.CoreService.ESBCertificateManager/Setup/SelectContainersForm.cs +++ /dev/null @@ -1,432 +0,0 @@ -using ZA.CoreService.ESBCertificateManager.Models; -using ZA.CoreService.ESBCertificateManager.Services; - -namespace ZA.CoreService.ESBCertificateManager.Setup; - -public sealed class SelectContainersForm : Form -{ - private readonly SetupCoordinator _coordinator; - private readonly SonicSystemOption _system; - private readonly SonicContainerScanner _scanner; - - private readonly ComboBox _cmbCompany; - private readonly CheckedListBox _lstContainers; - private readonly TextBox _txtManual; - private readonly TextBox _txtUser; - private readonly TextBox _txtPassword; - private readonly Label _lblStatus; - private readonly Button _btnScan; - private readonly Button _btnSave; - private bool _running; - - public int SavedCount { get; private set; } - - public SelectContainersForm( - SetupCoordinator coordinator, - SonicSystemOption system) - { - _coordinator = coordinator; - _system = system; - _scanner = new SonicContainerScanner(coordinator.Settings.Runtime); - - Text = "Container auswählen"; - StartPosition = FormStartPosition.CenterParent; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; - ShowInTaskbar = false; - ClientSize = new Size(640, 620); - BackColor = SettingsUi.Background; - ForeColor = SettingsUi.Text; - Font = new Font("Segoe UI", 10f); - - Label title = new() - { - Text = "Container für System", - Location = new Point(28, 20), - AutoSize = true, - Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold), - ForeColor = SettingsUi.Text - }; - - Label subtitle = new() - { - Text = - $"{system.ConnectionName} · {system.ConnectionUrl}" - + Environment.NewLine - + "Scan von Sonic oder manuell eintragen, dann übernehmen.", - Location = new Point(30, 54), - Size = new Size(580, 40), - ForeColor = SettingsUi.Muted - }; - - Panel card = SettingsUi.CreateCard(); - card.Location = new Point(28, 110); - card.Size = new Size(584, 430); - card.Paint += SettingsUi.PaintCardBorder; - - _cmbCompany = SettingsUi.CreateComboBox(); - _lstContainers = new CheckedListBox - { - BackColor = Color.FromArgb(12, 28, 48), - ForeColor = SettingsUi.Text, - BorderStyle = BorderStyle.FixedSingle, - CheckOnClick = true, - Font = new Font("Segoe UI", 10f) - }; - - _txtManual = SettingsUi.CreateTextBox(); - _txtUser = SettingsUi.CreateTextBox(); - _txtPassword = SettingsUi.CreateTextBox(); - _txtPassword.UseSystemPasswordChar = true; - - AddField(card, "Company", _cmbCompany, 18, 16, 540); - AddField(card, "Gefundene / gewählte Container", _lstContainers, 18, 76, 540, 150); - - Label manualLabel = SettingsUi.CreateFieldLabel("Manuell hinzufügen"); - manualLabel.Location = new Point(18, 240); - _txtManual.Location = new Point(18, 262); - _txtManual.Width = 400; - - Button btnAddManual = SettingsUi.CreateGhostButton("+"); - btnAddManual.Location = new Point(430, 260); - btnAddManual.Size = new Size(50, 32); - btnAddManual.Click += (_, _) => AddManualContainer(); - - Button btnSuggest = SettingsUi.CreateGhostButton("ct-ZADBService"); - btnSuggest.Location = new Point(490, 260); - btnSuggest.Size = new Size(68, 32); - btnSuggest.Font = new Font("Segoe UI", 7.5f, FontStyle.Bold); - btnSuggest.Click += (_, _) => - { - _txtManual.Text = "ct-ZADBService"; - AddManualContainer(); - }; - - AddField(card, "Sonic-Benutzer (für Scan)", _txtUser, 18, 308, 250); - AddField(card, "Kennwort", _txtPassword, 288, 308, 270); - - _btnScan = SettingsUi.CreatePrimaryButton("Von Sonic scannen"); - _btnScan.Location = new Point(18, 375); - _btnScan.Size = new Size(200, 36); - _btnScan.Click += async (_, _) => await ScanAsync(); - - _lblStatus = new Label - { - Location = new Point(230, 380), - Size = new Size(330, 30), - ForeColor = SettingsUi.Muted - }; - - card.Controls.Add(manualLabel); - card.Controls.Add(_txtManual); - card.Controls.Add(btnAddManual); - card.Controls.Add(btnSuggest); - card.Controls.Add(_btnScan); - card.Controls.Add(_lblStatus); - - _btnSave = SettingsUi.CreatePrimaryButton("Auswahl speichern"); - _btnSave.Location = new Point(352, 555); - _btnSave.Size = new Size(260, 42); - _btnSave.Click += async (_, _) => await SaveAsync(); - - Button skip = SettingsUi.CreateGhostButton("Später"); - skip.Location = new Point(230, 555); - skip.Size = new Size(110, 42); - skip.Click += (_, _) => - { - DialogResult = DialogResult.Cancel; - Close(); - }; - - Controls.Add(title); - Controls.Add(subtitle); - Controls.Add(card); - Controls.Add(skip); - Controls.Add(_btnSave); - - Shown += async (_, _) => await InitializeAsync(); - } - - private async Task InitializeAsync() - { - try - { - IReadOnlyList companies = - await _coordinator.LoadCompaniesAsync(); - - _cmbCompany.Items.Clear(); - - foreach (CompanyOption company in companies) - { - _cmbCompany.Items.Add(company); - } - - if (_cmbCompany.Items.Count > 0) - { - int deIndex = 0; - - for (int index = 0; index < companies.Count; index++) - { - if (string.Equals( - companies[index].CompanyCode, - "DE", - StringComparison.OrdinalIgnoreCase)) - { - deIndex = index; - break; - } - } - - _cmbCompany.SelectedIndex = deIndex; - } - - IReadOnlyList profiles = - await _coordinator.LoadCredentialsAsync( - _system.SonicConnectionId); - - SonicCredentialProfile? profile = - profiles.FirstOrDefault(item => item.IsDefault) - ?? profiles.FirstOrDefault(); - - if (profile is not null) - { - _txtUser.Text = profile.UserName; - _txtPassword.Text = profile.Secret; - _lblStatus.Text = - "Profil geladen — Scan möglich."; - _lblStatus.ForeColor = SettingsUi.Green; - } - else - { - _lblStatus.Text = - "Kein Profil — Benutzer/Kennwort für Scan eingeben oder manuell."; - _lblStatus.ForeColor = SettingsUi.Gold; - } - - // Bereits vorhandene Container vorselektieren - IReadOnlyList existing = - await _coordinator.LoadContainersAsync( - _system.SonicConnectionId); - - foreach (ContainerOption container in existing) - { - AddContainerName(container.ContainerName, check: true); - } - } - catch (Exception ex) - { - _lblStatus.Text = ex.Message; - _lblStatus.ForeColor = SettingsUi.Red; - } - } - - private void AddManualContainer() - { - string name = _txtManual.Text.Trim(); - - if (name.Length == 0) - { - return; - } - - AddContainerName(name, check: true); - _txtManual.Clear(); - _txtManual.Focus(); - } - - private void AddContainerName(string name, bool check) - { - for (int index = 0; index < _lstContainers.Items.Count; index++) - { - if (string.Equals( - _lstContainers.Items[index]?.ToString(), - name, - StringComparison.OrdinalIgnoreCase)) - { - _lstContainers.SetItemChecked(index, check); - return; - } - } - - int added = _lstContainers.Items.Add(name); - _lstContainers.SetItemChecked(added, check); - } - - private async Task ScanAsync() - { - if (_running) - { - return; - } - - _running = true; - _btnScan.Enabled = false; - _btnSave.Enabled = false; - Cursor = Cursors.WaitCursor; - _lblStatus.Text = "Scanne Domain…"; - _lblStatus.ForeColor = SettingsUi.Muted; - - try - { - ContainerScanResult result = - await _scanner.ScanAsync( - _system, - _txtUser.Text, - _txtPassword.Text); - - if (!result.Success) - { - _lblStatus.Text = result.Message; - _lblStatus.ForeColor = SettingsUi.Red; - return; - } - - foreach (string container in result.Containers) - { - AddContainerName(container, check: true); - } - - _lblStatus.Text = - result.Containers.Count == 0 - ? "Scan OK, aber keine Container gefunden — bitte manuell." - : $"{result.Containers.Count} Container gescannt und vorausgewählt."; - - _lblStatus.ForeColor = - result.Containers.Count == 0 - ? SettingsUi.Gold - : SettingsUi.Green; - } - catch (Exception ex) - { - _lblStatus.Text = ex.Message; - _lblStatus.ForeColor = SettingsUi.Red; - } - finally - { - _running = false; - _btnScan.Enabled = true; - _btnSave.Enabled = true; - Cursor = Cursors.Default; - } - } - - private async Task SaveAsync() - { - if (_running) - { - return; - } - - if (_cmbCompany.SelectedItem is not CompanyOption company) - { - MessageBox.Show( - this, - "Bitte eine Company wählen.", - "Container", - MessageBoxButtons.OK, - MessageBoxIcon.Warning); - return; - } - - List selected = _lstContainers.CheckedItems - .Cast() - .Select(item => item.ToString() ?? string.Empty) - .Where(name => name.Length > 0) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - - if (selected.Count == 0) - { - MessageBox.Show( - this, - "Bitte mindestens einen Container angehakt lassen " - + "oder manuell hinzufügen.", - "Container", - MessageBoxButtons.OK, - MessageBoxIcon.Warning); - return; - } - - _running = true; - _btnSave.Enabled = false; - Cursor = Cursors.WaitCursor; - - try - { - IReadOnlyList existing = - await _coordinator.LoadContainersAsync( - _system.SonicConnectionId); - - HashSet existingNames = existing - .Select(item => item.ContainerName) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - int created = 0; - - foreach (string containerName in selected) - { - if (existingNames.Contains(containerName)) - { - continue; - } - - await _coordinator.AddSonicContainerAsync( - company.CompanyId, - _system.SonicConnectionId, - containerName, - containerDisplayName: null, - restartTimeoutSeconds: 180); - - created++; - } - - SavedCount = created; - - MessageBox.Show( - this, - created == 0 - ? "Alle gewählten Container waren bereits vorhanden." - : $"{created} Container gespeichert und geprüft.", - "Prüfung OK", - MessageBoxButtons.OK, - MessageBoxIcon.Information); - - DialogResult = DialogResult.OK; - Close(); - } - catch (Exception ex) - { - MessageBox.Show( - this, - ex.Message, - "Container", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - } - finally - { - _running = false; - _btnSave.Enabled = true; - Cursor = Cursors.Default; - } - } - - private static void AddField( - Control parent, - string label, - Control input, - int x, - int y, - int width, - int height = 30) - { - Label fieldLabel = SettingsUi.CreateFieldLabel(label); - fieldLabel.Location = new Point(x, y); - input.Location = new Point(x, y + 22); - input.Width = width; - input.Height = height; - parent.Controls.Add(fieldLabel); - parent.Controls.Add(input); - } -} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs index cfa5235..ccc3aa1 100644 --- a/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs +++ b/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs @@ -1,4 +1,5 @@ using ZA.CoreService.ESBCertificateManager.Models; +using ZA.CoreService.ESBCertificateManager.Services; namespace ZA.CoreService.ESBCertificateManager.Setup; @@ -381,9 +382,7 @@ public sealed class SetupWizardForm : Form Panel toolbar = CreateToolbar( "+ Container hinzufügen", - async () => await AddContainerAsync(), - secondaryText: "Von Sonic scannen", - onSecondary: async () => await ScanContainersForSelectedSystemAsync()); + async () => await AddContainerAsync()); _lvContainers = SettingsUi.CreateListView(); _lvContainers.Dock = DockStyle.Fill; @@ -400,20 +399,22 @@ public sealed class SetupWizardForm : Form { _panelPaths = CreateContentPanel( "Zertifikat-Pfade", - "Zielverzeichnisse und Dateinamen für Deployments."); + "Dateipfade (UNC/lokal) zum Austausch. Container = nur Neustart-Ziel."); Panel toolbar = CreateToolbar( "+ Pfad hinzufügen", - async () => await AddPathAsync()); + async () => await AddPathAsync(), + "Pfad prüfen", + async () => await ProbeSelectedPathAsync()); _lvPaths = SettingsUi.CreateListView(); _lvPaths.Dock = DockStyle.Fill; _lvPaths.Columns.Add("Company", 70); - _lvPaths.Columns.Add("Container", 140); - _lvPaths.Columns.Add("Verzeichnis", 280); - _lvPaths.Columns.Add("Datei", 120); + _lvPaths.Columns.Add("Neustart-Container", 140); + _lvPaths.Columns.Add("Zertifikat-Pfad", 360); + _lvPaths.Columns.Add("Ablauf", 140); _lvPaths.Columns.Add("Backup", 80); - _lvPaths.Columns.Add("System", 140); + _lvPaths.Columns.Add("System", 120); FinishPanelLayout(_panelPaths, _lvPaths, toolbar); } @@ -644,13 +645,14 @@ public sealed class SetupWizardForm : Form await _coordinator.LoadCertificateTargetsAsync(); _lvPaths.Items.Clear(); + CertificateProbeService probe = new(); foreach (CertificateTargetOption target in targets) { ListViewItem item = new(target.CompanyCode); item.SubItems.Add(target.ContainerName); - item.SubItems.Add(target.TargetDirectory); - item.SubItems.Add(target.TargetFileName); + item.SubItems.Add(target.FullTargetPath); + item.SubItems.Add("…"); item.SubItems.Add( target.BackupEnabled ? target.BackupDirectoryName @@ -658,6 +660,11 @@ public sealed class SetupWizardForm : Form item.SubItems.Add(target.ConnectionName); item.Tag = target; _lvPaths.Items.Add(item); + + CertificateProbeResult result = await Task.Run( + () => probe.Probe(target.FullTargetPath)); + + item.SubItems[3].Text = FormatProbeExpiry(result); } } catch (Exception ex) @@ -666,6 +673,110 @@ public sealed class SetupWizardForm : Form } } + private async Task ProbeSelectedPathAsync() + { + if (_lvPaths.SelectedItems.Count == 0 + || _lvPaths.SelectedItems[0].Tag is not CertificateTargetOption target) + { + MessageBox.Show( + this, + "Bitte zuerst einen Zertifikat-Pfad in der Liste wählen.", + "Pfad prüfen", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + return; + } + + CertificateProbeService probe = new(); + CertificateProbeResult result = probe.Probe( + target.FullTargetPath, + () => PromptCertificatePassword(this)); + + _lvPaths.SelectedItems[0].SubItems[3].Text = + FormatProbeExpiry(result); + + MessageBoxIcon icon = result.Success && result.Certificate is not null + ? (result.Certificate.IsCurrentlyValid + ? MessageBoxIcon.Information + : MessageBoxIcon.Warning) + : MessageBoxIcon.Warning; + + MessageBox.Show( + this, + result.Message + "\n\n" + target.FullTargetPath, + "Pfad prüfen", + MessageBoxButtons.OK, + icon); + } + + private static string FormatProbeExpiry(CertificateProbeResult result) + { + if (result.Success && result.Certificate is not null) + { + return result.Certificate.ValidUntil + .ToLocalTime() + .ToString("dd.MM.yyyy HH:mm"); + } + + if (result.FileExists) + { + return "Kennwort nötig"; + } + + return result.Success ? "fehlt" : "n/a"; + } + + private static string? PromptCertificatePassword(IWin32Window owner) + { + using Form dialog = new() + { + Text = "PFX/P12-Kennwort", + FormBorderStyle = FormBorderStyle.FixedDialog, + StartPosition = FormStartPosition.CenterParent, + ClientSize = new Size(360, 140), + MaximizeBox = false, + MinimizeBox = false, + ShowInTaskbar = false, + BackColor = SettingsUi.Background, + ForeColor = SettingsUi.Text + }; + + Label label = new() + { + Text = "Kennwort für die Zertifikatsdatei:", + Location = new Point(16, 16), + AutoSize = true + }; + + TextBox txtPwd = new() + { + Location = new Point(16, 44), + Width = 320, + UseSystemPasswordChar = true + }; + + Button btnOk = SettingsUi.CreatePrimaryButton("OK"); + btnOk.Location = new Point(196, 90); + btnOk.Size = new Size(140, 32); + btnOk.DialogResult = DialogResult.OK; + + Button btnCancel = SettingsUi.CreateGhostButton("Abbrechen"); + btnCancel.Location = new Point(100, 90); + btnCancel.Size = new Size(90, 32); + btnCancel.DialogResult = DialogResult.Cancel; + + dialog.Controls.Add(label); + dialog.Controls.Add(txtPwd); + dialog.Controls.Add(btnOk); + dialog.Controls.Add(btnCancel); + dialog.AcceptButton = btnOk; + dialog.CancelButton = btnCancel; + + return dialog.ShowDialog(owner) == DialogResult.OK + ? txtPwd.Text + : null; + } + private async Task AddSystemAsync() { using AddSonicSystemForm dialog = new(_coordinator); @@ -706,78 +817,6 @@ public sealed class SetupWizardForm : Form "Prüfung OK", MessageBoxButtons.OK, MessageBoxIcon.Information); - - SonicSystemOption? createdSystem = _systems - .FirstOrDefault(system => - system.SonicConnectionId == createdId); - - if (createdSystem is null - && _lvSystems.SelectedItems.Count > 0) - { - createdSystem = - _lvSystems.SelectedItems[0].Tag as SonicSystemOption; - } - - if (createdSystem is null) - { - return; - } - - DialogResult next = MessageBox.Show( - this, - "Container jetzt per Sonic-Scan oder manuell zuordnen?", - "Container", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question); - - if (next != DialogResult.Yes) - { - return; - } - - using SelectContainersForm selectForm = - new(_coordinator, createdSystem); - - if (selectForm.ShowDialog(this) == DialogResult.OK) - { - await LoadContainersListAsync(); - await RefreshAllAsync(); - } - } - - private async Task ScanContainersForSelectedSystemAsync() - { - SonicSystemOption? system = null; - - if (_lvSystems.SelectedItems.Count > 0) - { - system = _lvSystems.SelectedItems[0].Tag as SonicSystemOption; - } - - system ??= _systems.FirstOrDefault(); - - if (system is null) - { - // Systeme-Liste ggf. nachladen - await LoadSystemsListAsync(); - system = _systems.FirstOrDefault(); - } - - if (system is null) - { - ShowWarning( - "Bitte zuerst ein Sonic-System anlegen oder auswählen."); - return; - } - - using SelectContainersForm selectForm = - new(_coordinator, system); - - if (selectForm.ShowDialog(this) == DialogResult.OK) - { - await LoadContainersListAsync(); - await RefreshAllAsync(); - } } private async Task AddContainerAsync() diff --git a/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java index 35b014f..bfc758d 100644 --- a/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java +++ b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java @@ -1,7 +1,5 @@ import java.lang.reflect.Method; import java.util.Hashtable; -import java.util.Iterator; -import java.util.Set; import javax.management.ObjectName; @@ -11,14 +9,13 @@ import javax.management.ObjectName; * Befehle: * validate - Anmeldung + lesender Container-Zugriff * restart - Container neu starten (stop/restart) - * list - Container der Domain auflisten * * javac -source 8 -target 8 SonicMfContainerTool.java */ public final class SonicMfContainerTool { public static final String TOOL_VERSION = - "2026-07-29-validate-restart-list"; + "2026-07-29-validate-restart"; public static void main(String[] args) { @@ -29,16 +26,15 @@ public final class SonicMfContainerTool Args parsedArguments = Args.parse(args); if (!"validate".equals(parsedArguments.command) - && !"restart".equals(parsedArguments.command) - && !"list".equals(parsedArguments.command)) + && !"restart".equals(parsedArguments.command)) { fail( "Usage: SonicMfContainerTool " - + " " + + " " + "--domain D " + "--url U " + "--user U " - + "[--container C] " + + "--container C " + "[--timeout SEC]"); return; } @@ -51,12 +47,7 @@ public final class SonicMfContainerTool return; } - boolean needsContainer = - "validate".equals(parsedArguments.command) - || "restart".equals(parsedArguments.command); - - if (needsContainer - && isBlank(parsedArguments.container)) + if (isBlank(parsedArguments.container)) { fail("container ist Pflicht fuer validate/restart."); return; @@ -82,13 +73,7 @@ public final class SonicMfContainerTool try { - if ("list".equals(parsedArguments.command)) - { - listContainers( - connector, - parsedArguments.domain); - } - else if ("validate".equals( + if ("validate".equals( parsedArguments.command)) { validateConnection( @@ -126,115 +111,6 @@ public final class SonicMfContainerTool } } - private static void listContainers( - Object connector, - String domain) - throws Exception - { - Method queryNamesMethod = - findMethod( - connector.getClass(), - "queryNames", - new Class[] - { - ObjectName.class, - javax.management.QueryExp.class - }); - - if (queryNamesMethod == null) - { - throw new IllegalStateException( - "queryNames(ObjectName, QueryExp) wurde nicht gefunden."); - } - - ObjectName pattern = - new ObjectName("*:ID=AGENT"); - - Object rawResult = queryNamesMethod.invoke( - connector, - new Object[] - { - pattern, - null - }); - - if (!(rawResult instanceof Set)) - { - throw new IllegalStateException( - "queryNames lieferte kein Set."); - } - - Set names = (Set) rawResult; - String domainPrefix = domain + "."; - int count = 0; - - System.out.println("OK:ContainerListBegin"); - - Iterator iterator = names.iterator(); - - while (iterator.hasNext()) - { - Object entry = iterator.next(); - - if (!(entry instanceof ObjectName)) - { - continue; - } - - ObjectName objectName = (ObjectName) entry; - String canonical = objectName.getCanonicalName(); - - if (canonical == null) - { - continue; - } - - // Erwartet: domain.containerName:ID=AGENT - if (!canonical.regionMatches( - true, - 0, - domainPrefix, - 0, - domainPrefix.length())) - { - continue; - } - - int colon = canonical.indexOf(':'); - - if (colon <= domainPrefix.length()) - { - continue; - } - - String containerName = - canonical.substring( - domainPrefix.length(), - colon); - - if (isBlank(containerName)) - { - continue; - } - - System.out.println( - "CONTAINER=" - + containerName.trim()); - - count++; - } - - System.out.println( - "OK:ContainerListEnd count=" - + count - + " domain=" - + domain - + " tool=" - + TOOL_VERSION); - - System.out.flush(); - } - private static Object connect( String url, String user, diff --git a/ZA.CoreService.ESBCertificateManager/appsettings.json b/ZA.CoreService.ESBCertificateManager/appsettings.json index aaa43a2..ea9755d 100644 --- a/ZA.CoreService.ESBCertificateManager/appsettings.json +++ b/ZA.CoreService.ESBCertificateManager/appsettings.json @@ -14,5 +14,13 @@ "CertificateThumbprint": "95E906CCC9C5F227969877CF3BD46D2AD7168853", "StoreName": "My", "StoreLocation": "CurrentUser" + }, + "Demo": { + "Enabled": true, + "TargetDirectory": "\\\\dekun-painwbdet\\e$\\proalpha-inwb\\test\\Containers\\proalpha-test.ct-ZADBService\\certs", + "TargetFileName": "server.p12", + "ContainerName": "ct-ZADBService", + "DisplayName": "DE / Demo ZADBService", + "SonicConnectionName": "DE-Test" } } \ No newline at end of file