Files
Deploymentcenter BotandClaude Opus 5 56b1d2631a fix(setup): Dateirechte bei Geheimnissen, plattformabhaengige Zielnamen
Zwei Beobachtungen aus der laufenden Integration.

1. Der Installer schrieb Geheimnisse weltlesbar
   SetupWriter benutzte File.WriteAllText ohne Rechteanpassung - unter Linux
   also die Standardmaske und damit ueblicherweise 644. In genau dieser Datei
   stehen Lizenzschluessel und Anwendungstoken; jeder Benutzer des Systems
   konnte sie lesen. Der Lizenz-Cache in StateStore wird aus demselben Grund
   seit jeher auf 600 gesetzt - der Installer zog nicht nach.
   Enthaelt ein Ziel mindestens einen geheimen Wert, wird die Datei jetzt auf
   den eigenen Benutzer beschraenkt. Als geheim gilt type=secret UND
   source=provision: ein so geholtes Token traegt oft den Typ "string", ist
   aber genauso schutzbeduerftig. Unter Windows bleibt es beim Profil-ACL.

2. Zielnamen koennen plattformabhaengig unterschiedlich sein
   %APPDATA%\MeineAnwendung gegen $XDG_CONFIG_HOME/meineanwendung - eine
   setup.json kannte nur eine Schreibweise. Die kleingeschriebene Form allein
   traegt, weil NTFS die Schreibweise ignoriert, aber nur solange das
   Dateisystem tatsaechlich unempfindlich ist; auf APFS mit Beachtung der
   Schreibweise oder bei groesseren Unterschieden entstuende ein zweites,
   leeres Verzeichnis neben dem, aus dem die Anwendung liest.
   Ziele haben deshalb optional fileWindows, fileLinux und fileMacOS; ohne
   Angabe gilt weiterhin file.

Die Dokumentation haelt ausserdem fest, dass der Installer bewusst Klartext
schreibt und die Anwendung selbst entscheidet, ob und wie sie ihn danach
schuetzt - und dass eine Entschluesselung, die Klartext durchreicht, deshalb
kein Altlast-Zweig mehr ist, sondern ein aktiv genutzter Pfad.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:37:01 +02:00

177 lines
7.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Deploymentcenter.Client.Models
{
/// <summary>
/// Beschreibt, was eine Anwendung zum Laufen braucht - als Datei
/// <c>setup.json</c> im Wurzelverzeichnis des Pakets, neben der
/// manifest.json.
///
/// Bewusst im Paket und nicht zentral im Deploymentcenter: so ist die
/// Beschreibung mit der Anwendung versioniert. Braucht Version 2.0 ein
/// Feld mehr als 1.9, stimmt es automatisch - eine zweite Pflegestelle
/// wuerde frueher oder spaeter auseinanderlaufen.
///
/// Fehlt die Datei, laesst sich die Anwendung trotzdem installieren; der
/// Installer entpackt sie dann nur und fragt nichts ab.
/// </summary>
public class SetupDefinition
{
/// <summary>Format-Version dieser Datei.</summary>
[JsonPropertyName("schema")]
public int Schema { get; set; } = 1;
/// <summary>Anzeigename, sonst wird der Projekt-Slug verwendet.</summary>
[JsonPropertyName("displayName")]
public string DisplayName { get; set; } = string.Empty;
/// <summary>Kurzer Hinweistext, der vor der Abfrage angezeigt wird.</summary>
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
/// <summary>Wohin die ermittelten Werte geschrieben werden.</summary>
[JsonPropertyName("targets")]
public List<SetupTarget> Targets { get; set; } = new List<SetupTarget>();
/// <summary>Die benoetigten Werte.</summary>
[JsonPropertyName("fields")]
public List<SetupField> Fields { get; set; } = new List<SetupField>();
}
/// <summary>Eine Datei, in die Werte geschrieben werden.</summary>
public class SetupTarget
{
/// <summary>
/// Pfad, bezogen auf <see cref="Location"/>. Umgebungsvariablen sind
/// in beiden Schreibweisen erlaubt: <c>${XDG_CONFIG_HOME}</c> und
/// <c>%APPDATA%</c>. Ein absoluter Pfad wird unveraendert benutzt.
/// </summary>
[JsonPropertyName("file")]
public string File { get; set; } = string.Empty;
/// <summary>
/// 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 <see cref="File"/>.
/// </summary>
[JsonPropertyName("fileWindows")]
public string FileWindows { get; set; } = string.Empty;
/// <summary>Abweichender Pfad unter Linux.</summary>
[JsonPropertyName("fileLinux")]
public string FileLinux { get; set; } = string.Empty;
/// <summary>Abweichender Pfad unter macOS.</summary>
[JsonPropertyName("fileMacOS")]
public string FileMacOS { get; set; } = string.Empty;
/// <summary>
/// Wo die Datei liegt:
///
/// install (Vorgabe) - im Installationsverzeichnis
/// config - %APPDATA% bzw. $XDG_CONFIG_HOME (~/.config)
/// data - %LOCALAPPDATA% bzw. $XDG_DATA_HOME
/// home - Benutzerverzeichnis
///
/// Anwendungen, die sich unter Linux richtig verhalten, legen ihre
/// Konfiguration nicht neben das Programm - /opt/&lt;app&gt; ist fuer den
/// Dienstbenutzer meist nicht schreibbar. Ohne diese Angabe schriebe
/// der Installer dorthin, wo die Anwendung nie nachsieht.
/// </summary>
[JsonPropertyName("location")]
public string Location { get; set; } = "install";
/// <summary>"json" oder "env".</summary>
[JsonPropertyName("format")]
public string Format { get; set; } = "json";
/// <summary>
/// Kennung, ueber die Felder dieser Datei zugeordnet werden. Ohne
/// Angabe schreiben alle Felder in das erste Ziel.
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
}
/// <summary>Ein einzelner Konfigurationswert.</summary>
public class SetupField
{
/// <summary>
/// Schluessel im Ziel. Bei JSON trennt ein Doppelpunkt die Ebenen
/// ("ConnectionStrings:Main"), passend zur Schreibweise von
/// Microsoft.Extensions.Configuration.
/// </summary>
[JsonPropertyName("key")]
public string Key { get; set; } = string.Empty;
/// <summary>Beschriftung fuer die Abfrage.</summary>
[JsonPropertyName("label")]
public string Label { get; set; } = string.Empty;
/// <summary>Erlaeuterung, die unter der Frage steht.</summary>
[JsonPropertyName("help")]
public string Help { get; set; } = string.Empty;
/// <summary>string | secret | url | int | bool | enum</summary>
[JsonPropertyName("type")]
public string Type { get; set; } = "string";
/// <summary>Muss ein Wert vorliegen?</summary>
[JsonPropertyName("required")]
public bool Required { get; set; } = true;
/// <summary>Vorbelegung.</summary>
[JsonPropertyName("default")]
public string Default { get; set; } = string.Empty;
/// <summary>
/// Woher der Wert kommt, wenn nicht gefragt werden soll:
/// detect:hostname | detect:platform | detect:installdir | detect:username
/// provision - Token beim Deploymentcenter anfordern
/// ask - Vorgabe: nachfragen
/// </summary>
[JsonPropertyName("source")]
public string Source { get; set; } = "ask";
/// <summary>Rechte fuer source = provision.</summary>
[JsonPropertyName("scopes")]
public List<string> Scopes { get; set; } = new List<string>();
/// <summary>Auswahlmoeglichkeiten fuer type = enum.</summary>
[JsonPropertyName("options")]
public List<string> Options { get; set; } = new List<string>();
/// <summary>Regulaerer Ausdruck, gegen den der Wert geprueft wird.</summary>
[JsonPropertyName("validate")]
public string Validate { get; set; } = string.Empty;
/// <summary>Meldung, wenn die Pruefung fehlschlaegt.</summary>
[JsonPropertyName("validationMessage")]
public string ValidationMessage { get; set; } = string.Empty;
/// <summary>Kennung des Ziels aus <see cref="SetupTarget.Id"/>.</summary>
[JsonPropertyName("target")]
public string Target { get; set; } = string.Empty;
/// <summary>Wird der Wert verdeckt eingegeben und nicht angezeigt?</summary>
[JsonIgnore]
public bool IsSecret =>
string.Equals(Type, "secret", StringComparison.OrdinalIgnoreCase)
|| string.Equals(Source, "provision", StringComparison.OrdinalIgnoreCase);
}
}