diff --git a/client-dotnet/Deploymentcenter.Client/Models/SetupDefinition.cs b/client-dotnet/Deploymentcenter.Client/Models/SetupDefinition.cs
index 9a15fdb..80c4fef 100644
--- a/client-dotnet/Deploymentcenter.Client/Models/SetupDefinition.cs
+++ b/client-dotnet/Deploymentcenter.Client/Models/SetupDefinition.cs
@@ -51,6 +51,34 @@ namespace Deploymentcenter.Client.Models
[JsonPropertyName("file")]
public string File { get; set; } = string.Empty;
+ ///
+ /// Abweichender Pfad unter Windows.
+ ///
+ /// Konfigurationsordner heissen plattformabhaengig unterschiedlich:
+ /// unter Windows ist "%APPDATA%\MeineAnwendung" ueblich, unter Linux
+ /// "$XDG_CONFIG_HOME/meineanwendung" klein. Meist genuegt es, die
+ /// kleingeschriebene Form anzugeben - NTFS unterscheidet nicht nach
+ /// Gross- und Kleinschreibung, umgekehrt entstuende unter Linux ein
+ /// zweites, leeres Verzeichnis neben dem, aus dem die Anwendung liest.
+ ///
+ /// Das traegt aber nur, solange das Dateisystem tatsaechlich
+ /// unempfindlich ist - auf APFS mit Beachtung der Schreibweise oder
+ /// einem per fsutil umgestellten NTFS-Ordner nicht mehr. Und es hilft
+ /// gar nicht, wenn sich die Namen um mehr als die Schreibweise
+ /// unterscheiden. Fuer diese Faelle sind die folgenden Felder da; ohne
+ /// Angabe gilt .
+ ///
+ [JsonPropertyName("fileWindows")]
+ public string FileWindows { get; set; } = string.Empty;
+
+ /// Abweichender Pfad unter Linux.
+ [JsonPropertyName("fileLinux")]
+ public string FileLinux { get; set; } = string.Empty;
+
+ /// Abweichender Pfad unter macOS.
+ [JsonPropertyName("fileMacOS")]
+ public string FileMacOS { get; set; } = string.Empty;
+
///
/// Wo die Datei liegt:
///
diff --git a/client-dotnet/Deploymentcenter.Client/SetupPaths.cs b/client-dotnet/Deploymentcenter.Client/SetupPaths.cs
index 684cd85..750e63b 100644
--- a/client-dotnet/Deploymentcenter.Client/SetupPaths.cs
+++ b/client-dotnet/Deploymentcenter.Client/SetupPaths.cs
@@ -24,7 +24,7 @@ namespace Deploymentcenter.Client
/// Installationsverzeichnis der Anwendung.
public static string Resolve(SetupTarget target, string installDir)
{
- string file = Expand(target?.File ?? string.Empty);
+ string file = Expand(FileFor(target));
// Ein absoluter Pfad ist bereits die Antwort - etwa nachdem
// ${XDG_CONFIG_HOME} ersetzt wurde.
@@ -37,6 +37,28 @@ namespace Deploymentcenter.Client
return Path.GetFullPath(Path.Combine(root, file.Replace('/', Path.DirectorySeparatorChar)));
}
+ ///
+ /// Waehlt den Pfad fuer die laufende Plattform.
+ ///
+ /// Konfigurationsordner heissen plattformabhaengig unterschiedlich -
+ /// "ClawdDotNet" unter Windows, "clawddotnet" unter Linux. Die
+ /// kleingeschriebene Form allein traegt nur, solange das Dateisystem
+ /// die Schreibweise ignoriert; auf einem case-sensitiven Volume
+ /// entstuende sonst ein zweites, leeres Verzeichnis neben dem, aus dem
+ /// die Anwendung liest.
+ ///
+ public static string FileFor(SetupTarget? target)
+ {
+ if (target == null)
+ return string.Empty;
+
+ string specific = OperatingSystemHelpers.IsWindows() ? target.FileWindows
+ : OperatingSystemHelpers.IsMacOS() ? target.FileMacOS
+ : target.FileLinux;
+
+ return string.IsNullOrWhiteSpace(specific) ? (target.File ?? string.Empty) : specific;
+ }
+
///
/// Wurzelverzeichnis fuer eine Ortsangabe.
///
diff --git a/client-dotnet/Deploymentcenter.Client/SetupWriter.cs b/client-dotnet/Deploymentcenter.Client/SetupWriter.cs
index db99473..6b9cac9 100644
--- a/client-dotnet/Deploymentcenter.Client/SetupWriter.cs
+++ b/client-dotnet/Deploymentcenter.Client/SetupWriter.cs
@@ -12,11 +12,14 @@ namespace Deploymentcenter.Client
/// Ein zu schreibender Konfigurationswert.
public sealed class SetupValue
{
- public SetupValue(string key, string value, string type = "string")
+ private readonly bool? _isSecret;
+
+ public SetupValue(string key, string value, string type = "string", bool? isSecret = null)
{
Key = key;
Value = value;
Type = type;
+ _isSecret = isSecret;
}
/// Schluessel, Ebenen durch Doppelpunkt getrennt.
@@ -26,6 +29,15 @@ namespace Deploymentcenter.Client
/// string | secret | url | int | bool | enum
public string Type { get; }
+
+ ///
+ /// Ist der Wert schutzbeduerftig? Ohne ausdrueckliche Angabe wird der
+ /// Typ herangezogen. Ein per source: "provision" geholtes Token
+ /// traegt haeufig den Typ "string" und muss deshalb gesondert
+ /// gekennzeichnet werden.
+ ///
+ public bool IsSecret =>
+ _isSecret ?? string.Equals(Type, "secret", StringComparison.OrdinalIgnoreCase);
}
///
@@ -137,13 +149,68 @@ namespace Deploymentcenter.Client
/// Schreibt in das Format, das zum Ziel passt.
public static void Write(string path, string format, IEnumerable values)
{
+ var list = values as IList ?? new List(values);
+
if (string.Equals(format, "env", StringComparison.OrdinalIgnoreCase))
{
- WriteEnv(path, values);
- return;
+ WriteEnv(path, list);
+ }
+ else
+ {
+ WriteJson(path, list);
}
- WriteJson(path, values);
+ // Enthaelt die Datei Geheimnisse, gehoert sie niemandem sonst.
+ foreach (var value in list)
+ {
+ if (value.IsSecret)
+ {
+ RestrictPermissions(path);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Beschraenkt die Datei auf den eigenen Benutzer.
+ ///
+ /// Ohne das schreibt .NET mit der Standardmaske - unter Linux
+ /// ueblicherweise 644, also weltlesbar. In dieser Datei stehen der
+ /// Lizenzschluessel und das Token der Anwendung; jeder Benutzer des
+ /// Systems koennte sie lesen. Der Lizenz-Cache in
+ /// wird aus demselben Grund auf 600 gesetzt.
+ ///
+ /// Unter Windows erben Dateien in %APPDATA% die Zugriffsrechte des
+ /// Profils; dort ist nichts zu tun.
+ ///
+ public static void RestrictPermissions(string path)
+ {
+ if (OperatingSystemHelpers.IsWindows())
+ return;
+
+#if NET8_0_OR_GREATER
+ try
+ {
+ File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
+ }
+ catch
+ {
+ // Auf Dateisystemen ohne Unix-Rechte schlicht nicht anwendbar.
+ }
+#else
+ try
+ {
+ var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "chmod",
+ Arguments = $"600 \"{path}\"",
+ UseShellExecute = false,
+ CreateNoWindow = true
+ });
+ proc?.WaitForExit();
+ }
+ catch { }
+#endif
}
///
diff --git a/client-dotnet/Deploymentcenter.UpdateAgent/Installation.cs b/client-dotnet/Deploymentcenter.UpdateAgent/Installation.cs
index 14522f7..fb2427c 100644
--- a/client-dotnet/Deploymentcenter.UpdateAgent/Installation.cs
+++ b/client-dotnet/Deploymentcenter.UpdateAgent/Installation.cs
@@ -300,7 +300,11 @@ namespace Deploymentcenter.UpdateAgent
{
var forTarget = resolved
.Where(pair => BelongsToTarget(pair.Field, target, targets))
- .Select(pair => new SetupValue(pair.Field.Key, pair.Value, pair.Field.Type))
+ // IsSecret ausdruecklich weiterreichen: ein per provision
+ // geholtes Token traegt oft den Typ "string", muss die
+ // Datei aber trotzdem auf den eigenen Benutzer beschraenken.
+ .Select(pair => new SetupValue(
+ pair.Field.Key, pair.Value, pair.Field.Type, pair.Field.IsSecret))
.ToList();
if (forTarget.Count == 0)
diff --git a/docs/SETUP_INTEGRATION_GUIDE.md b/docs/SETUP_INTEGRATION_GUIDE.md
index 4f4dc20..de0578f 100644
--- a/docs/SETUP_INTEGRATION_GUIDE.md
+++ b/docs/SETUP_INTEGRATION_GUIDE.md
@@ -219,6 +219,54 @@ Platzhalter fällt auf, ein stillschweigend falscher Pfad nicht.
> deshalb den **aufgelösten Pfad** aus — prüfe ihn. Für Dienste ist ein
> absoluter Pfad oft die ehrlichere Angabe.
+#### Wenn die Schreibweise plattformabhängig ist
+
+Konfigurationsordner heißen nicht überall gleich: `%APPDATA%\MeineAnwendung`
+ist unter Windows üblich, `$XDG_CONFIG_HOME/meineanwendung` unter Linux klein.
+
+Meistens genügt es, die **kleingeschriebene Form** anzugeben. Sie trifft unter
+Linux exakt und unter Windows ebenfalls, weil NTFS die Schreibweise ignoriert.
+Andersherum funktioniert es nicht — unter Linux entstünde ein zweites, leeres
+Verzeichnis neben dem, aus dem die Anwendung liest.
+
+Dieser Kniff trägt aber nur, solange das Dateisystem wirklich unempfindlich
+ist. Auf APFS mit Beachtung der Schreibweise, auf einem per `fsutil`
+umgestellten NTFS-Ordner oder wenn sich die Namen um **mehr als die
+Schreibweise** unterscheiden, braucht es eine eigene Angabe:
+
+```json
+{
+ "id": "user",
+ "file": "meineanwendung/Settings.json",
+ "fileWindows": "MeineAnwendung/Settings.json",
+ "location": "config"
+}
+```
+
+`fileLinux` und `fileMacOS` gibt es ebenso. Ohne Angabe gilt `file`.
+
+### Dateirechte
+
+Enthält ein Ziel mindestens einen als geheim gekennzeichneten Wert, wird die
+geschriebene Datei auf den eigenen Benutzer beschränkt (`600` unter Linux und
+macOS). Ohne das schriebe .NET mit der Standardmaske, unter Linux üblicherweise
+`644` — und in dieser Datei stehen Lizenzschlüssel und Anwendungstoken.
+
+Als geheim gilt jedes Feld mit `type: "secret"` **und** jedes mit
+`source: "provision"` — ein so geholtes Token trägt oft den Typ `string`, ist
+aber genauso schutzbedürftig.
+
+Unter Windows geschieht nichts: Dateien in `%APPDATA%` erben die
+Zugriffsrechte des Profils.
+
+> **Der Installer verschlüsselt nicht.** Er schreibt Klartext; ob und wie
+> Geheimnisse anschließend geschützt werden, entscheidet die Anwendung — etwa
+> über DPAPI beim ersten Speichern. Das setzt voraus, dass eure
+> Entschlüsselung Klartext unverändert durchreicht, statt an ihm zu scheitern.
+> Wer eine solche Weiche hat, sollte sie im Test als **aktiv genutzt**
+> beschreiben und nicht als Altlast — sonst wird sie eines Tages als tot
+> entfernt und sperrt jede frisch installierte Instanz aus.
+
`type` bestimmt auch, **wie** geschrieben wird: `int` und `bool` landen als
JSON-Zahl beziehungsweise -Wahrheitswert, nicht als Zeichenkette. Sonst
scheitert die Bindung in der Anwendung.
diff --git a/docs/setup.example.json b/docs/setup.example.json
index d80abff..f7b0c19 100644
--- a/docs/setup.example.json
+++ b/docs/setup.example.json
@@ -6,9 +6,16 @@
"description": "Fuer den Betrieb werden eine Datenbank und ein Zugang zum Kursdatenanbieter benoetigt.",
"_targets_comment": "Wohin geschrieben wird. 'location' bestimmt den Bezugspunkt: install (Vorgabe, Installationsverzeichnis), config (%APPDATA% bzw. $XDG_CONFIG_HOME), data, home. In 'file' sind ${VAR} und %VAR% erlaubt, ein absoluter Pfad wird unveraendert benutzt. format ist 'json' oder 'env'. Ueber 'id' lassen sich einzelne Felder gezielt einer Datei zuordnen; ohne Angabe schreibt ein Feld in das erste Ziel.",
+ "_platform_comment": "Heisst der Ordner je nach Plattform anders, hilft meist die kleingeschriebene Form allein: sie trifft unter Linux exakt und unter Windows auch, weil NTFS die Schreibweise ignoriert. Auf case-sensitiven Dateisystemen oder bei groesseren Unterschieden gibt es fileWindows, fileLinux und fileMacOS.",
"targets": [
{ "id": "app", "file": "appsettings.json", "location": "install", "format": "json" },
- { "id": "user", "file": "beispielanwendung/Settings.json", "location": "config", "format": "json" },
+ {
+ "id": "user",
+ "file": "beispielanwendung/Settings.json",
+ "fileWindows": "Beispielanwendung/Settings.json",
+ "location": "config",
+ "format": "json"
+ },
{ "id": "env", "file": ".env", "location": "install", "format": "env" }
],