Authenticated encryption for enterprise secrets (#1078)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-17 20:36:34 +02:00
1 parent 031874cebf
commit 274d718314
26 files changed
+740 -89

No files matched your search

@@ -0,0 +1,135 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.Settings;
/// <summary>
/// Checks in which format an installation exports encrypted secrets, depending on whether it
/// existed before the format became a choice.
/// </summary>
/// <remarks>
/// Large organizations package updates themselves, so their administrators run a new version weeks
/// before everybody else. Exports in the authenticated format v2 would be unreadable for the rest
/// of such an organization meanwhile. An installation which existed before v26.10.1 therefore keeps
/// v1, and only a new one starts with v2. Keeping v1 is a compatibility shim, see
/// documentation/compatibility-shims/2026-10-enterprise-encryption-export-format.md.
/// </remarks>
[TestFixture]
[NonParallelizable]
public sealed class EnterpriseEncryptionExportFormatTests
{
private const string SETTINGS_FILENAME = "settings.json";
private string? previousConfigDirectory;
private string? previousDataDirectory;
private string testDirectory = string.Empty;
private string SettingsPath => Path.Combine(this.testDirectory, SETTINGS_FILENAME);
[SetUp]
public void PrepareTestDirectory()
{
// Both directories are static state of the whole application, which is why this fixture
// does not run alongside others:
this.previousConfigDirectory = SettingsManager.ConfigDirectory;
this.previousDataDirectory = SettingsManager.DataDirectory;
this.testDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-encryption-format-{Guid.NewGuid():N}");
Directory.CreateDirectory(this.testDirectory);
SettingsManager.ConfigDirectory = this.testDirectory;
SettingsManager.DataDirectory = this.testDirectory;
}
[TearDown]
public void RemoveTestDirectory()
{
SettingsManager.ConfigDirectory = this.previousConfigDirectory;
SettingsManager.DataDirectory = this.previousDataDirectory;
try
{
Directory.Delete(this.testDirectory, true);
}
catch (IOException)
{
// A temporary directory we could not remove says nothing about the code under test.
}
}
[Test]
public async Task ANewInstallationStartsWithTheAuthenticatedFormat()
{
var settingsManager = CreateSettingsManager();
await settingsManager.LoadSettings();
Assert.Multiple(() =>
{
Assert.That(File.Exists(this.SettingsPath), Is.False, "A new installation has no settings file yet.");
Assert.That(settingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, Is.EqualTo(EnterpriseEncryptionFormat.V2));
});
}
[Test]
public async Task SettingsFromBeforeTheChoiceKeepTheLegacyFormat()
{
var settings = SerializeToNode(new Data());
settings["App"]!.AsObject().Remove(nameof(DataApp.EnterpriseEncryptionExportFormat));
await File.WriteAllTextAsync(this.SettingsPath, settings.ToJsonString(SettingsManager.JSON_OPTIONS));
var settingsManager = CreateSettingsManager();
await settingsManager.LoadSettings();
await settingsManager.StoreSettings();
Assert.Multiple(() =>
{
Assert.That(settingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, Is.EqualTo(EnterpriseEncryptionFormat.V1));
Assert.That(this.ReadStoredFormat(), Is.EqualTo(nameof(EnterpriseEncryptionFormat.V1)), "Once stored, the settings hold the format, and the shim has nothing left to do.");
});
}
[Test]
public async Task SettingsOfAnOlderVersionKeepTheLegacyFormat()
{
await File.WriteAllTextAsync(this.SettingsPath, """{"Version": "V5"}""");
var settingsManager = CreateSettingsManager();
await settingsManager.LoadSettings();
Assert.That(settingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, Is.EqualTo(EnterpriseEncryptionFormat.V1));
}
[TestCase(EnterpriseEncryptionFormat.V1)]
[TestCase(EnterpriseEncryptionFormat.V2)]
public async Task AChosenFormatStays(EnterpriseEncryptionFormat format)
{
var data = new Data();
data.App.EnterpriseEncryptionExportFormat = format;
await File.WriteAllTextAsync(this.SettingsPath, SerializeToNode(data).ToJsonString(SettingsManager.JSON_OPTIONS));
var settingsManager = CreateSettingsManager();
await settingsManager.LoadSettings();
Assert.That(settingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, Is.EqualTo(format));
}
/// <remarks>
/// The rust service is handed in as null on purpose: neither storing nor reading settings ever
/// asks it anything.
/// </remarks>
private static SettingsManager CreateSettingsManager() => new(NullLogger<SettingsManager>.Instance, null!);
private static JsonObject SerializeToNode(Data data) => JsonSerializer.SerializeToNode(data, SettingsManager.JSON_OPTIONS)!.AsObject();
private string? ReadStoredFormat()
{
using var settingsDocument = JsonDocument.Parse(File.ReadAllText(this.SettingsPath));
return settingsDocument.RootElement.GetProperty("App").GetProperty(nameof(DataApp.EnterpriseEncryptionExportFormat)).GetString();
}
}