big changes
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class DeploymentTargetRepository
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public DeploymentTargetRepository(string connectionString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der SQL-Connection-String ist nicht konfiguriert.",
|
||||
nameof(connectionString));
|
||||
}
|
||||
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DeploymentTarget>> GetActiveAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT
|
||||
target.CertificateTargetId,
|
||||
target.TargetDirectory,
|
||||
target.TargetFileName,
|
||||
target.BackupEnabled,
|
||||
target.BackupDirectoryName,
|
||||
target.BackupRetentionDays,
|
||||
target.IsActive AS TargetIsActive,
|
||||
|
||||
container.SonicContainerId,
|
||||
container.ContainerName,
|
||||
container.ContainerDisplayName,
|
||||
container.RestartTimeoutSeconds,
|
||||
|
||||
connection.ConnectionName,
|
||||
|
||||
service.ServiceCode,
|
||||
service.ServiceName,
|
||||
|
||||
environment.EnvironmentCode,
|
||||
environment.SortOrder AS EnvironmentSortOrder,
|
||||
|
||||
company.CompanyCode,
|
||||
country.CountryCode
|
||||
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.EsbService AS service
|
||||
ON service.EsbServiceId = container.EsbServiceId
|
||||
INNER JOIN dbo.Company AS company
|
||||
ON company.CompanyId = service.CompanyId
|
||||
LEFT JOIN dbo.Country AS country
|
||||
ON country.CountryId = service.CountryId
|
||||
INNER JOIN dbo.Environment AS environment
|
||||
ON environment.EnvironmentId = service.EnvironmentId
|
||||
WHERE target.IsActive = 1
|
||||
AND container.IsActive = 1
|
||||
AND connection.IsActive = 1
|
||||
AND service.IsActive = 1
|
||||
AND company.IsActive = 1
|
||||
AND environment.IsActive = 1
|
||||
AND
|
||||
(
|
||||
country.CountryId IS NULL
|
||||
OR country.IsActive = 1
|
||||
)
|
||||
ORDER BY
|
||||
company.CompanyCode,
|
||||
country.CountryCode,
|
||||
environment.SortOrder,
|
||||
service.ServiceCode,
|
||||
container.ContainerName,
|
||||
target.TargetDirectory,
|
||||
target.TargetFileName;
|
||||
""";
|
||||
|
||||
List<DeploymentTarget> targets = [];
|
||||
|
||||
await using SqlConnection connection = new(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command = new(sql, connection);
|
||||
|
||||
await using SqlDataReader reader =
|
||||
await command.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
int targetIdOrdinal =
|
||||
reader.GetOrdinal("CertificateTargetId");
|
||||
|
||||
int targetDirectoryOrdinal =
|
||||
reader.GetOrdinal("TargetDirectory");
|
||||
|
||||
int targetFileNameOrdinal =
|
||||
reader.GetOrdinal("TargetFileName");
|
||||
|
||||
int backupEnabledOrdinal =
|
||||
reader.GetOrdinal("BackupEnabled");
|
||||
|
||||
int backupDirectoryNameOrdinal =
|
||||
reader.GetOrdinal("BackupDirectoryName");
|
||||
|
||||
int backupRetentionDaysOrdinal =
|
||||
reader.GetOrdinal("BackupRetentionDays");
|
||||
|
||||
int targetIsActiveOrdinal =
|
||||
reader.GetOrdinal("TargetIsActive");
|
||||
|
||||
int containerNameOrdinal =
|
||||
reader.GetOrdinal("ContainerName");
|
||||
|
||||
int containerDisplayNameOrdinal =
|
||||
reader.GetOrdinal("ContainerDisplayName");
|
||||
|
||||
int restartTimeoutOrdinal =
|
||||
reader.GetOrdinal("RestartTimeoutSeconds");
|
||||
|
||||
int connectionNameOrdinal =
|
||||
reader.GetOrdinal("ConnectionName");
|
||||
|
||||
int serviceCodeOrdinal =
|
||||
reader.GetOrdinal("ServiceCode");
|
||||
|
||||
int serviceNameOrdinal =
|
||||
reader.GetOrdinal("ServiceName");
|
||||
|
||||
int environmentCodeOrdinal =
|
||||
reader.GetOrdinal("EnvironmentCode");
|
||||
|
||||
int environmentSortOrderOrdinal =
|
||||
reader.GetOrdinal("EnvironmentSortOrder");
|
||||
|
||||
int companyCodeOrdinal =
|
||||
reader.GetOrdinal("CompanyCode");
|
||||
|
||||
int countryCodeOrdinal =
|
||||
reader.GetOrdinal("CountryCode");
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
string containerName =
|
||||
reader.GetString(containerNameOrdinal);
|
||||
|
||||
string? containerDisplayName =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
containerDisplayNameOrdinal);
|
||||
|
||||
string serviceCode =
|
||||
reader.GetString(serviceCodeOrdinal);
|
||||
|
||||
string serviceName =
|
||||
reader.GetString(serviceNameOrdinal);
|
||||
|
||||
string companyCode =
|
||||
reader.GetString(companyCodeOrdinal);
|
||||
|
||||
string? countryCode =
|
||||
ReadNullableString(reader, countryCodeOrdinal);
|
||||
|
||||
string displayName =
|
||||
string.IsNullOrWhiteSpace(containerDisplayName)
|
||||
? containerName
|
||||
: containerDisplayName;
|
||||
|
||||
string locationCode =
|
||||
string.IsNullOrWhiteSpace(countryCode)
|
||||
? companyCode
|
||||
: $"{companyCode}/{countryCode}";
|
||||
|
||||
targets.Add(new DeploymentTarget
|
||||
{
|
||||
Id = reader.GetInt32(targetIdOrdinal),
|
||||
|
||||
Name =
|
||||
$"{locationCode} / {serviceName} / {displayName}",
|
||||
|
||||
Environment =
|
||||
reader.GetString(environmentCodeOrdinal),
|
||||
|
||||
IsActive =
|
||||
reader.GetBoolean(targetIsActiveOrdinal),
|
||||
|
||||
TargetDirectory =
|
||||
reader.GetString(targetDirectoryOrdinal),
|
||||
|
||||
CertificateFileName =
|
||||
reader.GetString(targetFileNameOrdinal),
|
||||
|
||||
BackupEnabled =
|
||||
reader.GetBoolean(backupEnabledOrdinal),
|
||||
|
||||
BackupDirectoryName =
|
||||
reader.GetString(backupDirectoryNameOrdinal),
|
||||
|
||||
BackupRetentionDays =
|
||||
ReadNullableInt32(
|
||||
reader,
|
||||
backupRetentionDaysOrdinal),
|
||||
|
||||
ContainerName = containerName,
|
||||
|
||||
RestartType = RestartType.SonicContainer,
|
||||
|
||||
SonicConnectionName =
|
||||
reader.GetString(connectionNameOrdinal),
|
||||
|
||||
RestartTimeoutSeconds =
|
||||
reader.GetInt32(restartTimeoutOrdinal),
|
||||
|
||||
TlsHost = string.Empty,
|
||||
|
||||
TlsPort = null,
|
||||
|
||||
TlsServerName = string.Empty,
|
||||
|
||||
ExpectedFingerprint = null,
|
||||
|
||||
SortOrder =
|
||||
reader.GetInt32(environmentSortOrderOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static string? ReadNullableString(
|
||||
SqlDataReader reader,
|
||||
int ordinal)
|
||||
{
|
||||
return reader.IsDBNull(ordinal)
|
||||
? null
|
||||
: reader.GetString(ordinal);
|
||||
}
|
||||
|
||||
private static int? ReadNullableInt32(
|
||||
SqlDataReader reader,
|
||||
int ordinal)
|
||||
{
|
||||
return reader.IsDBNull(ordinal)
|
||||
? null
|
||||
: reader.GetInt32(ordinal);
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,24 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
public sealed class DeploymentOrchestrator
|
||||
{
|
||||
private readonly RestartExecutor _restartExecutor;
|
||||
private readonly LocalCertificateDeployer _localCertificateDeployer;
|
||||
private readonly PreflightValidator _preflightValidator = new();
|
||||
private readonly AppSettings _settings;
|
||||
|
||||
public DeploymentOrchestrator(AppSettings settings)
|
||||
public DeploymentOrchestrator(
|
||||
AppSettings settings,
|
||||
IReadOnlyList<SonicConnection> sonicConnections)
|
||||
{
|
||||
_settings = settings;
|
||||
_restartExecutor = new RestartExecutor(settings.SonicConnections);
|
||||
|
||||
_restartExecutor =
|
||||
new RestartExecutor(sonicConnections);
|
||||
|
||||
_localCertificateDeployer =
|
||||
new LocalCertificateDeployer();
|
||||
}
|
||||
|
||||
|
||||
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||
=> _preflightValidator.ValidateRestartOnly(selectedTargets);
|
||||
|
||||
@@ -52,7 +61,75 @@ public sealed class DeploymentOrchestrator
|
||||
|
||||
return runResult;
|
||||
}
|
||||
public async Task<DeploymentRunResult> DeployAsync(
|
||||
string certificateFilePath,
|
||||
string certificateFingerprint,
|
||||
IReadOnlyList<DeploymentTarget> selectedTargets,
|
||||
IProgress<TargetProgressUpdate>? progress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(certificateFilePath))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Es wurde kein Zertifikatsdateipfad angegeben.",
|
||||
nameof(certificateFilePath));
|
||||
}
|
||||
|
||||
if (!File.Exists(certificateFilePath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"Die ausgewählte Zertifikatsdatei wurde nicht gefunden.",
|
||||
certificateFilePath);
|
||||
}
|
||||
|
||||
if (selectedTargets.Count == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Es wurde kein Bereitstellungsziel ausgewählt.",
|
||||
nameof(selectedTargets));
|
||||
}
|
||||
|
||||
DateTimeOffset startedAt = DateTimeOffset.Now;
|
||||
List<TargetStepResult> results = [];
|
||||
|
||||
using RunLogger logger = new(_settings.LogDirectory);
|
||||
|
||||
logger.Write(
|
||||
$"Deployment gestartet für {selectedTargets.Count} Ziel(e). " +
|
||||
$"Zertifikat={Path.GetFileName(certificateFilePath)}");
|
||||
|
||||
foreach (DeploymentTarget target in selectedTargets)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
TargetStepResult result = await DeployTargetAsync(
|
||||
certificateFilePath,
|
||||
target,
|
||||
logger,
|
||||
progress,
|
||||
cancellationToken);
|
||||
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
||||
|
||||
DeploymentRunResult runResult = new()
|
||||
{
|
||||
TargetResults = results,
|
||||
StartedAt = startedAt,
|
||||
FinishedAt = finishedAt,
|
||||
CertificateFilePath = certificateFilePath,
|
||||
CertificateFingerprint = certificateFingerprint
|
||||
};
|
||||
|
||||
logger.Write(
|
||||
$"Deployment beendet. Erfolg={runResult.OverallSuccess}; " +
|
||||
$"Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; " +
|
||||
$"Log={logger.LogFilePath}");
|
||||
|
||||
return runResult;
|
||||
}
|
||||
private async Task<TargetStepResult> RestartTargetOnlyAsync(
|
||||
DeploymentTarget target,
|
||||
RunLogger logger,
|
||||
@@ -88,6 +165,125 @@ public sealed class DeploymentOrchestrator
|
||||
progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, restartOk));
|
||||
return targetResult;
|
||||
}
|
||||
private async Task<TargetStepResult> DeployTargetAsync(
|
||||
string certificateFilePath,
|
||||
DeploymentTarget target,
|
||||
RunLogger logger,
|
||||
IProgress<TargetProgressUpdate>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<string> steps = [];
|
||||
DateTimeOffset targetStart = DateTimeOffset.Now;
|
||||
|
||||
try
|
||||
{
|
||||
progress?.Report(new TargetProgressUpdate(
|
||||
target.Id,
|
||||
$"Zertifikat wird nach '{target.TargetDirectory}' kopiert ...",
|
||||
false));
|
||||
|
||||
string deployedFilePath =
|
||||
await _localCertificateDeployer.DeployAsync(
|
||||
certificateFilePath,
|
||||
target.TargetDirectory,
|
||||
target.CertificateFileName,
|
||||
target.BackupEnabled,
|
||||
target.BackupDirectoryName,
|
||||
cancellationToken);
|
||||
|
||||
string copyStatus = $"Zertifikat kopiert: {Path.GetFileName(deployedFilePath)}";
|
||||
|
||||
steps.Add(copyStatus);
|
||||
logger.Write($"[{target.Name}] {copyStatus} | Ziel={deployedFilePath}");
|
||||
|
||||
progress?.Report(new TargetProgressUpdate(
|
||||
target.Id,
|
||||
copyStatus,
|
||||
true));
|
||||
|
||||
return new TargetStepResult
|
||||
{
|
||||
TargetId = target.Id,
|
||||
TargetName = target.Name,
|
||||
Success = true,
|
||||
StatusText = "Kopieren erfolgreich",
|
||||
Detail = deployedFilePath,
|
||||
Steps = steps,
|
||||
StartedAt = targetStart,
|
||||
FinishedAt = DateTimeOffset.Now,
|
||||
|
||||
CopySucceeded = true,
|
||||
RestartSucceeded = true,
|
||||
TlsSucceeded = true,
|
||||
ObservedFingerprint = null
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string errorStatus = $"Kopieren fehlgeschlagen: {ex.Message}";
|
||||
|
||||
steps.Add(errorStatus);
|
||||
logger.Write($"[{target.Name}] {errorStatus}");
|
||||
|
||||
progress?.Report(new TargetProgressUpdate(
|
||||
target.Id,
|
||||
errorStatus,
|
||||
false));
|
||||
|
||||
return new TargetStepResult
|
||||
{
|
||||
TargetId = target.Id,
|
||||
TargetName = target.Name,
|
||||
Success = false,
|
||||
StatusText = "Kopieren fehlgeschlagen",
|
||||
Detail = ex.Message,
|
||||
Steps = steps,
|
||||
StartedAt = targetStart,
|
||||
FinishedAt = DateTimeOffset.Now,
|
||||
|
||||
CopySucceeded = false,
|
||||
RestartSucceeded = false,
|
||||
TlsSucceeded = false,
|
||||
ObservedFingerprint = null
|
||||
};
|
||||
}
|
||||
}
|
||||
private static string? CreateCertificateBackup(string currentCertificatePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentCertificatePath) ||
|
||||
!File.Exists(currentCertificatePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string certificateDirectory =
|
||||
Path.GetDirectoryName(currentCertificatePath)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Kein Zielordner für Zertifikat gefunden: {currentCertificatePath}");
|
||||
|
||||
string backupDirectory = Path.Combine(certificateDirectory, "Backup");
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
|
||||
string fileNameWithoutExtension =
|
||||
Path.GetFileNameWithoutExtension(currentCertificatePath);
|
||||
|
||||
string extension = Path.GetExtension(currentCertificatePath);
|
||||
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
|
||||
string backupFileName =
|
||||
$"{fileNameWithoutExtension}_{timestamp}{extension}";
|
||||
|
||||
string backupPath = Path.Combine(backupDirectory, backupFileName);
|
||||
|
||||
File.Copy(currentCertificatePath, backupPath, overwrite: false);
|
||||
|
||||
return backupPath;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct TargetProgressUpdate(int TargetId, string StatusText, bool? SuccessHint);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public interface ISecretProvider
|
||||
{
|
||||
StoredCredential GetCredential(string credentialReference);
|
||||
}
|
||||
|
||||
public sealed record StoredCredential(
|
||||
string UserName,
|
||||
string Secret);
|
||||
@@ -0,0 +1,185 @@
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class LocalCertificateDeployer
|
||||
{
|
||||
private static readonly HashSet<string> CertificateExtensions =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".cer",
|
||||
".crt",
|
||||
".pem",
|
||||
".pfx"
|
||||
};
|
||||
|
||||
public async Task<string> DeployAsync(
|
||||
string sourceCertificatePath,
|
||||
string configuredTargetDirectory,
|
||||
string targetFileName,
|
||||
bool backupEnabled,
|
||||
string backupDirectoryName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sourceCertificatePath))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Es wurde keine Zertifikatsquelle angegeben.",
|
||||
nameof(sourceCertificatePath));
|
||||
}
|
||||
|
||||
if (!File.Exists(sourceCertificatePath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"Die ausgewählte Zertifikatsdatei wurde nicht gefunden.",
|
||||
sourceCertificatePath);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configuredTargetDirectory))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Zertifikats-Zielordner ist nicht konfiguriert.",
|
||||
nameof(configuredTargetDirectory));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(targetFileName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Zertifikats-Zieldateiname ist nicht konfiguriert.",
|
||||
nameof(targetFileName));
|
||||
}
|
||||
|
||||
if (Path.GetFileName(targetFileName) != targetFileName)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TargetFileName darf keinen Verzeichnispfad enthalten.");
|
||||
}
|
||||
|
||||
string targetDirectory = Path.GetFullPath(
|
||||
Path.IsPathRooted(configuredTargetDirectory)
|
||||
? configuredTargetDirectory
|
||||
: Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
configuredTargetDirectory));
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
|
||||
string destinationPath = Path.Combine(
|
||||
targetDirectory,
|
||||
targetFileName);
|
||||
|
||||
string temporaryPath = destinationPath + ".tmp";
|
||||
|
||||
try
|
||||
{
|
||||
if (backupEnabled)
|
||||
{
|
||||
BackupExistingCertificates(
|
||||
targetDirectory,
|
||||
backupDirectoryName,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await using (FileStream source = new(
|
||||
sourceCertificatePath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read))
|
||||
await using (FileStream temporaryTarget = new(
|
||||
temporaryPath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.None))
|
||||
{
|
||||
await source.CopyToAsync(
|
||||
temporaryTarget,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
File.Move(
|
||||
temporaryPath,
|
||||
destinationPath,
|
||||
overwrite: true);
|
||||
|
||||
return destinationPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
TryDeleteTemporaryFile(temporaryPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void BackupExistingCertificates(
|
||||
string targetDirectory,
|
||||
string configuredBackupDirectoryName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<string> filesToBackup = Directory
|
||||
.EnumerateFiles(
|
||||
targetDirectory,
|
||||
"*",
|
||||
SearchOption.TopDirectoryOnly)
|
||||
.Where(path =>
|
||||
CertificateExtensions.Contains(
|
||||
Path.GetExtension(path)))
|
||||
.ToList();
|
||||
|
||||
if (filesToBackup.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string backupDirectoryName =
|
||||
string.IsNullOrWhiteSpace(configuredBackupDirectoryName)
|
||||
? "Backup"
|
||||
: configuredBackupDirectoryName.Trim();
|
||||
|
||||
if (Path.GetFileName(backupDirectoryName)
|
||||
!= backupDirectoryName)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"BackupDirectoryName darf keinen Verzeichnispfad enthalten.");
|
||||
}
|
||||
|
||||
string backupDirectory = Path.Combine(
|
||||
targetDirectory,
|
||||
backupDirectoryName,
|
||||
DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"));
|
||||
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
|
||||
foreach (string certificateFile in filesToBackup)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string backupPath = Path.Combine(
|
||||
backupDirectory,
|
||||
Path.GetFileName(certificateFile));
|
||||
|
||||
File.Copy(
|
||||
certificateFile,
|
||||
backupPath,
|
||||
overwrite: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDeleteTemporaryFile(
|
||||
string temporaryPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(temporaryPath))
|
||||
{
|
||||
File.Delete(temporaryPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Der ursprüngliche Fehler darf nicht verdeckt werden.
|
||||
// TODO: Aufräumfehler später separat protokollieren.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Text.Json;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class LocalSetupSelectionStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions =
|
||||
new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly string _filePath;
|
||||
|
||||
public LocalSetupSelectionStore()
|
||||
{
|
||||
string directory = Path.Combine(
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.LocalApplicationData),
|
||||
"ZA.CoreService.ESBCertificateManager");
|
||||
|
||||
_filePath = Path.Combine(
|
||||
directory,
|
||||
"setup-selection.json");
|
||||
}
|
||||
|
||||
public LocalSetupSelection? Load()
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json =
|
||||
File.ReadAllText(_filePath);
|
||||
|
||||
return JsonSerializer.Deserialize<LocalSetupSelection>(
|
||||
json,
|
||||
JsonOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Eine beschädigte lokale Auswahl darf den Start
|
||||
// nicht verhindern. Das Setup wird erneut angezeigt.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(
|
||||
LocalSetupSelection selection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(selection);
|
||||
|
||||
string? directory =
|
||||
Path.GetDirectoryName(_filePath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Der lokale Setup-Ordner konnte nicht bestimmt werden.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
string json =
|
||||
JsonSerializer.Serialize(
|
||||
selection,
|
||||
JsonOptions);
|
||||
|
||||
File.WriteAllText(
|
||||
_filePath,
|
||||
json);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
if (File.Exists(_filePath))
|
||||
{
|
||||
File.Delete(_filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,26 @@ public static class PathResolver
|
||||
return Path.GetFullPath(path);
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, path));
|
||||
return Path.GetFullPath(
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
path));
|
||||
}
|
||||
|
||||
public static string ResolveRequiredFile(
|
||||
string configuredPath,
|
||||
string description)
|
||||
{
|
||||
string resolvedPath =
|
||||
ResolvePath(configuredPath);
|
||||
|
||||
if (!File.Exists(resolvedPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"{description} wurde nicht gefunden.",
|
||||
resolvedPath);
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
public sealed class RestartExecutor
|
||||
{
|
||||
private readonly IReadOnlyList<SonicConnection> _sonicConnections;
|
||||
private readonly ISecretProvider _secretProvider;
|
||||
|
||||
public RestartExecutor(IReadOnlyList<SonicConnection> sonicConnections)
|
||||
public RestartExecutor(
|
||||
IReadOnlyList<SonicConnection> sonicConnections)
|
||||
{
|
||||
_sonicConnections = sonicConnections;
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Status, string? Detail)> ExecuteAsync(
|
||||
DeploymentTarget target,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -29,7 +30,8 @@ public sealed class RestartExecutor
|
||||
$"Ziel '{target.Name}': SonicConnection '{target.SonicConnectionName}' fehlt in appsettings.");
|
||||
}
|
||||
|
||||
using SonicManagementClient client = new(connection);
|
||||
using SonicManagementClient client =
|
||||
new(connection);
|
||||
return await client.RestartContainerAsync(target.ContainerName, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.Diagnostics;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class RuntimeEnvironmentValidator
|
||||
{
|
||||
public async Task<RuntimeValidationResult> ValidateAsync(
|
||||
RuntimeSettings settings,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<string> issues = [];
|
||||
|
||||
string javaExecutablePath =
|
||||
PathResolver.ResolvePath(
|
||||
settings.JavaExecutablePath);
|
||||
|
||||
string sonicClientLibraryPath =
|
||||
PathResolver.ResolvePath(
|
||||
settings.SonicClientLibraryPath);
|
||||
|
||||
string javaVersion = string.Empty;
|
||||
|
||||
if (!File.Exists(javaExecutablePath))
|
||||
{
|
||||
issues.Add(
|
||||
$"Java wurde nicht gefunden: {javaExecutablePath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
javaVersion = await ReadJavaVersionAsync(
|
||||
javaExecutablePath,
|
||||
cancellationToken);
|
||||
|
||||
if (!ContainsJava8Version(javaVersion))
|
||||
{
|
||||
issues.Add(
|
||||
"Die gefundene Java-Version ist nicht Java 8. " +
|
||||
$"Ausgabe: {javaVersion}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
issues.Add(
|
||||
$"Java konnte nicht ausgeführt werden: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!Directory.Exists(sonicClientLibraryPath))
|
||||
{
|
||||
issues.Add(
|
||||
"Der Sonic-Client-Bibliotheksordner wurde " +
|
||||
$"nicht gefunden: {sonicClientLibraryPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
string[] requiredJarNames =
|
||||
[
|
||||
"mgmt_client.jar",
|
||||
"mfcontext.jar"
|
||||
];
|
||||
|
||||
foreach (string requiredJarName in requiredJarNames)
|
||||
{
|
||||
string requiredJarPath = Path.Combine(
|
||||
sonicClientLibraryPath,
|
||||
requiredJarName);
|
||||
|
||||
if (!File.Exists(requiredJarPath))
|
||||
{
|
||||
issues.Add(
|
||||
$"Erforderliche Sonic-Bibliothek fehlt: " +
|
||||
$"{requiredJarPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new RuntimeValidationResult
|
||||
{
|
||||
IsValid = issues.Count == 0,
|
||||
JavaExecutablePath = javaExecutablePath,
|
||||
SonicClientLibraryPath = sonicClientLibraryPath,
|
||||
JavaVersion = javaVersion,
|
||||
Issues = issues
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<string> ReadJavaVersionAsync(
|
||||
string javaExecutablePath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = javaExecutablePath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add("-version");
|
||||
|
||||
using Process process = new()
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Der Java-Prozess konnte nicht gestartet werden.");
|
||||
}
|
||||
|
||||
Task<string> outputTask =
|
||||
process.StandardOutput.ReadToEndAsync(
|
||||
cancellationToken);
|
||||
|
||||
Task<string> errorTask =
|
||||
process.StandardError.ReadToEndAsync(
|
||||
cancellationToken);
|
||||
|
||||
await process.WaitForExitAsync(
|
||||
cancellationToken);
|
||||
|
||||
string output = await outputTask;
|
||||
string error = await errorTask;
|
||||
|
||||
string combined = string.Join(
|
||||
Environment.NewLine,
|
||||
new[] { output, error }
|
||||
.Where(value =>
|
||||
!string.IsNullOrWhiteSpace(value)));
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"java -version endete mit Code {process.ExitCode}. " +
|
||||
combined);
|
||||
}
|
||||
|
||||
return combined.Trim();
|
||||
}
|
||||
|
||||
private static bool ContainsJava8Version(
|
||||
string javaVersionOutput)
|
||||
{
|
||||
return javaVersionOutput.Contains(
|
||||
"\"1.8.",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| javaVersionOutput.Contains(
|
||||
"version 8",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RuntimeValidationResult
|
||||
{
|
||||
public bool IsValid { get; init; }
|
||||
|
||||
public string JavaExecutablePath { get; init; } =
|
||||
string.Empty;
|
||||
|
||||
public string SonicClientLibraryPath { get; init; } =
|
||||
string.Empty;
|
||||
|
||||
public string JavaVersion { get; init; } =
|
||||
string.Empty;
|
||||
|
||||
public IReadOnlyList<string> Issues { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class SonicConnectionRepository
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public SonicConnectionRepository(
|
||||
string connectionString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der SQL-Connection-String ist nicht konfiguriert.",
|
||||
nameof(connectionString));
|
||||
}
|
||||
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DatabaseSonicConnection>>
|
||||
GetActiveAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT
|
||||
SonicConnectionId,
|
||||
ConnectionName,
|
||||
ManagementHost,
|
||||
ManagementPort,
|
||||
ConnectionProtocol,
|
||||
DomainName,
|
||||
CredentialUserName,
|
||||
CredentialReference,
|
||||
CredentialSecret,
|
||||
JavaHomePath,
|
||||
SonicHomePath,
|
||||
Notes,
|
||||
IsActive
|
||||
FROM dbo.SonicConnection
|
||||
WHERE IsActive = 1
|
||||
ORDER BY ConnectionName;
|
||||
""";
|
||||
|
||||
List<DatabaseSonicConnection> connections = [];
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
await using SqlDataReader reader =
|
||||
await command.ExecuteReaderAsync(
|
||||
cancellationToken);
|
||||
|
||||
int idOrdinal =
|
||||
reader.GetOrdinal("SonicConnectionId");
|
||||
|
||||
int nameOrdinal =
|
||||
reader.GetOrdinal("ConnectionName");
|
||||
|
||||
int hostOrdinal =
|
||||
reader.GetOrdinal("ManagementHost");
|
||||
|
||||
int portOrdinal =
|
||||
reader.GetOrdinal("ManagementPort");
|
||||
|
||||
int protocolOrdinal =
|
||||
reader.GetOrdinal("ConnectionProtocol");
|
||||
|
||||
int domainOrdinal =
|
||||
reader.GetOrdinal("DomainName");
|
||||
|
||||
int userNameOrdinal =
|
||||
reader.GetOrdinal("CredentialUserName");
|
||||
|
||||
int credentialReferenceOrdinal =
|
||||
reader.GetOrdinal("CredentialReference");
|
||||
|
||||
int credentialSecretOrdinal =
|
||||
reader.GetOrdinal("CredentialSecret");
|
||||
|
||||
int javaHomeOrdinal =
|
||||
reader.GetOrdinal("JavaHomePath");
|
||||
|
||||
int sonicHomeOrdinal =
|
||||
reader.GetOrdinal("SonicHomePath");
|
||||
|
||||
int notesOrdinal =
|
||||
reader.GetOrdinal("Notes");
|
||||
|
||||
int isActiveOrdinal =
|
||||
reader.GetOrdinal("IsActive");
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
connections.Add(
|
||||
new DatabaseSonicConnection
|
||||
{
|
||||
Id = reader.GetInt32(idOrdinal),
|
||||
|
||||
Name =
|
||||
reader.GetString(nameOrdinal),
|
||||
|
||||
ManagementHost =
|
||||
reader.GetString(hostOrdinal),
|
||||
|
||||
ManagementPort =
|
||||
ReadNullableInt32(
|
||||
reader,
|
||||
portOrdinal),
|
||||
|
||||
ConnectionProtocol =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
protocolOrdinal),
|
||||
|
||||
DomainName =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
domainOrdinal),
|
||||
|
||||
CredentialUserName =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
userNameOrdinal),
|
||||
|
||||
CredentialReference =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
credentialReferenceOrdinal),
|
||||
|
||||
CredentialSecret =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
credentialSecretOrdinal),
|
||||
|
||||
JavaHomePath =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
javaHomeOrdinal),
|
||||
|
||||
SonicHomePath =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
sonicHomeOrdinal),
|
||||
|
||||
Notes =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
notesOrdinal),
|
||||
|
||||
IsActive =
|
||||
reader.GetBoolean(
|
||||
isActiveOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
return connections;
|
||||
}
|
||||
|
||||
public async Task SaveCredentialsAsync(
|
||||
int sonicConnectionId,
|
||||
string credentialUserName,
|
||||
string credentialSecret,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sonicConnectionId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sonicConnectionId),
|
||||
"Die SonicConnectionId ist ungültig.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(
|
||||
credentialUserName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Sonic-Benutzername fehlt.",
|
||||
nameof(credentialUserName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(
|
||||
credentialSecret))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das Sonic-Kennwort fehlt.",
|
||||
nameof(credentialSecret));
|
||||
}
|
||||
|
||||
string normalizedUserName =
|
||||
credentialUserName.Trim();
|
||||
|
||||
if (normalizedUserName.Length > 256)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Sonic-Benutzername darf maximal 256 Zeichen enthalten.",
|
||||
nameof(credentialUserName));
|
||||
}
|
||||
|
||||
if (credentialSecret.Length > 512)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das Sonic-Kennwort darf maximal 512 Zeichen enthalten.",
|
||||
nameof(credentialSecret));
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
UPDATE dbo.SonicConnection
|
||||
SET
|
||||
CredentialUserName = @CredentialUserName,
|
||||
CredentialSecret = @CredentialSecret,
|
||||
UpdatedAtUtc = SYSUTCDATETIME()
|
||||
WHERE SonicConnectionId = @SonicConnectionId
|
||||
AND IsActive = 1;
|
||||
""";
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialUserName",
|
||||
SqlDbType.NVarChar,
|
||||
256)
|
||||
{
|
||||
Value = normalizedUserName
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialSecret",
|
||||
SqlDbType.NVarChar,
|
||||
512)
|
||||
{
|
||||
Value = credentialSecret
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
int affectedRows =
|
||||
await command.ExecuteNonQueryAsync(
|
||||
cancellationToken);
|
||||
|
||||
if (affectedRows != 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Die aktive Sonic-Verbindung wurde nicht eindeutig gefunden.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearCredentialsAsync(
|
||||
int sonicConnectionId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sonicConnectionId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sonicConnectionId),
|
||||
"Die SonicConnectionId ist ungültig.");
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
UPDATE dbo.SonicConnection
|
||||
SET
|
||||
CredentialUserName = NULL,
|
||||
CredentialSecret = NULL,
|
||||
UpdatedAtUtc = SYSUTCDATETIME()
|
||||
WHERE SonicConnectionId = @SonicConnectionId;
|
||||
""";
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
int affectedRows =
|
||||
await command.ExecuteNonQueryAsync(
|
||||
cancellationToken);
|
||||
|
||||
if (affectedRows != 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Die Sonic-Verbindung wurde nicht eindeutig gefunden.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ReadNullableString(
|
||||
SqlDataReader reader,
|
||||
int ordinal)
|
||||
{
|
||||
return reader.IsDBNull(ordinal)
|
||||
? null
|
||||
: reader.GetString(ordinal);
|
||||
}
|
||||
|
||||
private static int? ReadNullableInt32(
|
||||
SqlDataReader reader,
|
||||
int ordinal)
|
||||
{
|
||||
return reader.IsDBNull(ordinal)
|
||||
? null
|
||||
: reader.GetInt32(ordinal);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Liefert Ziele nur aus appsettings KnownContainers (kein Live-Ping/List).
|
||||
/// </summary>
|
||||
public sealed class SonicContainerDiscovery
|
||||
{
|
||||
private readonly IReadOnlyList<SonicConnection> _connections;
|
||||
|
||||
public SonicContainerDiscovery(IReadOnlyList<SonicConnection> connections)
|
||||
{
|
||||
_connections = connections;
|
||||
}
|
||||
|
||||
public bool HasConnections => _connections.Count > 0;
|
||||
public IReadOnlyList<SonicConnection> Connections => _connections;
|
||||
|
||||
public IReadOnlyList<DeploymentTarget> BuildTargetsFromConfig()
|
||||
{
|
||||
List<DeploymentTarget> result = [];
|
||||
int id = 1;
|
||||
|
||||
foreach (SonicConnection connection in _connections)
|
||||
{
|
||||
foreach (string containerName in connection.KnownContainers
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n)))
|
||||
{
|
||||
result.Add(new DeploymentTarget
|
||||
{
|
||||
Id = id++,
|
||||
Name = $"{connection.Name} / {containerName}",
|
||||
Environment = connection.DomainName,
|
||||
IsActive = true,
|
||||
TargetDirectory = string.Empty,
|
||||
CertificateFileName = string.Empty,
|
||||
ContainerName = containerName.Trim(),
|
||||
RestartType = RestartType.SonicContainer,
|
||||
SonicConnectionName = connection.Name,
|
||||
SortOrder = id * 10
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class SonicCredentialTester
|
||||
{
|
||||
private readonly RuntimeSettings _runtimeSettings;
|
||||
|
||||
public SonicCredentialTester(
|
||||
RuntimeSettings runtimeSettings)
|
||||
{
|
||||
_runtimeSettings =
|
||||
runtimeSettings
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(runtimeSettings));
|
||||
}
|
||||
|
||||
public async Task<CredentialTestResult> TestAsync(
|
||||
SonicSystemOption system,
|
||||
string userName,
|
||||
string secret,
|
||||
string containerName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(system);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
"Der Sonic-Benutzername fehlt.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
"Das Sonic-Kennwort fehlt.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(containerName))
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
"Für den Verbindungstest fehlt ein Container.");
|
||||
}
|
||||
|
||||
string javaPath =
|
||||
PathResolver.ResolvePath(
|
||||
_runtimeSettings.JavaExecutablePath);
|
||||
|
||||
string libraryPath =
|
||||
PathResolver.ResolvePath(
|
||||
_runtimeSettings.SonicClientLibraryPath);
|
||||
|
||||
string? toolsPath =
|
||||
ResolveToolsDirectory();
|
||||
|
||||
if (!File.Exists(javaPath))
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
$"Java wurde nicht gefunden: {javaPath}");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(libraryPath))
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
$"Der Sonic-JAR-Ordner wurde nicht gefunden: {libraryPath}");
|
||||
}
|
||||
|
||||
if (toolsPath is null)
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
"SonicMfContainerTool.class wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
string[] jars =
|
||||
Directory.GetFiles(
|
||||
libraryPath,
|
||||
"*.jar",
|
||||
SearchOption.TopDirectoryOnly);
|
||||
|
||||
if (jars.Length == 0)
|
||||
{
|
||||
return CredentialTestResult.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("validate");
|
||||
|
||||
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("--container");
|
||||
startInfo.ArgumentList.Add(
|
||||
containerName);
|
||||
|
||||
startInfo.ArgumentList.Add("--timeout");
|
||||
startInfo.ArgumentList.Add("30");
|
||||
|
||||
startInfo.Environment[
|
||||
"ESB_SONIC_PASSWORD"] = secret;
|
||||
|
||||
using Process process = new()
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (!process.Start())
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
"Der Java-Prozess konnte nicht gestartet werden.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CredentialTestResult.Failed(
|
||||
$"Java konnte nicht gestartet werden: {ex.Message}");
|
||||
}
|
||||
|
||||
Task<string> standardOutputTask =
|
||||
process.StandardOutput.ReadToEndAsync();
|
||||
|
||||
Task<string> standardErrorTask =
|
||||
process.StandardError.ReadToEndAsync();
|
||||
|
||||
using CancellationTokenSource timeoutSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken);
|
||||
|
||||
timeoutSource.CancelAfter(
|
||||
TimeSpan.FromSeconds(35));
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(
|
||||
timeoutSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
TryKill(process);
|
||||
|
||||
return CredentialTestResult.Failed(
|
||||
"Der Sonic-Verbindungstest hat das Zeitlimit überschritten.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
TryKill(process);
|
||||
throw;
|
||||
}
|
||||
|
||||
string standardOutput =
|
||||
(await standardOutputTask).Trim();
|
||||
|
||||
string standardError =
|
||||
(await standardErrorTask).Trim();
|
||||
|
||||
string combinedOutput =
|
||||
string.Join(
|
||||
Environment.NewLine,
|
||||
new[]
|
||||
{
|
||||
standardOutput,
|
||||
standardError
|
||||
}
|
||||
.Where(value =>
|
||||
!string.IsNullOrWhiteSpace(value)));
|
||||
|
||||
bool success =
|
||||
process.ExitCode == 0
|
||||
&& combinedOutput.Contains(
|
||||
"OK:ConnectionValidated",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
&& !combinedOutput.Contains(
|
||||
"ERROR:",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (success)
|
||||
{
|
||||
return CredentialTestResult.Successful();
|
||||
}
|
||||
|
||||
string error =
|
||||
ExtractError(combinedOutput)
|
||||
?? $"Sonic-Verbindungstest fehlgeschlagen, " +
|
||||
$"ExitCode={process.ExitCode}.";
|
||||
|
||||
return CredentialTestResult.Failed(error);
|
||||
}
|
||||
|
||||
private static string? ResolveToolsDirectory()
|
||||
{
|
||||
string[] candidates =
|
||||
[
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Tools"),
|
||||
|
||||
Path.Combine(
|
||||
Directory.GetCurrentDirectory(),
|
||||
"Tools")
|
||||
];
|
||||
|
||||
return candidates.FirstOrDefault(
|
||||
directory =>
|
||||
File.Exists(
|
||||
Path.Combine(
|
||||
directory,
|
||||
"SonicMfContainerTool.class")));
|
||||
}
|
||||
|
||||
private static string? ExtractError(
|
||||
string output)
|
||||
{
|
||||
return output
|
||||
.Split(
|
||||
['\r', '\n'],
|
||||
StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault(
|
||||
line =>
|
||||
line.StartsWith(
|
||||
"ERROR:",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
?["ERROR:".Length..]
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static void TryKill(
|
||||
Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill(
|
||||
entireProcessTree: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Der ursprüngliche Testfehler bleibt erhalten.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CredentialTestResult(
|
||||
bool Success,
|
||||
string Message)
|
||||
{
|
||||
public static CredentialTestResult Successful()
|
||||
{
|
||||
return new CredentialTestResult(
|
||||
true,
|
||||
"Sonic-Anmeldung und lesender Containerzugriff waren erfolgreich.");
|
||||
}
|
||||
|
||||
public static CredentialTestResult Failed(
|
||||
string message)
|
||||
{
|
||||
return new CredentialTestResult(
|
||||
false,
|
||||
message);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ public sealed class SonicManagementClient : IDisposable
|
||||
private readonly SonicConnection _connection;
|
||||
private readonly SonicMfApiExecutor _mfApi;
|
||||
|
||||
public SonicManagementClient(SonicConnection connection)
|
||||
public SonicManagementClient(
|
||||
SonicConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
_mfApi = new SonicMfApiExecutor(connection);
|
||||
|
||||
@@ -5,37 +5,99 @@ using ZA.CoreService.ESBCertificateManager.Models;
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Startet nur den Container-Restart über SonicMfContainerTool (Java + MF-Client-JARs).
|
||||
/// Startet den Container-Restart über SonicMfContainerTool
|
||||
/// mit Java und den Sonic-MfApi-Clientbibliotheken.
|
||||
/// </summary>
|
||||
public sealed class SonicMfApiExecutor
|
||||
{
|
||||
private readonly SonicConnection _connection;
|
||||
|
||||
public SonicMfApiExecutor(SonicConnection connection)
|
||||
public SonicMfApiExecutor(
|
||||
SonicConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
_connection =
|
||||
connection
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(connection));
|
||||
}
|
||||
|
||||
public (string SonicHome, string? JavaExe) ResolveRuntimePaths()
|
||||
public (string SonicHome, string? JavaExe)
|
||||
ResolveRuntimePaths()
|
||||
{
|
||||
string sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome)
|
||||
? "(leer)"
|
||||
: _connection.SonicHome.Trim();
|
||||
return (sonicHome, ResolveJavaExe());
|
||||
string sonicHome =
|
||||
string.IsNullOrWhiteSpace(
|
||||
_connection.SonicHome)
|
||||
? "(leer)"
|
||||
: _connection.SonicHome.Trim();
|
||||
|
||||
return (
|
||||
sonicHome,
|
||||
ResolveJavaExe());
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string? Output, string? Error)> RestartAsync(
|
||||
string containerName,
|
||||
CancellationToken cancellationToken = default)
|
||||
public async Task<(
|
||||
bool Success,
|
||||
string? Output,
|
||||
string? Error)> RestartAsync(
|
||||
string containerName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
(bool prepared, string? javaExe, string? classDir, string? classpath, string? prepareError) =
|
||||
PrepareTool();
|
||||
if (!prepared)
|
||||
if (string.IsNullOrWhiteSpace(containerName))
|
||||
{
|
||||
return (false, null, prepareError);
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
"Der Sonic-Containername fehlt.");
|
||||
}
|
||||
|
||||
ProcessStartInfo psi = new()
|
||||
if (string.IsNullOrWhiteSpace(
|
||||
_connection.Username)
|
||||
|| string.IsNullOrEmpty(
|
||||
_connection.Password))
|
||||
{
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
"Für die Sonic-Verbindung sind keine " +
|
||||
"vollständigen Zugangsdaten eingerichtet. " +
|
||||
"Bitte das Setup ausführen.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(
|
||||
_connection.DomainName))
|
||||
{
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
"Für die Sonic-Verbindung fehlt DomainName.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(
|
||||
_connection.ConnectionUrl))
|
||||
{
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
"Für die Sonic-Verbindung fehlt ConnectionUrl.");
|
||||
}
|
||||
|
||||
(
|
||||
bool prepared,
|
||||
string? javaExe,
|
||||
string? classDir,
|
||||
string? classpath,
|
||||
string? prepareError
|
||||
) = PrepareTool();
|
||||
|
||||
if (!prepared)
|
||||
{
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
prepareError);
|
||||
}
|
||||
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = javaExe!,
|
||||
UseShellExecute = false,
|
||||
@@ -47,173 +109,393 @@ public sealed class SonicMfApiExecutor
|
||||
WorkingDirectory = classDir!
|
||||
};
|
||||
|
||||
psi.ArgumentList.Add("-cp");
|
||||
psi.ArgumentList.Add(classpath!);
|
||||
psi.ArgumentList.Add("SonicMfContainerTool");
|
||||
psi.ArgumentList.Add("restart");
|
||||
psi.ArgumentList.Add("--domain");
|
||||
psi.ArgumentList.Add(_connection.DomainName);
|
||||
psi.ArgumentList.Add("--url");
|
||||
psi.ArgumentList.Add(_connection.ConnectionUrl);
|
||||
psi.ArgumentList.Add("--user");
|
||||
psi.ArgumentList.Add(_connection.Username);
|
||||
psi.ArgumentList.Add("--container");
|
||||
psi.ArgumentList.Add(containerName);
|
||||
psi.ArgumentList.Add("--timeout");
|
||||
psi.ArgumentList.Add(Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString());
|
||||
startInfo.ArgumentList.Add("-cp");
|
||||
startInfo.ArgumentList.Add(classpath!);
|
||||
startInfo.ArgumentList.Add(
|
||||
"SonicMfContainerTool");
|
||||
|
||||
psi.Environment["ESB_SONIC_PASSWORD"] = _connection.Password ?? string.Empty;
|
||||
startInfo.ArgumentList.Add("restart");
|
||||
|
||||
using Process process = new() { StartInfo = psi };
|
||||
if (!process.Start())
|
||||
startInfo.ArgumentList.Add("--domain");
|
||||
startInfo.ArgumentList.Add(
|
||||
_connection.DomainName);
|
||||
|
||||
startInfo.ArgumentList.Add("--url");
|
||||
startInfo.ArgumentList.Add(
|
||||
_connection.ConnectionUrl);
|
||||
|
||||
startInfo.ArgumentList.Add("--user");
|
||||
startInfo.ArgumentList.Add(
|
||||
_connection.Username);
|
||||
|
||||
startInfo.ArgumentList.Add("--container");
|
||||
startInfo.ArgumentList.Add(
|
||||
containerName);
|
||||
|
||||
startInfo.ArgumentList.Add("--timeout");
|
||||
startInfo.ArgumentList.Add(
|
||||
Math.Clamp(
|
||||
_connection.TimeoutSeconds,
|
||||
5,
|
||||
600)
|
||||
.ToString());
|
||||
|
||||
/*
|
||||
* Das Kennwort wird nicht als Kommandozeilenargument
|
||||
* übergeben. Dadurch erscheint das Kennwort nicht in
|
||||
* der Prozessargumentliste.
|
||||
*
|
||||
* CredentialSecret wurde zuvor durch Microsoft.Data.SqlClient
|
||||
* clientseitig entschlüsselt und als Laufzeitwert in
|
||||
* SonicConnection.Password übernommen.
|
||||
*/
|
||||
startInfo.Environment[
|
||||
"ESB_SONIC_PASSWORD"] =
|
||||
_connection.Password;
|
||||
|
||||
using Process process = new()
|
||||
{
|
||||
return (false, null, "Java-Prozess konnte nicht gestartet werden.");
|
||||
}
|
||||
|
||||
using CancellationTokenSource timeoutCts =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600)));
|
||||
|
||||
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
Task<string> stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(timeoutCts.Token);
|
||||
if (!process.Start())
|
||||
{
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
"Der Java-Prozess konnte nicht gestartet werden.");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { process.Kill(entireProcessTree: true); } catch { /* ignore */ }
|
||||
return (false, null, $"MfApi Timeout nach {_connection.TimeoutSeconds}s.");
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
$"Der Java-Prozess konnte nicht gestartet werden: " +
|
||||
$"{ex.Message}");
|
||||
}
|
||||
|
||||
string stdout = (await stdoutTask).Trim();
|
||||
string stderr = (await stderrTask).Trim();
|
||||
string combined = string.Join("\n", new[] { stdout, stderr }.Where(s => s.Length > 0));
|
||||
using CancellationTokenSource timeoutCts =
|
||||
CancellationTokenSource
|
||||
.CreateLinkedTokenSource(
|
||||
cancellationToken);
|
||||
|
||||
bool ok = process.ExitCode == 0
|
||||
&& combined.Contains("OK:RestartInvoked", StringComparison.OrdinalIgnoreCase)
|
||||
&& !combined.Contains("ERROR:", StringComparison.OrdinalIgnoreCase);
|
||||
int timeoutSeconds =
|
||||
Math.Clamp(
|
||||
_connection.TimeoutSeconds,
|
||||
10,
|
||||
600);
|
||||
|
||||
if (ok)
|
||||
timeoutCts.CancelAfter(
|
||||
TimeSpan.FromSeconds(
|
||||
timeoutSeconds));
|
||||
|
||||
Task<string> stdoutTask =
|
||||
process.StandardOutput
|
||||
.ReadToEndAsync();
|
||||
|
||||
Task<string> stderrTask =
|
||||
process.StandardError
|
||||
.ReadToEndAsync();
|
||||
|
||||
try
|
||||
{
|
||||
return (true, combined, null);
|
||||
await process.WaitForExitAsync(
|
||||
timeoutCts.Token);
|
||||
}
|
||||
|
||||
string err = ExtractError(combined)
|
||||
?? $"SonicMfContainerTool ExitCode={process.ExitCode}";
|
||||
|
||||
if (err.Contains("ClassNotFoundException", StringComparison.OrdinalIgnoreCase)
|
||||
|| combined.Contains("JMSConnectorAddress", StringComparison.OrdinalIgnoreCase))
|
||||
catch (OperationCanceledException)
|
||||
when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
err +=
|
||||
"\n\nClasspath unvollständig. MfClientLibPath auf Ordner mit mgmt_client.jar / mfcontext.jar setzen.";
|
||||
TryKillProcess(process);
|
||||
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
$"MfApi-Timeout nach {timeoutSeconds} Sekunden.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
TryKillProcess(process);
|
||||
throw;
|
||||
}
|
||||
|
||||
string detail = string.IsNullOrWhiteSpace(combined) ? err : err + "\n\n--- Tool-Output ---\n" + combined;
|
||||
return (false, combined.Length > 0 ? combined : null, detail);
|
||||
string stdout =
|
||||
(await stdoutTask).Trim();
|
||||
|
||||
string stderr =
|
||||
(await stderrTask).Trim();
|
||||
|
||||
string combined = string.Join(
|
||||
Environment.NewLine,
|
||||
new[] { stdout, stderr }
|
||||
.Where(output =>
|
||||
!string.IsNullOrWhiteSpace(output)));
|
||||
|
||||
bool succeeded =
|
||||
process.ExitCode == 0
|
||||
&& combined.Contains(
|
||||
"OK:RestartInvoked",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
&& !combined.Contains(
|
||||
"ERROR:",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
return (
|
||||
true,
|
||||
string.IsNullOrWhiteSpace(combined)
|
||||
? null
|
||||
: combined,
|
||||
null);
|
||||
}
|
||||
|
||||
string error =
|
||||
ExtractError(combined)
|
||||
?? $"SonicMfContainerTool ExitCode=" +
|
||||
$"{process.ExitCode}";
|
||||
|
||||
if (error.Contains(
|
||||
"ClassNotFoundException",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| combined.Contains(
|
||||
"JMSConnectorAddress",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error +=
|
||||
Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"Der Classpath ist vermutlich unvollständig. " +
|
||||
"Runtime:SonicClientLibraryPath muss auf den " +
|
||||
"Ordner mit den benötigten Sonic-JARs zeigen.";
|
||||
}
|
||||
|
||||
string detail =
|
||||
string.IsNullOrWhiteSpace(combined)
|
||||
? error
|
||||
: error
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ "--- Tool-Output ---"
|
||||
+ Environment.NewLine
|
||||
+ combined;
|
||||
|
||||
return (
|
||||
false,
|
||||
string.IsNullOrWhiteSpace(combined)
|
||||
? null
|
||||
: combined,
|
||||
detail);
|
||||
}
|
||||
|
||||
private (bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error) PrepareTool()
|
||||
private (
|
||||
bool Ok,
|
||||
string? JavaExe,
|
||||
string? ClassDir,
|
||||
string? Classpath,
|
||||
string? Error) PrepareTool()
|
||||
{
|
||||
string? javaExe = ResolveJavaExe();
|
||||
string? javaExe =
|
||||
ResolveJavaExe();
|
||||
|
||||
if (javaExe is null)
|
||||
{
|
||||
return (false, null, null, null,
|
||||
"Java nicht gefunden. In appsettings JavaPath auf java.exe (Java 8+) setzen.");
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"Java wurde nicht gefunden. " +
|
||||
"Runtime:JavaExecutablePath prüfen.");
|
||||
}
|
||||
|
||||
(string? libClasspath, string? libError) = ResolveSonicClasspath();
|
||||
if (libClasspath is null)
|
||||
(
|
||||
string? libraryClasspath,
|
||||
string? libraryError
|
||||
) = ResolveSonicClasspath();
|
||||
|
||||
if (libraryClasspath is null)
|
||||
{
|
||||
return (false, null, null, null, libError);
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
libraryError);
|
||||
}
|
||||
|
||||
string? toolsDir = ResolveToolsDir();
|
||||
if (toolsDir is null)
|
||||
string? toolsDirectory =
|
||||
ResolveToolsDir();
|
||||
|
||||
if (toolsDirectory is null)
|
||||
{
|
||||
return (false, null, null, null,
|
||||
"Tools\\SonicMfContainerTool.class nicht gefunden. Projekt neu bauen.");
|
||||
return (
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"Tools\\SonicMfContainerTool.class " +
|
||||
"wurde nicht gefunden. " +
|
||||
"Projekt vollständig neu bauen.");
|
||||
}
|
||||
|
||||
string classpath = toolsDir + Path.PathSeparator + libClasspath;
|
||||
return (true, javaExe, toolsDir, classpath, null);
|
||||
string classpath =
|
||||
toolsDirectory
|
||||
+ Path.PathSeparator
|
||||
+ libraryClasspath;
|
||||
|
||||
return (
|
||||
true,
|
||||
javaExe,
|
||||
toolsDirectory,
|
||||
classpath,
|
||||
null);
|
||||
}
|
||||
|
||||
private string? ResolveJavaExe()
|
||||
{
|
||||
foreach (string? candidate in EnumerateJavaCandidates())
|
||||
foreach (string? candidate
|
||||
in EnumerateJavaCandidates())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate))
|
||||
if (string.IsNullOrWhiteSpace(candidate))
|
||||
{
|
||||
return Path.GetFullPath(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
string resolvedCandidate;
|
||||
|
||||
try
|
||||
{
|
||||
resolvedCandidate =
|
||||
Path.IsPathRooted(candidate)
|
||||
? Path.GetFullPath(candidate)
|
||||
: Path.GetFullPath(
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
candidate));
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.Exists(resolvedCandidate))
|
||||
{
|
||||
return resolvedCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IEnumerable<string?> EnumerateJavaCandidates()
|
||||
private IEnumerable<string?>
|
||||
EnumerateJavaCandidates()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_connection.JavaPath))
|
||||
if (!string.IsNullOrWhiteSpace(
|
||||
_connection.JavaPath))
|
||||
{
|
||||
yield return _connection.JavaPath.Trim();
|
||||
yield return
|
||||
_connection.JavaPath.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_connection.JavaHome))
|
||||
if (!string.IsNullOrWhiteSpace(
|
||||
_connection.JavaHome))
|
||||
{
|
||||
yield return Path.Combine(_connection.JavaHome.Trim(), "bin", "java.exe");
|
||||
yield return Path.Combine(
|
||||
_connection.JavaHome.Trim(),
|
||||
"bin",
|
||||
"java.exe");
|
||||
}
|
||||
|
||||
string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
|
||||
string? javaHome =
|
||||
Environment.GetEnvironmentVariable(
|
||||
"JAVA_HOME");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(javaHome))
|
||||
{
|
||||
yield return Path.Combine(javaHome.Trim(), "bin", "java.exe");
|
||||
yield return Path.Combine(
|
||||
javaHome.Trim(),
|
||||
"bin",
|
||||
"java.exe");
|
||||
}
|
||||
|
||||
string? jreHome = Environment.GetEnvironmentVariable("JRE_HOME");
|
||||
string? jreHome =
|
||||
Environment.GetEnvironmentVariable(
|
||||
"JRE_HOME");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(jreHome))
|
||||
{
|
||||
yield return Path.Combine(jreHome.Trim(), "bin", "java.exe");
|
||||
yield return Path.Combine(
|
||||
jreHome.Trim(),
|
||||
"bin",
|
||||
"java.exe");
|
||||
}
|
||||
|
||||
yield return "java.exe"; // PATH
|
||||
yield return "java.exe";
|
||||
}
|
||||
|
||||
private (string? Classpath, string? Error) ResolveSonicClasspath()
|
||||
private (
|
||||
string? Classpath,
|
||||
string? Error) ResolveSonicClasspath()
|
||||
{
|
||||
List<string> libDirs = [];
|
||||
List<string> libraryDirectories = [];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath))
|
||||
if (!string.IsNullOrWhiteSpace(
|
||||
_connection.MfClientLibPath))
|
||||
{
|
||||
libDirs.Add(_connection.MfClientLibPath.Trim());
|
||||
libraryDirectories.Add(
|
||||
ResolveDirectoryPath(
|
||||
_connection.MfClientLibPath));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
|
||||
if (!string.IsNullOrWhiteSpace(
|
||||
_connection.SonicHome))
|
||||
{
|
||||
string home = _connection.SonicHome.Trim();
|
||||
libDirs.Add(Path.Combine(home, "lib"));
|
||||
libDirs.Add(home);
|
||||
string sonicHome =
|
||||
ResolveDirectoryPath(
|
||||
_connection.SonicHome);
|
||||
|
||||
libraryDirectories.Add(
|
||||
Path.Combine(
|
||||
sonicHome,
|
||||
"lib"));
|
||||
|
||||
libraryDirectories.Add(
|
||||
sonicHome);
|
||||
}
|
||||
|
||||
HashSet<string> jars = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string dir in libDirs.Where(Directory.Exists))
|
||||
HashSet<string> jars =
|
||||
new(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (string directory
|
||||
in libraryDirectories
|
||||
.Where(Directory.Exists)
|
||||
.Distinct(
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (string jar in Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly))
|
||||
foreach (string jar
|
||||
in Directory.EnumerateFiles(
|
||||
directory,
|
||||
"*.jar",
|
||||
SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
jars.Add(jar);
|
||||
jars.Add(
|
||||
Path.GetFullPath(jar));
|
||||
}
|
||||
}
|
||||
|
||||
if (jars.Count == 0)
|
||||
{
|
||||
return (null,
|
||||
"Keine Sonic-Client-JARs gefunden. MfClientLibPath oder SonicHome\\lib setzen "
|
||||
+ "(mgmt_client.jar, mfcontext.jar, …).");
|
||||
return (
|
||||
null,
|
||||
"Keine Sonic-Client-JARs gefunden. " +
|
||||
"Runtime:SonicClientLibraryPath prüfen.");
|
||||
}
|
||||
|
||||
// Explizite Kern-JARs zuerst, falls vorhanden
|
||||
string[] preferred =
|
||||
string[] preferredJarNames =
|
||||
[
|
||||
"mgmt_client.jar",
|
||||
"mfcontext.jar",
|
||||
@@ -222,58 +504,125 @@ public sealed class SonicMfApiExecutor
|
||||
"mf_common.jar"
|
||||
];
|
||||
|
||||
List<string> ordered = [];
|
||||
foreach (string name in preferred)
|
||||
List<string> orderedJars = [];
|
||||
|
||||
foreach (string preferredJarName
|
||||
in preferredJarNames)
|
||||
{
|
||||
string? hit = jars.FirstOrDefault(j =>
|
||||
string.Equals(Path.GetFileName(j), name, StringComparison.OrdinalIgnoreCase));
|
||||
if (hit is not null)
|
||||
string? matchingJar =
|
||||
jars.FirstOrDefault(
|
||||
jar => string.Equals(
|
||||
Path.GetFileName(jar),
|
||||
preferredJarName,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matchingJar is not null)
|
||||
{
|
||||
ordered.Add(hit);
|
||||
orderedJars.Add(
|
||||
matchingJar);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string jar in jars.OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase))
|
||||
foreach (string jar
|
||||
in jars.OrderBy(
|
||||
Path.GetFileName,
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!ordered.Contains(jar, StringComparer.OrdinalIgnoreCase))
|
||||
if (!orderedJars.Contains(
|
||||
jar,
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
ordered.Add(jar);
|
||||
orderedJars.Add(jar);
|
||||
}
|
||||
}
|
||||
|
||||
return (string.Join(Path.PathSeparator, ordered), null);
|
||||
return (
|
||||
string.Join(
|
||||
Path.PathSeparator,
|
||||
orderedJars),
|
||||
null);
|
||||
}
|
||||
|
||||
private static string ResolveDirectoryPath(
|
||||
string configuredPath)
|
||||
{
|
||||
return Path.IsPathRooted(configuredPath)
|
||||
? Path.GetFullPath(configuredPath)
|
||||
: Path.GetFullPath(
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
configuredPath));
|
||||
}
|
||||
|
||||
private static string? ResolveToolsDir()
|
||||
{
|
||||
string[] dirs =
|
||||
string[] directories =
|
||||
[
|
||||
Path.Combine(AppContext.BaseDirectory, "Tools"),
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "Tools"),
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Tools"),
|
||||
|
||||
Path.Combine(
|
||||
Directory.GetCurrentDirectory(),
|
||||
"Tools"),
|
||||
|
||||
AppContext.BaseDirectory
|
||||
];
|
||||
|
||||
foreach (string dir in dirs.Where(Directory.Exists))
|
||||
foreach (string directory
|
||||
in directories
|
||||
.Where(Directory.Exists))
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir, "SonicMfContainerTool.class")))
|
||||
string toolPath =
|
||||
Path.Combine(
|
||||
directory,
|
||||
"SonicMfContainerTool.class");
|
||||
|
||||
if (File.Exists(toolPath))
|
||||
{
|
||||
return dir;
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ExtractError(string combined)
|
||||
private static string? ExtractError(
|
||||
string combinedOutput)
|
||||
{
|
||||
foreach (string line in combined.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (string line
|
||||
in combinedOutput.Split(
|
||||
['\r', '\n'],
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
|
||||
if (line.StartsWith(
|
||||
"ERROR:",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return line["ERROR:".Length..].Trim();
|
||||
return line[
|
||||
"ERROR:".Length..]
|
||||
.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryKillProcess(
|
||||
Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill(
|
||||
entireProcessTree: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Der ursprüngliche Fehler oder Timeout
|
||||
// darf nicht verdeckt werden.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class SonicSetupRepository
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public SonicSetupRepository(string connectionString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der SQL-Connection-String fehlt.",
|
||||
nameof(connectionString));
|
||||
}
|
||||
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SonicSystemOption>> GetSystemsAsync(
|
||||
string environmentCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(environmentCode))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der EnvironmentCode fehlt.",
|
||||
nameof(environmentCode));
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
connection.SonicConnectionId,
|
||||
connection.ConnectionName,
|
||||
environment.EnvironmentCode,
|
||||
connection.DomainName,
|
||||
connection.ManagementHost,
|
||||
connection.ManagementPort,
|
||||
connection.ConnectionProtocol,
|
||||
MIN(container.ContainerName)
|
||||
AS ValidationContainerName
|
||||
FROM dbo.SonicConnection AS connection
|
||||
INNER JOIN dbo.SonicContainer AS container
|
||||
ON container.SonicConnectionId =
|
||||
connection.SonicConnectionId
|
||||
INNER JOIN dbo.EsbService AS service
|
||||
ON service.EsbServiceId =
|
||||
container.EsbServiceId
|
||||
INNER JOIN dbo.Environment AS environment
|
||||
ON environment.EnvironmentId =
|
||||
service.EnvironmentId
|
||||
WHERE connection.IsActive = 1
|
||||
AND container.IsActive = 1
|
||||
AND service.IsActive = 1
|
||||
AND environment.IsActive = 1
|
||||
AND environment.EnvironmentCode =
|
||||
@EnvironmentCode
|
||||
GROUP BY
|
||||
connection.SonicConnectionId,
|
||||
connection.ConnectionName,
|
||||
environment.EnvironmentCode,
|
||||
connection.DomainName,
|
||||
connection.ManagementHost,
|
||||
connection.ManagementPort,
|
||||
connection.ConnectionProtocol
|
||||
ORDER BY connection.ConnectionName;
|
||||
""";
|
||||
|
||||
List<SonicSystemOption> systems = [];
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@EnvironmentCode",
|
||||
SqlDbType.NVarChar,
|
||||
30)
|
||||
{
|
||||
Value = environmentCode
|
||||
.Trim()
|
||||
.ToUpperInvariant()
|
||||
});
|
||||
|
||||
await using SqlDataReader reader =
|
||||
await command.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
int sonicConnectionIdOrdinal =
|
||||
reader.GetOrdinal("SonicConnectionId");
|
||||
|
||||
int connectionNameOrdinal =
|
||||
reader.GetOrdinal("ConnectionName");
|
||||
|
||||
int environmentCodeOrdinal =
|
||||
reader.GetOrdinal("EnvironmentCode");
|
||||
|
||||
int domainNameOrdinal =
|
||||
reader.GetOrdinal("DomainName");
|
||||
|
||||
int managementHostOrdinal =
|
||||
reader.GetOrdinal("ManagementHost");
|
||||
|
||||
int managementPortOrdinal =
|
||||
reader.GetOrdinal("ManagementPort");
|
||||
|
||||
int connectionProtocolOrdinal =
|
||||
reader.GetOrdinal("ConnectionProtocol");
|
||||
|
||||
int validationContainerNameOrdinal =
|
||||
reader.GetOrdinal("ValidationContainerName");
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
if (reader.IsDBNull(managementPortOrdinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
systems.Add(
|
||||
new SonicSystemOption
|
||||
{
|
||||
SonicConnectionId =
|
||||
reader.GetInt32(
|
||||
sonicConnectionIdOrdinal),
|
||||
|
||||
ConnectionName =
|
||||
reader.GetString(
|
||||
connectionNameOrdinal),
|
||||
|
||||
EnvironmentCode =
|
||||
reader.GetString(
|
||||
environmentCodeOrdinal),
|
||||
|
||||
DomainName =
|
||||
reader.IsDBNull(domainNameOrdinal)
|
||||
? string.Empty
|
||||
: reader.GetString(
|
||||
domainNameOrdinal),
|
||||
|
||||
ManagementHost =
|
||||
reader.GetString(
|
||||
managementHostOrdinal),
|
||||
|
||||
ManagementPort =
|
||||
reader.GetInt32(
|
||||
managementPortOrdinal),
|
||||
|
||||
ConnectionProtocol =
|
||||
reader.IsDBNull(connectionProtocolOrdinal)
|
||||
? "tcp"
|
||||
: reader.GetString(
|
||||
connectionProtocolOrdinal),
|
||||
|
||||
ValidationContainerName =
|
||||
reader.IsDBNull(
|
||||
validationContainerNameOrdinal)
|
||||
? string.Empty
|
||||
: reader.GetString(
|
||||
validationContainerNameOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
return systems;
|
||||
}
|
||||
|
||||
public async Task<int> AddCredentialAsync(
|
||||
int sonicConnectionId,
|
||||
string credentialName,
|
||||
string userName,
|
||||
string secret,
|
||||
bool isDefault,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sonicConnectionId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sonicConnectionId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(credentialName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Profilname fehlt.",
|
||||
nameof(credentialName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Sonic-Benutzername fehlt.",
|
||||
nameof(userName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das Sonic-Kennwort fehlt.",
|
||||
nameof(secret));
|
||||
}
|
||||
|
||||
string normalizedCredentialName =
|
||||
credentialName.Trim();
|
||||
|
||||
string normalizedUserName =
|
||||
userName.Trim();
|
||||
|
||||
if (normalizedCredentialName.Length > 150)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Profilname darf maximal 150 Zeichen enthalten.",
|
||||
nameof(credentialName));
|
||||
}
|
||||
|
||||
if (normalizedUserName.Length > 256)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Benutzername darf maximal 256 Zeichen enthalten.",
|
||||
nameof(userName));
|
||||
}
|
||||
|
||||
if (secret.Length > 512)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das Kennwort darf maximal 512 Zeichen enthalten.",
|
||||
nameof(secret));
|
||||
}
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlTransaction transaction =
|
||||
(SqlTransaction)
|
||||
await connection.BeginTransactionAsync(
|
||||
cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (isDefault)
|
||||
{
|
||||
const string clearDefaultSql = """
|
||||
UPDATE dbo.SonicCredential
|
||||
SET
|
||||
IsDefault = 0,
|
||||
UpdatedAtUtc = SYSUTCDATETIME()
|
||||
WHERE SonicConnectionId =
|
||||
@SonicConnectionId
|
||||
AND IsDefault = 1;
|
||||
""";
|
||||
|
||||
await using SqlCommand clearDefaultCommand =
|
||||
new(
|
||||
clearDefaultSql,
|
||||
connection,
|
||||
transaction);
|
||||
|
||||
clearDefaultCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
await clearDefaultCommand.ExecuteNonQueryAsync(
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
const string insertSql = """
|
||||
INSERT INTO dbo.SonicCredential
|
||||
(
|
||||
SonicConnectionId,
|
||||
CredentialName,
|
||||
CredentialUserName,
|
||||
CredentialSecret,
|
||||
IsDefault,
|
||||
IsActive,
|
||||
CreatedAtUtc,
|
||||
UpdatedAtUtc
|
||||
)
|
||||
OUTPUT INSERTED.SonicCredentialId
|
||||
VALUES
|
||||
(
|
||||
@SonicConnectionId,
|
||||
@CredentialName,
|
||||
@CredentialUserName,
|
||||
@CredentialSecret,
|
||||
@IsDefault,
|
||||
1,
|
||||
SYSUTCDATETIME(),
|
||||
NULL
|
||||
);
|
||||
""";
|
||||
|
||||
await using SqlCommand insertCommand =
|
||||
new(
|
||||
insertSql,
|
||||
connection,
|
||||
transaction);
|
||||
|
||||
insertCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
insertCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialName",
|
||||
SqlDbType.NVarChar,
|
||||
150)
|
||||
{
|
||||
Value = normalizedCredentialName
|
||||
});
|
||||
|
||||
insertCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialUserName",
|
||||
SqlDbType.NVarChar,
|
||||
256)
|
||||
{
|
||||
Value = normalizedUserName
|
||||
});
|
||||
|
||||
insertCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialSecret",
|
||||
SqlDbType.NVarChar,
|
||||
512)
|
||||
{
|
||||
Value = secret
|
||||
});
|
||||
|
||||
insertCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@IsDefault",
|
||||
SqlDbType.Bit)
|
||||
{
|
||||
Value = isDefault
|
||||
});
|
||||
|
||||
object? result =
|
||||
await insertCommand.ExecuteScalarAsync(
|
||||
cancellationToken);
|
||||
|
||||
if (result is null || result is DBNull)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Das Benutzerprofil konnte nicht angelegt werden.");
|
||||
}
|
||||
|
||||
int credentialId =
|
||||
Convert.ToInt32(result);
|
||||
|
||||
await transaction.CommitAsync(
|
||||
cancellationToken);
|
||||
|
||||
return credentialId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
await transaction.RollbackAsync(
|
||||
CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Die ursprüngliche Exception soll erhalten bleiben.
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SonicCredentialProfile>>
|
||||
GetCredentialsAsync(
|
||||
int sonicConnectionId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sonicConnectionId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sonicConnectionId));
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
SonicCredentialId,
|
||||
SonicConnectionId,
|
||||
CredentialName,
|
||||
CredentialUserName,
|
||||
CredentialSecret,
|
||||
IsDefault,
|
||||
IsActive
|
||||
FROM dbo.SonicCredential
|
||||
WHERE SonicConnectionId = @SonicConnectionId
|
||||
AND IsActive = 1
|
||||
ORDER BY
|
||||
IsDefault DESC,
|
||||
CredentialName;
|
||||
""";
|
||||
|
||||
List<SonicCredentialProfile> profiles = [];
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
await using SqlDataReader reader =
|
||||
await command.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
int credentialIdOrdinal =
|
||||
reader.GetOrdinal("SonicCredentialId");
|
||||
|
||||
int connectionIdOrdinal =
|
||||
reader.GetOrdinal("SonicConnectionId");
|
||||
|
||||
int credentialNameOrdinal =
|
||||
reader.GetOrdinal("CredentialName");
|
||||
|
||||
int userNameOrdinal =
|
||||
reader.GetOrdinal("CredentialUserName");
|
||||
|
||||
int secretOrdinal =
|
||||
reader.GetOrdinal("CredentialSecret");
|
||||
|
||||
int isDefaultOrdinal =
|
||||
reader.GetOrdinal("IsDefault");
|
||||
|
||||
int isActiveOrdinal =
|
||||
reader.GetOrdinal("IsActive");
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
string? userName =
|
||||
reader.IsDBNull(userNameOrdinal)
|
||||
? null
|
||||
: reader.GetString(userNameOrdinal);
|
||||
|
||||
string? secret =
|
||||
reader.IsDBNull(secretOrdinal)
|
||||
? null
|
||||
: reader.GetString(secretOrdinal);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName)
|
||||
|| string.IsNullOrEmpty(secret))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
profiles.Add(
|
||||
new SonicCredentialProfile
|
||||
{
|
||||
SonicCredentialId =
|
||||
reader.GetInt32(
|
||||
credentialIdOrdinal),
|
||||
|
||||
SonicConnectionId =
|
||||
reader.GetInt32(
|
||||
connectionIdOrdinal),
|
||||
|
||||
CredentialName =
|
||||
reader.GetString(
|
||||
credentialNameOrdinal),
|
||||
|
||||
UserName = userName,
|
||||
Secret = secret,
|
||||
|
||||
IsDefault =
|
||||
reader.GetBoolean(
|
||||
isDefaultOrdinal),
|
||||
|
||||
IsActive =
|
||||
reader.GetBoolean(
|
||||
isActiveOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class WindowsCredentialManagerSecretProvider
|
||||
: ISecretProvider
|
||||
{
|
||||
private const uint GenericCredentialType = 1;
|
||||
private const int ErrorNotFound = 1168;
|
||||
|
||||
public StoredCredential GetCredential(
|
||||
string credentialReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(credentialReference))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Die CredentialReference ist nicht konfiguriert.",
|
||||
nameof(credentialReference));
|
||||
}
|
||||
|
||||
bool success = CredRead(
|
||||
credentialReference,
|
||||
GenericCredentialType,
|
||||
0,
|
||||
out IntPtr credentialPointer);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
|
||||
if (errorCode == ErrorNotFound)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Die Windows-Anmeldeinformation " +
|
||||
$"'{credentialReference}' wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
throw new Win32Exception(
|
||||
errorCode,
|
||||
"Die Windows-Anmeldeinformation konnte nicht gelesen werden.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
NativeCredential nativeCredential =
|
||||
Marshal.PtrToStructure<NativeCredential>(
|
||||
credentialPointer);
|
||||
|
||||
string userName =
|
||||
Marshal.PtrToStringUni(nativeCredential.UserName)
|
||||
?? string.Empty;
|
||||
|
||||
string secret = string.Empty;
|
||||
|
||||
if (nativeCredential.CredentialBlob != IntPtr.Zero
|
||||
&& nativeCredential.CredentialBlobSize > 0)
|
||||
{
|
||||
int characterCount = checked(
|
||||
(int)nativeCredential.CredentialBlobSize / 2);
|
||||
|
||||
secret = Marshal.PtrToStringUni(
|
||||
nativeCredential.CredentialBlob,
|
||||
characterCount)
|
||||
?? string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Die Windows-Anmeldeinformation " +
|
||||
$"'{credentialReference}' enthält keinen Benutzernamen.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Die Windows-Anmeldeinformation " +
|
||||
$"'{credentialReference}' enthält kein Kennwort.");
|
||||
}
|
||||
|
||||
return new StoredCredential(
|
||||
userName,
|
||||
secret);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CredFree(credentialPointer);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport(
|
||||
"advapi32.dll",
|
||||
EntryPoint = "CredReadW",
|
||||
CharSet = CharSet.Unicode,
|
||||
SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CredRead(
|
||||
string target,
|
||||
uint type,
|
||||
int reservedFlag,
|
||||
out IntPtr credentialPointer);
|
||||
|
||||
[DllImport("advapi32.dll")]
|
||||
private static extern void CredFree(
|
||||
IntPtr credentialPointer);
|
||||
|
||||
[StructLayout(
|
||||
LayoutKind.Sequential,
|
||||
CharSet = CharSet.Unicode)]
|
||||
private struct NativeCredential
|
||||
{
|
||||
public uint Flags;
|
||||
public uint Type;
|
||||
public IntPtr TargetName;
|
||||
public IntPtr Comment;
|
||||
public long LastWritten;
|
||||
public uint CredentialBlobSize;
|
||||
public IntPtr CredentialBlob;
|
||||
public uint Persist;
|
||||
public uint AttributeCount;
|
||||
public IntPtr Attributes;
|
||||
public IntPtr TargetAlias;
|
||||
public IntPtr UserName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user