big changes
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Setup;
|
||||
|
||||
public sealed class AddCredentialProfileForm : Form
|
||||
{
|
||||
private readonly SetupCoordinator _coordinator;
|
||||
private readonly SonicSystemOption _system;
|
||||
|
||||
private readonly TextBox _txtProfileName;
|
||||
private readonly TextBox _txtUserName;
|
||||
private readonly TextBox _txtPassword;
|
||||
private readonly TextBox _txtPasswordRepeat;
|
||||
private readonly CheckBox _chkDefault;
|
||||
private readonly Button _btnSave;
|
||||
private readonly Button _btnCancel;
|
||||
|
||||
private bool _operationRunning;
|
||||
|
||||
public SonicCredentialProfile? CreatedProfile
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public AddCredentialProfileForm(
|
||||
SetupCoordinator coordinator,
|
||||
SonicSystemOption system)
|
||||
{
|
||||
_coordinator =
|
||||
coordinator
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(coordinator));
|
||||
|
||||
_system =
|
||||
system
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(system));
|
||||
|
||||
Text = "Benutzerprofil hinzufügen";
|
||||
|
||||
StartPosition =
|
||||
FormStartPosition.CenterParent;
|
||||
|
||||
ClientSize =
|
||||
new Size(560, 525);
|
||||
|
||||
FormBorderStyle =
|
||||
FormBorderStyle.FixedDialog;
|
||||
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
ShowInTaskbar = false;
|
||||
|
||||
BackColor =
|
||||
Color.FromArgb(10, 18, 34);
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(241, 245, 249);
|
||||
|
||||
Font =
|
||||
new Font("Segoe UI", 10);
|
||||
|
||||
Label title = new()
|
||||
{
|
||||
Text =
|
||||
$"Profil für {_system.ConnectionName}",
|
||||
|
||||
Location =
|
||||
new Point(28, 22),
|
||||
|
||||
AutoSize = true,
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(241, 245, 249),
|
||||
|
||||
Font =
|
||||
new Font(
|
||||
"Segoe UI",
|
||||
17,
|
||||
FontStyle.Bold)
|
||||
};
|
||||
|
||||
Label details = new()
|
||||
{
|
||||
Text =
|
||||
$"Umgebung: {_system.EnvironmentCode}"
|
||||
+ Environment.NewLine
|
||||
+ $"Domain: {_system.DomainName}"
|
||||
+ Environment.NewLine
|
||||
+ $"Adresse: {_system.ConnectionUrl}"
|
||||
+ Environment.NewLine
|
||||
+ $"Prüfcontainer: "
|
||||
+ $"{_system.ValidationContainerName}",
|
||||
|
||||
Location =
|
||||
new Point(30, 62),
|
||||
|
||||
Size =
|
||||
new Size(495, 90),
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(169, 184, 200)
|
||||
};
|
||||
|
||||
Label profileLabel =
|
||||
CreateLabel(
|
||||
"Profilname",
|
||||
160);
|
||||
|
||||
_txtProfileName =
|
||||
CreateTextBox(185);
|
||||
|
||||
_txtProfileName.Text =
|
||||
"Administrator";
|
||||
|
||||
Label userLabel =
|
||||
CreateLabel(
|
||||
"Sonic-Benutzername",
|
||||
225);
|
||||
|
||||
_txtUserName =
|
||||
CreateTextBox(250);
|
||||
|
||||
_txtUserName.Text =
|
||||
"Administrator";
|
||||
|
||||
Label passwordLabel =
|
||||
CreateLabel(
|
||||
"Kennwort",
|
||||
290);
|
||||
|
||||
_txtPassword =
|
||||
CreateTextBox(315);
|
||||
|
||||
_txtPassword.UseSystemPasswordChar =
|
||||
true;
|
||||
|
||||
Label repeatLabel =
|
||||
CreateLabel(
|
||||
"Kennwort wiederholen",
|
||||
355);
|
||||
|
||||
_txtPasswordRepeat =
|
||||
CreateTextBox(380);
|
||||
|
||||
_txtPasswordRepeat.UseSystemPasswordChar =
|
||||
true;
|
||||
|
||||
_chkDefault = new CheckBox
|
||||
{
|
||||
Text =
|
||||
"Als Standardprofil verwenden",
|
||||
|
||||
Location =
|
||||
new Point(30, 425),
|
||||
|
||||
AutoSize = true,
|
||||
Checked = true,
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(241, 245, 249)
|
||||
};
|
||||
|
||||
_btnCancel = new Button
|
||||
{
|
||||
Text = "Abbrechen",
|
||||
|
||||
Location =
|
||||
new Point(125, 465),
|
||||
|
||||
Size =
|
||||
new Size(110, 38),
|
||||
|
||||
BackColor =
|
||||
Color.FromArgb(39, 52, 73),
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(241, 245, 249),
|
||||
|
||||
FlatStyle =
|
||||
FlatStyle.Flat,
|
||||
|
||||
DialogResult =
|
||||
DialogResult.Cancel
|
||||
};
|
||||
|
||||
_btnCancel.FlatAppearance.BorderSize = 0;
|
||||
|
||||
_btnSave = new Button
|
||||
{
|
||||
Text =
|
||||
"Verbindung prüfen und hinzufügen",
|
||||
|
||||
Location =
|
||||
new Point(245, 465),
|
||||
|
||||
Size =
|
||||
new Size(280, 38),
|
||||
|
||||
BackColor =
|
||||
Color.FromArgb(208, 171, 57),
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(30, 25, 10),
|
||||
|
||||
FlatStyle =
|
||||
FlatStyle.Flat,
|
||||
|
||||
Font =
|
||||
new Font(
|
||||
"Segoe UI",
|
||||
9,
|
||||
FontStyle.Bold)
|
||||
};
|
||||
|
||||
_btnSave.FlatAppearance.BorderSize = 0;
|
||||
|
||||
_btnSave.Click +=
|
||||
async (_, _) =>
|
||||
await TestAndSaveAsync();
|
||||
|
||||
Controls.Add(title);
|
||||
Controls.Add(details);
|
||||
Controls.Add(profileLabel);
|
||||
Controls.Add(_txtProfileName);
|
||||
Controls.Add(userLabel);
|
||||
Controls.Add(_txtUserName);
|
||||
Controls.Add(passwordLabel);
|
||||
Controls.Add(_txtPassword);
|
||||
Controls.Add(repeatLabel);
|
||||
Controls.Add(_txtPasswordRepeat);
|
||||
Controls.Add(_chkDefault);
|
||||
Controls.Add(_btnCancel);
|
||||
Controls.Add(_btnSave);
|
||||
|
||||
AcceptButton = _btnSave;
|
||||
CancelButton = _btnCancel;
|
||||
}
|
||||
|
||||
private Label CreateLabel(
|
||||
string text,
|
||||
int y)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
|
||||
Location =
|
||||
new Point(30, y),
|
||||
|
||||
AutoSize = true,
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(203, 213, 225)
|
||||
};
|
||||
}
|
||||
|
||||
private TextBox CreateTextBox(int y)
|
||||
{
|
||||
return new TextBox
|
||||
{
|
||||
Location =
|
||||
new Point(30, y),
|
||||
|
||||
Size =
|
||||
new Size(495, 27),
|
||||
|
||||
BorderStyle =
|
||||
BorderStyle.FixedSingle,
|
||||
|
||||
BackColor =
|
||||
Color.FromArgb(24, 35, 54),
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(241, 245, 249)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task TestAndSaveAsync()
|
||||
{
|
||||
if (_operationRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string profileName =
|
||||
_txtProfileName.Text.Trim();
|
||||
|
||||
string userName =
|
||||
_txtUserName.Text.Trim();
|
||||
|
||||
string password =
|
||||
_txtPassword.Text;
|
||||
|
||||
string passwordRepeat =
|
||||
_txtPasswordRepeat.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(profileName))
|
||||
{
|
||||
ShowWarning(
|
||||
"Bitte einen Profilnamen eingeben.");
|
||||
|
||||
_txtProfileName.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (profileName.Length > 150)
|
||||
{
|
||||
ShowWarning(
|
||||
"Der Profilname darf maximal 150 Zeichen enthalten.");
|
||||
|
||||
_txtProfileName.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
ShowWarning(
|
||||
"Bitte einen Sonic-Benutzernamen eingeben.");
|
||||
|
||||
_txtUserName.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (userName.Length > 256)
|
||||
{
|
||||
ShowWarning(
|
||||
"Der Benutzername darf maximal 256 Zeichen enthalten.");
|
||||
|
||||
_txtUserName.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(password))
|
||||
{
|
||||
ShowWarning(
|
||||
"Bitte ein Kennwort eingeben.");
|
||||
|
||||
_txtPassword.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.Length > 512)
|
||||
{
|
||||
ShowWarning(
|
||||
"Das Kennwort darf maximal 512 Zeichen enthalten.");
|
||||
|
||||
_txtPassword.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(
|
||||
password,
|
||||
passwordRepeat,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
ShowWarning(
|
||||
"Die eingegebenen Kennwörter stimmen nicht überein.");
|
||||
|
||||
_txtPasswordRepeat.Clear();
|
||||
_txtPasswordRepeat.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
await RunSaveOperationAsync(
|
||||
profileName,
|
||||
userName,
|
||||
password);
|
||||
}
|
||||
|
||||
private async Task RunSaveOperationAsync(
|
||||
string profileName,
|
||||
string userName,
|
||||
string password)
|
||||
{
|
||||
_operationRunning = true;
|
||||
SetOperationState(isRunning: true);
|
||||
|
||||
try
|
||||
{
|
||||
CreatedProfile =
|
||||
await _coordinator.AddCredentialAsync(
|
||||
_system,
|
||||
profileName,
|
||||
userName,
|
||||
password,
|
||||
_chkDefault.Checked);
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Keine Fehlermeldung bei einem regulären Abbruch.
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Das Benutzerprofil konnte nicht angelegt werden."
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ exception.Message,
|
||||
"Benutzerprofil",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationRunning = false;
|
||||
|
||||
if (!IsDisposed)
|
||||
{
|
||||
SetOperationState(isRunning: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOperationState(bool isRunning)
|
||||
{
|
||||
UseWaitCursor = isRunning;
|
||||
|
||||
_btnSave.Enabled = !isRunning;
|
||||
_btnCancel.Enabled = !isRunning;
|
||||
|
||||
_txtProfileName.Enabled = !isRunning;
|
||||
_txtUserName.Enabled = !isRunning;
|
||||
_txtPassword.Enabled = !isRunning;
|
||||
_txtPasswordRepeat.Enabled = !isRunning;
|
||||
_chkDefault.Enabled = !isRunning;
|
||||
|
||||
_btnSave.Text =
|
||||
isRunning
|
||||
? "Verbindung wird geprüft ..."
|
||||
: "Verbindung prüfen und hinzufügen";
|
||||
}
|
||||
|
||||
private void ShowWarning(string message)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
message,
|
||||
"Eingabe prüfen",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
using static ZA.CoreService.ESBCertificateManager.Models.RuntimeSettings;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class AlwaysEncryptedCertificateService
|
||||
{
|
||||
private readonly string _expectedThumbprint;
|
||||
private readonly StoreName _storeName;
|
||||
private readonly StoreLocation _storeLocation;
|
||||
|
||||
public AlwaysEncryptedCertificateService(
|
||||
AlwaysEncryptedSettings settings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(
|
||||
settings.CertificateThumbprint))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"AlwaysEncrypted:CertificateThumbprint fehlt.");
|
||||
}
|
||||
|
||||
_expectedThumbprint = NormalizeThumbprint(
|
||||
settings.CertificateThumbprint);
|
||||
|
||||
if (!Enum.TryParse(
|
||||
settings.StoreName,
|
||||
ignoreCase: true,
|
||||
out _storeName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Ungültiger Zertifikatsspeicher: '{settings.StoreName}'.");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(
|
||||
settings.StoreLocation,
|
||||
ignoreCase: true,
|
||||
out _storeLocation))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Ungültiger Zertifikatsspeicherort: " +
|
||||
$"'{settings.StoreLocation}'.");
|
||||
}
|
||||
}
|
||||
|
||||
public CertificateCheckResult CheckCertificate()
|
||||
{
|
||||
using X509Store store = new(
|
||||
_storeName,
|
||||
_storeLocation);
|
||||
|
||||
store.Open(OpenFlags.ReadOnly);
|
||||
|
||||
X509Certificate2? certificate = store.Certificates
|
||||
.Find(
|
||||
X509FindType.FindByThumbprint,
|
||||
_expectedThumbprint,
|
||||
validOnly: false)
|
||||
.OfType<X509Certificate2>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (certificate is null)
|
||||
{
|
||||
return new CertificateCheckResult(
|
||||
IsValid: false,
|
||||
Message:
|
||||
$"Das Always-Encrypted-Zertifikat wurde nicht gefunden. " +
|
||||
$"Speicher: {_storeLocation}\\{_storeName}, " +
|
||||
$"Thumbprint: {_expectedThumbprint}");
|
||||
}
|
||||
|
||||
if (!certificate.HasPrivateKey)
|
||||
{
|
||||
return new CertificateCheckResult(
|
||||
IsValid: false,
|
||||
Message:
|
||||
"Das Always-Encrypted-Zertifikat wurde gefunden, " +
|
||||
"besitzt aber keinen verfügbaren privaten Schlüssel.");
|
||||
}
|
||||
|
||||
return new CertificateCheckResult(
|
||||
IsValid: true,
|
||||
Message:
|
||||
"Always-Encrypted-Zertifikat und privater " +
|
||||
"Schlüssel sind verfügbar.");
|
||||
}
|
||||
|
||||
public void ImportPfx(
|
||||
string pfxFilePath,
|
||||
string pfxPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pfxFilePath))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Es wurde keine PFX-Datei ausgewählt.",
|
||||
nameof(pfxFilePath));
|
||||
}
|
||||
|
||||
if (!File.Exists(pfxFilePath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"Die ausgewählte PFX-Datei wurde nicht gefunden.",
|
||||
pfxFilePath);
|
||||
}
|
||||
|
||||
X509KeyStorageFlags flags =
|
||||
X509KeyStorageFlags.UserKeySet
|
||||
| X509KeyStorageFlags.PersistKeySet;
|
||||
|
||||
using X509Certificate2 certificate = new(
|
||||
pfxFilePath,
|
||||
pfxPassword,
|
||||
flags);
|
||||
|
||||
string importedThumbprint =
|
||||
NormalizeThumbprint(certificate.Thumbprint);
|
||||
|
||||
if (!string.Equals(
|
||||
importedThumbprint,
|
||||
_expectedThumbprint,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Die ausgewählte PFX-Datei gehört nicht zum " +
|
||||
"erwarteten Always-Encrypted-Zertifikat.");
|
||||
}
|
||||
|
||||
if (!certificate.HasPrivateKey)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Die ausgewählte PFX-Datei enthält keinen privaten Schlüssel.");
|
||||
}
|
||||
|
||||
using X509Store store = new(
|
||||
_storeName,
|
||||
_storeLocation);
|
||||
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
store.Add(certificate);
|
||||
}
|
||||
|
||||
private static string NormalizeThumbprint(
|
||||
string thumbprint)
|
||||
{
|
||||
return thumbprint
|
||||
.Replace(" ", string.Empty)
|
||||
.Trim()
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CertificateCheckResult(
|
||||
bool IsValid,
|
||||
string Message);
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace ZA.CoreService.ESBCertificateManager.Setup;
|
||||
|
||||
public sealed class SetupCheckResult
|
||||
{
|
||||
public bool RuntimeReady { get; init; }
|
||||
|
||||
public bool CertificateReady { get; init; }
|
||||
|
||||
public bool DatabaseReady { get; init; }
|
||||
|
||||
public bool CredentialsReady { get; init; }
|
||||
|
||||
public string RuntimeMessage { get; init; } =
|
||||
string.Empty;
|
||||
|
||||
public string CertificateMessage { get; init; } =
|
||||
string.Empty;
|
||||
|
||||
public string DatabaseMessage { get; init; } =
|
||||
string.Empty;
|
||||
|
||||
public string CredentialsMessage { get; init; } =
|
||||
string.Empty;
|
||||
|
||||
public bool IsReady =>
|
||||
RuntimeReady
|
||||
&& CertificateReady
|
||||
&& DatabaseReady
|
||||
&& CredentialsReady;
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
using ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Setup;
|
||||
|
||||
public sealed class SetupCoordinator
|
||||
{
|
||||
private readonly AppSettings _settings;
|
||||
|
||||
private readonly RuntimeEnvironmentValidator
|
||||
_runtimeValidator;
|
||||
|
||||
private readonly AlwaysEncryptedCertificateService
|
||||
_certificateService;
|
||||
|
||||
private readonly SonicSetupRepository
|
||||
_setupRepository;
|
||||
|
||||
private readonly LocalSetupSelectionStore
|
||||
_selectionStore;
|
||||
private readonly SonicCredentialTester
|
||||
_credentialTester;
|
||||
private readonly SonicSetupRepository _repository;
|
||||
public SetupCoordinator(
|
||||
AppSettings settings, SonicSetupRepository repository)
|
||||
{
|
||||
_repository =
|
||||
repository
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(repository));
|
||||
|
||||
|
||||
_credentialTester =
|
||||
new SonicCredentialTester(
|
||||
settings.Runtime);
|
||||
|
||||
_settings =
|
||||
settings
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(settings));
|
||||
|
||||
_runtimeValidator =
|
||||
new RuntimeEnvironmentValidator();
|
||||
|
||||
_certificateService =
|
||||
new AlwaysEncryptedCertificateService(
|
||||
settings.AlwaysEncrypted);
|
||||
|
||||
_setupRepository =
|
||||
new SonicSetupRepository(
|
||||
settings.Database.ConnectionString);
|
||||
|
||||
_selectionStore =
|
||||
new LocalSetupSelectionStore();
|
||||
}
|
||||
|
||||
public AppSettings Settings =>
|
||||
_settings;
|
||||
|
||||
public AlwaysEncryptedCertificateService
|
||||
CertificateService =>
|
||||
_certificateService;
|
||||
|
||||
public LocalSetupSelection? LoadSelection()
|
||||
{
|
||||
return _selectionStore.Load();
|
||||
}
|
||||
public async Task<SonicCredentialProfile>
|
||||
TestAndAddCredentialAsync(
|
||||
SonicSystemOption system,
|
||||
string credentialName,
|
||||
string userName,
|
||||
string secret,
|
||||
bool isDefault,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
CredentialTestResult testResult =
|
||||
await _credentialTester.TestAsync(
|
||||
system,
|
||||
userName,
|
||||
secret,
|
||||
system.ValidationContainerName,
|
||||
cancellationToken);
|
||||
|
||||
if (!testResult.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Das Benutzerprofil wurde nicht gespeichert." +
|
||||
Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
testResult.Message);
|
||||
}
|
||||
|
||||
int credentialId =
|
||||
await _setupRepository.AddCredentialAsync(
|
||||
system.SonicConnectionId,
|
||||
credentialName,
|
||||
userName,
|
||||
secret,
|
||||
isDefault,
|
||||
cancellationToken);
|
||||
|
||||
return new SonicCredentialProfile
|
||||
{
|
||||
SonicCredentialId = credentialId,
|
||||
SonicConnectionId =
|
||||
system.SonicConnectionId,
|
||||
CredentialName =
|
||||
credentialName.Trim(),
|
||||
UserName =
|
||||
userName.Trim(),
|
||||
Secret = secret,
|
||||
IsDefault = isDefault,
|
||||
IsActive = true
|
||||
};
|
||||
}
|
||||
public async Task<SonicCredentialProfile> AddCredentialAsync(
|
||||
SonicSystemOption system,
|
||||
string credentialName,
|
||||
string userName,
|
||||
string secret,
|
||||
bool isDefault,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(system);
|
||||
|
||||
if (system.SonicConnectionId <= 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das ausgewählte Sonic-System ist ungültig.",
|
||||
nameof(system));
|
||||
}
|
||||
|
||||
int credentialId =
|
||||
await _repository.AddCredentialAsync(
|
||||
system.SonicConnectionId,
|
||||
credentialName,
|
||||
userName,
|
||||
secret,
|
||||
isDefault,
|
||||
cancellationToken);
|
||||
|
||||
IReadOnlyList<SonicCredentialProfile> profiles =
|
||||
await _repository.GetCredentialsAsync(
|
||||
system.SonicConnectionId,
|
||||
cancellationToken);
|
||||
|
||||
SonicCredentialProfile? createdProfile =
|
||||
profiles.FirstOrDefault(
|
||||
profile =>
|
||||
profile.SonicCredentialId
|
||||
== credentialId);
|
||||
|
||||
if (createdProfile is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Das Benutzerprofil wurde gespeichert, "
|
||||
+ "konnte anschließend aber nicht geladen werden.");
|
||||
}
|
||||
|
||||
return createdProfile;
|
||||
}
|
||||
public async Task<SetupCheckResult> CheckAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
RuntimeValidationResult runtimeResult =
|
||||
await _runtimeValidator.ValidateAsync(
|
||||
_settings.Runtime,
|
||||
cancellationToken);
|
||||
|
||||
CertificateCheckResult certificateResult =
|
||||
_certificateService.CheckCertificate();
|
||||
|
||||
if (!certificateResult.IsValid)
|
||||
{
|
||||
return new SetupCheckResult
|
||||
{
|
||||
RuntimeReady =
|
||||
runtimeResult.IsValid,
|
||||
|
||||
RuntimeMessage =
|
||||
BuildRuntimeMessage(runtimeResult),
|
||||
|
||||
CertificateReady = false,
|
||||
|
||||
CertificateMessage =
|
||||
certificateResult.Message,
|
||||
|
||||
DatabaseReady = false,
|
||||
|
||||
DatabaseMessage =
|
||||
"Die verschlüsselten SQL-Daten können ohne " +
|
||||
"das Always-Encrypted-Zertifikat nicht gelesen werden.",
|
||||
|
||||
CredentialsReady = false,
|
||||
|
||||
CredentialsMessage =
|
||||
"System und Benutzerprofil konnten nicht geprüft werden."
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IReadOnlyList<SonicSystemOption> systems =
|
||||
await LoadSystemsAsync(
|
||||
cancellationToken);
|
||||
|
||||
LocalSetupSelection? selection =
|
||||
_selectionStore.Load();
|
||||
|
||||
bool systemExists =
|
||||
selection is not null
|
||||
&& string.Equals(
|
||||
selection.EnvironmentCode,
|
||||
_settings.EnvironmentCode,
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
&& systems.Any(
|
||||
system =>
|
||||
system.SonicConnectionId ==
|
||||
selection.SonicConnectionId);
|
||||
|
||||
bool credentialExists = false;
|
||||
|
||||
if (systemExists
|
||||
&& selection is not null)
|
||||
{
|
||||
IReadOnlyList<SonicCredentialProfile> profiles =
|
||||
await LoadCredentialsAsync(
|
||||
selection.SonicConnectionId,
|
||||
cancellationToken);
|
||||
|
||||
credentialExists =
|
||||
profiles.Any(
|
||||
profile =>
|
||||
profile.SonicCredentialId ==
|
||||
selection.SonicCredentialId
|
||||
&& profile.IsComplete);
|
||||
}
|
||||
|
||||
return new SetupCheckResult
|
||||
{
|
||||
RuntimeReady =
|
||||
runtimeResult.IsValid,
|
||||
|
||||
RuntimeMessage =
|
||||
BuildRuntimeMessage(runtimeResult),
|
||||
|
||||
CertificateReady = true,
|
||||
|
||||
CertificateMessage =
|
||||
certificateResult.Message,
|
||||
|
||||
DatabaseReady =
|
||||
systems.Count > 0,
|
||||
|
||||
DatabaseMessage =
|
||||
systems.Count > 0
|
||||
? $"{systems.Count} System(e) für " +
|
||||
$"{_settings.EnvironmentCode} gefunden."
|
||||
: "Für die gewählte Umgebung wurde " +
|
||||
"kein Sonic-System gefunden.",
|
||||
|
||||
CredentialsReady =
|
||||
systemExists
|
||||
&& credentialExists,
|
||||
|
||||
CredentialsMessage =
|
||||
systemExists
|
||||
&& credentialExists
|
||||
? $"System '{selection!.ConnectionName}', " +
|
||||
$"Profil '{selection.CredentialName}' ausgewählt."
|
||||
: "Bitte Sonic-System und Benutzerprofil auswählen."
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new SetupCheckResult
|
||||
{
|
||||
RuntimeReady =
|
||||
runtimeResult.IsValid,
|
||||
|
||||
RuntimeMessage =
|
||||
BuildRuntimeMessage(runtimeResult),
|
||||
|
||||
CertificateReady = true,
|
||||
|
||||
CertificateMessage =
|
||||
certificateResult.Message,
|
||||
|
||||
DatabaseReady = false,
|
||||
|
||||
DatabaseMessage =
|
||||
$"SQL-Prüfung fehlgeschlagen: {ex.Message}",
|
||||
|
||||
CredentialsReady = false,
|
||||
|
||||
CredentialsMessage =
|
||||
"Systemauswahl konnte nicht geprüft werden."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SonicSystemOption>>
|
||||
LoadSystemsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _setupRepository.GetSystemsAsync(
|
||||
_settings.EnvironmentCode,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SonicCredentialProfile>>
|
||||
LoadCredentialsAsync(
|
||||
int sonicConnectionId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _setupRepository.GetCredentialsAsync(
|
||||
sonicConnectionId,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public void SaveSelection(
|
||||
SonicSystemOption system,
|
||||
SonicCredentialProfile profile)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(system);
|
||||
ArgumentNullException.ThrowIfNull(profile);
|
||||
|
||||
if (profile.SonicConnectionId
|
||||
!= system.SonicConnectionId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Das Benutzerprofil gehört nicht zum ausgewählten System.");
|
||||
}
|
||||
|
||||
_selectionStore.Save(
|
||||
new LocalSetupSelection
|
||||
{
|
||||
EnvironmentCode =
|
||||
_settings.EnvironmentCode,
|
||||
|
||||
SonicConnectionId =
|
||||
system.SonicConnectionId,
|
||||
|
||||
SonicCredentialId =
|
||||
profile.SonicCredentialId,
|
||||
|
||||
ConnectionName =
|
||||
system.ConnectionName,
|
||||
|
||||
CredentialName =
|
||||
profile.CredentialName,
|
||||
|
||||
SavedAtUtc =
|
||||
DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
private static string BuildRuntimeMessage(
|
||||
RuntimeValidationResult runtimeResult)
|
||||
{
|
||||
return runtimeResult.IsValid
|
||||
? "Java und Sonic-Runtime sind verfügbar."
|
||||
: string.Join(
|
||||
Environment.NewLine,
|
||||
runtimeResult.Issues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Setup;
|
||||
|
||||
public sealed class SetupWizardForm : Form
|
||||
{
|
||||
private readonly SetupCoordinator _coordinator;
|
||||
|
||||
private readonly Color _backgroundColor =
|
||||
Color.FromArgb(10, 18, 34);
|
||||
|
||||
private readonly Color _cardColor =
|
||||
Color.FromArgb(16, 38, 65);
|
||||
|
||||
private readonly Color _textColor =
|
||||
Color.FromArgb(241, 245, 249);
|
||||
|
||||
private readonly Color _mutedTextColor =
|
||||
Color.FromArgb(169, 184, 200);
|
||||
|
||||
private readonly Color _goldColor =
|
||||
Color.FromArgb(208, 171, 57);
|
||||
|
||||
private readonly Color _greenColor =
|
||||
Color.FromArgb(60, 203, 127);
|
||||
|
||||
private readonly Color _redColor =
|
||||
Color.FromArgb(239, 106, 106);
|
||||
|
||||
private Label _lblRuntime = null!;
|
||||
private Label _lblCertificate = null!;
|
||||
private Label _lblDatabase = null!;
|
||||
private Label _lblSelection = null!;
|
||||
private Label _lblEnvironment = null!;
|
||||
private Label _lblSystemDetails = null!;
|
||||
|
||||
private ComboBox _cmbSystem = null!;
|
||||
private ComboBox _cmbCredential = null!;
|
||||
|
||||
private Button _btnImportCertificate = null!;
|
||||
private Button _btnRefresh = null!;
|
||||
private Button _btnSaveSelection = null!;
|
||||
private Button _btnFinish = null!;
|
||||
private Button _btnAddCredential = null!;
|
||||
|
||||
private IReadOnlyList<SonicSystemOption> _systems = [];
|
||||
private IReadOnlyList<SonicCredentialProfile> _profiles = [];
|
||||
|
||||
private bool _running;
|
||||
|
||||
public SetupWizardForm(
|
||||
SetupCoordinator coordinator)
|
||||
{
|
||||
_coordinator = coordinator;
|
||||
|
||||
BuildInterface();
|
||||
|
||||
Shown += async (_, _) =>
|
||||
await InitializeAsync();
|
||||
}
|
||||
|
||||
private void BuildInterface()
|
||||
{
|
||||
Text = "ESB Certificate Manager einrichten";
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Size = new Size(950, 690);
|
||||
MinimumSize = new Size(900, 650);
|
||||
MaximizeBox = false;
|
||||
|
||||
BackColor = _backgroundColor;
|
||||
ForeColor = _textColor;
|
||||
Font = new Font("Segoe UI", 10);
|
||||
|
||||
Label title = new()
|
||||
{
|
||||
Text = "Ersteinrichtung",
|
||||
AutoSize = true,
|
||||
Location = new Point(38, 25),
|
||||
ForeColor = _textColor,
|
||||
Font = new Font(
|
||||
"Segoe UI",
|
||||
24,
|
||||
FontStyle.Bold)
|
||||
};
|
||||
|
||||
Label subtitle = new()
|
||||
{
|
||||
Text =
|
||||
"Runtime, Datenbank, Zertifikat und Systemauswahl prüfen.",
|
||||
AutoSize = true,
|
||||
Location = new Point(42, 70),
|
||||
ForeColor = _mutedTextColor
|
||||
};
|
||||
|
||||
Panel checkCard = CreateCard(
|
||||
new Point(38, 105),
|
||||
new Size(870, 225));
|
||||
|
||||
Label checkTitle = CreateTitle(
|
||||
"Systemprüfung",
|
||||
new Point(20, 16));
|
||||
|
||||
_lblRuntime = CreateStatus(
|
||||
new Point(20, 55));
|
||||
|
||||
_lblCertificate = CreateStatus(
|
||||
new Point(20, 95));
|
||||
|
||||
_lblDatabase = CreateStatus(
|
||||
new Point(20, 135));
|
||||
|
||||
_lblSelection = CreateStatus(
|
||||
new Point(20, 175));
|
||||
|
||||
_btnImportCertificate = CreateButton(
|
||||
"PFX importieren");
|
||||
|
||||
_btnImportCertificate.Location =
|
||||
new Point(680, 90);
|
||||
|
||||
_btnImportCertificate.Size =
|
||||
new Size(160, 38);
|
||||
|
||||
_btnImportCertificate.Click +=
|
||||
async (_, _) =>
|
||||
await ImportCertificateAsync();
|
||||
_cmbCredential = new ComboBox
|
||||
{
|
||||
Location = new Point(185, 133),
|
||||
Size = new Size(300, 30),
|
||||
DropDownStyle =
|
||||
ComboBoxStyle.DropDownList
|
||||
};
|
||||
|
||||
checkCard.Controls.Add(checkTitle);
|
||||
checkCard.Controls.Add(_lblRuntime);
|
||||
checkCard.Controls.Add(_lblCertificate);
|
||||
checkCard.Controls.Add(_lblDatabase);
|
||||
checkCard.Controls.Add(_lblSelection);
|
||||
checkCard.Controls.Add(_btnImportCertificate);
|
||||
|
||||
Panel selectionCard = CreateCard(
|
||||
new Point(38, 350),
|
||||
new Size(870, 205));
|
||||
|
||||
Label selectionTitle = CreateTitle(
|
||||
"System auswählen",
|
||||
new Point(20, 16));
|
||||
|
||||
_lblEnvironment = new Label
|
||||
{
|
||||
Text =
|
||||
$"Umgebung: {_coordinator.Settings.EnvironmentCode}",
|
||||
AutoSize = true,
|
||||
Location = new Point(22, 55),
|
||||
ForeColor = _goldColor,
|
||||
Font = new Font(
|
||||
"Segoe UI",
|
||||
10,
|
||||
FontStyle.Bold)
|
||||
};
|
||||
|
||||
Label systemLabel = CreateLabel(
|
||||
"Sonic-System",
|
||||
new Point(22, 92));
|
||||
|
||||
_cmbSystem = new ComboBox
|
||||
{
|
||||
Location = new Point(185, 88),
|
||||
Size = new Size(300, 30),
|
||||
DropDownStyle =
|
||||
ComboBoxStyle.DropDownList
|
||||
};
|
||||
|
||||
_cmbSystem.SelectedIndexChanged +=
|
||||
async (_, _) =>
|
||||
await SystemChangedAsync();
|
||||
|
||||
Label credentialLabel = CreateLabel(
|
||||
"Benutzerprofil",
|
||||
new Point(22, 137));
|
||||
|
||||
_cmbCredential = new ComboBox
|
||||
{
|
||||
Location = new Point(185, 133),
|
||||
Size = new Size(300, 30),
|
||||
DropDownStyle =
|
||||
ComboBoxStyle.DropDownList
|
||||
};
|
||||
|
||||
_lblSystemDetails = new Label
|
||||
{
|
||||
Location = new Point(515, 88),
|
||||
Size = new Size(325, 72),
|
||||
ForeColor = _mutedTextColor
|
||||
};
|
||||
|
||||
_btnSaveSelection = CreateButton(
|
||||
"Auswahl übernehmen");
|
||||
|
||||
_btnSaveSelection.Location =
|
||||
new Point(640, 153);
|
||||
|
||||
_btnSaveSelection.Size =
|
||||
new Size(200, 38);
|
||||
|
||||
_btnSaveSelection.Click +=
|
||||
async (_, _) =>
|
||||
await SaveSelectionAsync();
|
||||
_btnAddCredential = CreateButton(
|
||||
"Profil hinzufügen");
|
||||
|
||||
_btnAddCredential.Location =
|
||||
new Point(495, 132);
|
||||
|
||||
_btnAddCredential.Size =
|
||||
new Size(140, 32);
|
||||
|
||||
_btnAddCredential.Click +=
|
||||
async (_, _) =>
|
||||
await AddCredentialAsync();
|
||||
selectionCard.Controls.Add(selectionTitle);
|
||||
selectionCard.Controls.Add(_lblEnvironment);
|
||||
selectionCard.Controls.Add(systemLabel);
|
||||
selectionCard.Controls.Add(_cmbSystem);
|
||||
selectionCard.Controls.Add(credentialLabel);
|
||||
selectionCard.Controls.Add(credentialLabel);
|
||||
selectionCard.Controls.Add(_cmbCredential);
|
||||
selectionCard.Controls.Add(_btnAddCredential);
|
||||
selectionCard.Controls.Add(_lblSystemDetails);
|
||||
selectionCard.Controls.Add(_btnSaveSelection);
|
||||
|
||||
_btnRefresh = CreateButton(
|
||||
"Erneut prüfen");
|
||||
|
||||
_btnRefresh.Location =
|
||||
new Point(525, 580);
|
||||
|
||||
_btnRefresh.Size =
|
||||
new Size(140, 42);
|
||||
|
||||
_btnRefresh.Click +=
|
||||
async (_, _) =>
|
||||
await InitializeAsync();
|
||||
|
||||
Button cancelButton = CreateButton(
|
||||
"Abbrechen");
|
||||
|
||||
cancelButton.Location =
|
||||
new Point(675, 580);
|
||||
|
||||
cancelButton.Size =
|
||||
new Size(110, 42);
|
||||
|
||||
cancelButton.Click += (_, _) =>
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
};
|
||||
|
||||
_btnFinish = CreateButton(
|
||||
"Setup abschließen");
|
||||
|
||||
_btnFinish.Location =
|
||||
new Point(795, 580);
|
||||
|
||||
_btnFinish.Size =
|
||||
new Size(140, 42);
|
||||
|
||||
_btnFinish.Enabled = false;
|
||||
|
||||
_btnFinish.Click += (_, _) =>
|
||||
{
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
};
|
||||
|
||||
Controls.Add(title);
|
||||
Controls.Add(subtitle);
|
||||
Controls.Add(checkCard);
|
||||
Controls.Add(selectionCard);
|
||||
Controls.Add(_btnRefresh);
|
||||
Controls.Add(cancelButton);
|
||||
Controls.Add(_btnFinish);
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetRunning(true);
|
||||
|
||||
try
|
||||
{
|
||||
SetupCheckResult result =
|
||||
await _coordinator.CheckAsync();
|
||||
|
||||
SetStatus(
|
||||
_lblRuntime,
|
||||
"Runtime",
|
||||
result.RuntimeReady,
|
||||
result.RuntimeMessage);
|
||||
|
||||
SetStatus(
|
||||
_lblCertificate,
|
||||
"Always Encrypted",
|
||||
result.CertificateReady,
|
||||
result.CertificateMessage);
|
||||
|
||||
SetStatus(
|
||||
_lblDatabase,
|
||||
"Datenbank",
|
||||
result.DatabaseReady,
|
||||
result.DatabaseMessage);
|
||||
|
||||
SetStatus(
|
||||
_lblSelection,
|
||||
"Auswahl",
|
||||
result.CredentialsReady,
|
||||
result.CredentialsMessage);
|
||||
|
||||
_btnImportCertificate.Visible =
|
||||
!result.CertificateReady;
|
||||
|
||||
_btnFinish.Enabled =
|
||||
result.IsReady;
|
||||
|
||||
if (result.CertificateReady)
|
||||
{
|
||||
await LoadSystemsAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
ex.Message,
|
||||
"Setup fehlgeschlagen",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadSystemsAsync()
|
||||
{
|
||||
LocalSetupSelection? previousSelection =
|
||||
_coordinator.LoadSelection();
|
||||
|
||||
_systems =
|
||||
await _coordinator.LoadSystemsAsync();
|
||||
|
||||
_cmbSystem.Items.Clear();
|
||||
_cmbCredential.Items.Clear();
|
||||
|
||||
foreach (SonicSystemOption system in _systems)
|
||||
{
|
||||
_cmbSystem.Items.Add(system);
|
||||
}
|
||||
|
||||
if (_systems.Count == 0)
|
||||
{
|
||||
_lblSystemDetails.Text =
|
||||
"Kein System für die gewählte Umgebung gefunden.";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int selectedIndex = 0;
|
||||
|
||||
if (previousSelection is not null)
|
||||
{
|
||||
for (int index = 0;
|
||||
index < _systems.Count;
|
||||
index++)
|
||||
{
|
||||
if (_systems[index].SonicConnectionId ==
|
||||
previousSelection.SonicConnectionId)
|
||||
{
|
||||
selectedIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cmbSystem.SelectedIndex = selectedIndex;
|
||||
}
|
||||
private async Task AddCredentialAsync()
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cmbSystem.SelectedItem
|
||||
is not SonicSystemOption system)
|
||||
{
|
||||
ShowWarning(
|
||||
"Bitte zuerst ein Sonic-System auswählen.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
using AddCredentialProfileForm dialog =
|
||||
new(
|
||||
_coordinator,
|
||||
system);
|
||||
|
||||
DialogResult dialogResult =
|
||||
dialog.ShowDialog(this);
|
||||
|
||||
if (dialogResult != DialogResult.OK
|
||||
|| dialog.CreatedProfile is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetRunning(true);
|
||||
|
||||
try
|
||||
{
|
||||
_profiles =
|
||||
await _coordinator.LoadCredentialsAsync(
|
||||
system.SonicConnectionId);
|
||||
|
||||
_cmbCredential.Items.Clear();
|
||||
|
||||
foreach (SonicCredentialProfile profile
|
||||
in _profiles)
|
||||
{
|
||||
_cmbCredential.Items.Add(profile);
|
||||
}
|
||||
|
||||
for (int index = 0;
|
||||
index < _cmbCredential.Items.Count;
|
||||
index++)
|
||||
{
|
||||
if (_cmbCredential.Items[index]
|
||||
is SonicCredentialProfile profile
|
||||
&& profile.SonicCredentialId
|
||||
== dialog.CreatedProfile.SonicCredentialId)
|
||||
{
|
||||
_cmbCredential.SelectedIndex =
|
||||
index;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_lblSystemDetails.Text =
|
||||
$"Domain: {system.DomainName}"
|
||||
+ Environment.NewLine
|
||||
+ $"Adresse: {system.ConnectionUrl}"
|
||||
+ Environment.NewLine
|
||||
+ $"Profil: {dialog.CreatedProfile.CredentialName}";
|
||||
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Das geprüfte Benutzerprofil wurde geladen. " +
|
||||
"Klicke jetzt auf „Auswahl übernehmen“.",
|
||||
"Benutzerprofil hinzugefügt",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
ex.Message,
|
||||
"Benutzerprofile konnten nicht neu geladen werden",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetRunning(false);
|
||||
}
|
||||
}
|
||||
private async Task SystemChangedAsync()
|
||||
{
|
||||
if (_cmbSystem.SelectedItem
|
||||
is not SonicSystemOption system)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lblSystemDetails.Text =
|
||||
$"Domain: {system.DomainName}" +
|
||||
Environment.NewLine +
|
||||
$"Adresse: {system.ConnectionUrl}";
|
||||
|
||||
_profiles =
|
||||
await _coordinator.LoadCredentialsAsync(
|
||||
system.SonicConnectionId);
|
||||
|
||||
_cmbCredential.Items.Clear();
|
||||
|
||||
foreach (SonicCredentialProfile profile
|
||||
in _profiles)
|
||||
{
|
||||
_cmbCredential.Items.Add(profile);
|
||||
}
|
||||
|
||||
if (_profiles.Count == 0)
|
||||
{
|
||||
_lblSystemDetails.Text +=
|
||||
Environment.NewLine
|
||||
+ "Kein Benutzerprofil vorhanden. "
|
||||
+ "Bitte ein neues Profil hinzufügen.";
|
||||
|
||||
_btnSaveSelection.Enabled = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_btnSaveSelection.Enabled = true;
|
||||
|
||||
LocalSetupSelection? selection =
|
||||
_coordinator.LoadSelection();
|
||||
|
||||
int selectedIndex = 0;
|
||||
|
||||
if (selection is not null)
|
||||
{
|
||||
for (int index = 0;
|
||||
index < _profiles.Count;
|
||||
index++)
|
||||
{
|
||||
if (_profiles[index].SonicCredentialId ==
|
||||
selection.SonicCredentialId)
|
||||
{
|
||||
selectedIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int defaultIndex =
|
||||
_profiles
|
||||
.Select(
|
||||
(profile, index) =>
|
||||
new
|
||||
{
|
||||
profile,
|
||||
index
|
||||
})
|
||||
.Where(item =>
|
||||
item.profile.IsDefault)
|
||||
.Select(item => item.index)
|
||||
.FirstOrDefault();
|
||||
|
||||
selectedIndex = defaultIndex;
|
||||
}
|
||||
|
||||
_cmbCredential.SelectedIndex =
|
||||
selectedIndex;
|
||||
}
|
||||
|
||||
private async Task SaveSelectionAsync()
|
||||
{
|
||||
if (_cmbSystem.SelectedItem
|
||||
is not SonicSystemOption system)
|
||||
{
|
||||
ShowWarning(
|
||||
"Bitte ein Sonic-System auswählen.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cmbCredential.SelectedItem
|
||||
is not SonicCredentialProfile profile)
|
||||
{
|
||||
ShowWarning(
|
||||
"Bitte ein Benutzerprofil auswählen.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_coordinator.SaveSelection(
|
||||
system,
|
||||
profile);
|
||||
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"System und Benutzerprofil wurden ausgewählt.",
|
||||
"Setup",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
|
||||
await InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task ImportCertificateAsync()
|
||||
{
|
||||
using OpenFileDialog dialog = new()
|
||||
{
|
||||
Title =
|
||||
"Always-Encrypted-PFX auswählen",
|
||||
Filter =
|
||||
"PFX-Zertifikat (*.pfx)|*.pfx",
|
||||
CheckFileExists = true,
|
||||
Multiselect = false
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog(this)
|
||||
!= DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? password =
|
||||
PromptForPassword();
|
||||
|
||||
if (password is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_coordinator.CertificateService.ImportPfx(
|
||||
dialog.FileName,
|
||||
password);
|
||||
|
||||
password = string.Empty;
|
||||
|
||||
await InitializeAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
password = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? PromptForPassword()
|
||||
{
|
||||
using Form form = new()
|
||||
{
|
||||
Text = "PFX-Kennwort",
|
||||
Size = new Size(390, 160),
|
||||
StartPosition =
|
||||
FormStartPosition.CenterParent,
|
||||
FormBorderStyle =
|
||||
FormBorderStyle.FixedDialog
|
||||
};
|
||||
|
||||
TextBox textBox = new()
|
||||
{
|
||||
Location = new Point(15, 35),
|
||||
Width = 340,
|
||||
UseSystemPasswordChar = true
|
||||
};
|
||||
|
||||
Button ok = new()
|
||||
{
|
||||
Text = "OK",
|
||||
Location = new Point(190, 75),
|
||||
DialogResult = DialogResult.OK
|
||||
};
|
||||
|
||||
Button cancel = new()
|
||||
{
|
||||
Text = "Abbrechen",
|
||||
Location = new Point(275, 75),
|
||||
DialogResult = DialogResult.Cancel
|
||||
};
|
||||
|
||||
form.Controls.Add(textBox);
|
||||
form.Controls.Add(ok);
|
||||
form.Controls.Add(cancel);
|
||||
|
||||
form.AcceptButton = ok;
|
||||
form.CancelButton = cancel;
|
||||
|
||||
return form.ShowDialog()
|
||||
== DialogResult.OK
|
||||
? textBox.Text
|
||||
: null;
|
||||
}
|
||||
|
||||
private void SetRunning(bool running)
|
||||
{
|
||||
_running = running;
|
||||
|
||||
_cmbSystem.Enabled = !running;
|
||||
_cmbCredential.Enabled = !running;
|
||||
_btnAddCredential.Enabled = !running;
|
||||
_btnSaveSelection.Enabled = !running;
|
||||
_btnRefresh.Enabled = !running;
|
||||
_btnImportCertificate.Enabled = !running;
|
||||
|
||||
Cursor =
|
||||
running
|
||||
? Cursors.WaitCursor
|
||||
: Cursors.Default;
|
||||
}
|
||||
|
||||
private void ShowWarning(string message)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
message,
|
||||
"Setup",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
private Panel CreateCard(
|
||||
Point location,
|
||||
Size size)
|
||||
{
|
||||
return new Panel
|
||||
{
|
||||
Location = location,
|
||||
Size = size,
|
||||
BackColor = _cardColor,
|
||||
BorderStyle =
|
||||
BorderStyle.FixedSingle
|
||||
};
|
||||
}
|
||||
|
||||
private Label CreateTitle(
|
||||
string text,
|
||||
Point location)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
Location = location,
|
||||
AutoSize = true,
|
||||
ForeColor = _textColor,
|
||||
Font = new Font(
|
||||
"Segoe UI",
|
||||
14,
|
||||
FontStyle.Bold)
|
||||
};
|
||||
}
|
||||
|
||||
private Label CreateLabel(
|
||||
string text,
|
||||
Point location)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
Location = location,
|
||||
AutoSize = true,
|
||||
ForeColor = _mutedTextColor
|
||||
};
|
||||
}
|
||||
|
||||
private Label CreateStatus(
|
||||
Point location)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Location = location,
|
||||
Size = new Size(620, 32),
|
||||
ForeColor = _mutedTextColor
|
||||
};
|
||||
}
|
||||
|
||||
private Button CreateButton(string text)
|
||||
{
|
||||
return new Button
|
||||
{
|
||||
Text = text,
|
||||
BackColor = _goldColor,
|
||||
ForeColor =
|
||||
Color.FromArgb(30, 25, 10),
|
||||
FlatStyle = FlatStyle.Flat,
|
||||
Font = new Font(
|
||||
"Segoe UI",
|
||||
9,
|
||||
FontStyle.Bold),
|
||||
Cursor = Cursors.Hand
|
||||
};
|
||||
}
|
||||
|
||||
private void SetStatus(
|
||||
Label label,
|
||||
string category,
|
||||
bool success,
|
||||
string message)
|
||||
{
|
||||
label.Text =
|
||||
$"{(success ? "✓" : "✗")} " +
|
||||
$"{category}: {message}";
|
||||
|
||||
label.ForeColor =
|
||||
success
|
||||
? _greenColor
|
||||
: _redColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
Reference in New Issue
Block a user