From 398617391b583d1dc0074e98b8623076a10a2526 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 28 May 2026 17:39:05 +0100 Subject: [PATCH] Fix SSO auto-login and custom metadata settings not persisting on restart (#6468) --- .../configuration/ConfigInitializer.java | 32 ++++++- .../configuration/ConfigInitializerTest.java | 95 +++++++++++++++++++ .../common/util/GeneralUtilsTest.java | 40 ++++++++ .../src/main/resources/settings.yml.template | 4 +- .../ConfigInitializerRestartTest.java | 93 ++++++++++++++++++ testing/allEndpointsRemovedSettings.yml | 4 +- 6 files changed, 259 insertions(+), 9 deletions(-) create mode 100644 app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java create mode 100644 app/core/src/test/java/stirling/software/common/configuration/ConfigInitializerRestartTest.java diff --git a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java index 54e42504c..4b3c237ec 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java +++ b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java @@ -80,6 +80,7 @@ public class ConfigInitializer { YamlHelper settingsFile = new YamlHelper(settingTempPath); migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile); + migrateProFeaturesKeyCasing(settingsFile, settingsTemplateFile); boolean changesMade = settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile); @@ -116,31 +117,52 @@ public class ConfigInitializer { } if (yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin") != null) { template.updateValue( - List.of("premium", "proFeatures", "SSOAutoLogin"), + List.of("premium", "proFeatures", "ssoAutoLogin"), yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin")); } if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "autoUpdateMetadata") != null) { template.updateValue( - List.of("premium", "proFeatures", "CustomMetadata", "autoUpdateMetadata"), + List.of("premium", "proFeatures", "customMetadata", "autoUpdateMetadata"), yaml.getValueByExactKeyPath( "enterpriseEdition", "CustomMetadata", "autoUpdateMetadata")); } if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author") != null) { template.updateValue( - List.of("premium", "proFeatures", "CustomMetadata", "author"), + List.of("premium", "proFeatures", "customMetadata", "author"), yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author")); } if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator") != null) { template.updateValue( - List.of("premium", "proFeatures", "CustomMetadata", "creator"), + List.of("premium", "proFeatures", "customMetadata", "creator"), yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator")); } if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer") != null) { template.updateValue( - List.of("premium", "proFeatures", "CustomMetadata", "producer"), + List.of("premium", "proFeatures", "customMetadata", "producer"), yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer")); } } + + // TODO: Remove post migration + // settings.yml.template renamed the two non-camelCase proFeatures keys + // ("SSOAutoLogin" -> "ssoAutoLogin", "CustomMetadata" -> "customMetadata") so the whole + // settings pipeline is consistent camelCase. The save path (YamlHelper.updateValue) matches + // keys case-sensitively, so without this carry-forward an existing install's values written + // under the old PascalCase keys would be dropped on upgrade and reset to template defaults. + void migrateProFeaturesKeyCasing(YamlHelper yaml, YamlHelper template) { + Object ssoAutoLogin = yaml.getValueByExactKeyPath("premium", "proFeatures", "SSOAutoLogin"); + if (ssoAutoLogin != null) { + template.updateValue(List.of("premium", "proFeatures", "ssoAutoLogin"), ssoAutoLogin); + } + for (String field : List.of("autoUpdateMetadata", "author", "creator", "producer")) { + Object value = + yaml.getValueByExactKeyPath("premium", "proFeatures", "CustomMetadata", field); + if (value != null) { + template.updateValue( + List.of("premium", "proFeatures", "customMetadata", field), value); + } + } + } } diff --git a/app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java b/app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java new file mode 100644 index 000000000..4ce75d0c6 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java @@ -0,0 +1,95 @@ +package stirling.software.common.configuration; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.snakeyaml.engine.v2.api.LoadSettings; + +import stirling.software.common.util.YamlHelper; + +class ConfigInitializerTest { + + private static final LoadSettings LOAD_SETTINGS = + LoadSettings.builder() + .setUseMarks(true) + .setMaxAliasesForCollections(Integer.MAX_VALUE) + .setAllowRecursiveKeys(true) + .setParseComments(true) + .build(); + + // Mirrors the proFeatures block of settings.yml.template after the camelCase rename. + private static final String CAMEL_CASE_TEMPLATE = + """ + premium: + proFeatures: + ssoAutoLogin: false + customMetadata: + autoUpdateMetadata: false + author: username + creator: Stirling-PDF + producer: Stirling-PDF + """; + + @Test + void migrateProFeaturesKeyCasing_carriesForwardLegacyPascalCaseValues() { + // An existing install whose settings.yml still uses the old PascalCase keys. + String legacy = + """ + premium: + proFeatures: + SSOAutoLogin: true + CustomMetadata: + autoUpdateMetadata: true + author: alice + creator: bob + producer: carol + """; + YamlHelper template = new YamlHelper(LOAD_SETTINGS, CAMEL_CASE_TEMPLATE); + YamlHelper existing = new YamlHelper(LOAD_SETTINGS, legacy); + + new ConfigInitializer().migrateProFeaturesKeyCasing(existing, template); + + assertEquals( + "true", template.getValueByExactKeyPath("premium", "proFeatures", "ssoAutoLogin")); + assertEquals( + "true", + template.getValueByExactKeyPath( + "premium", "proFeatures", "customMetadata", "autoUpdateMetadata")); + assertEquals( + "alice", + template.getValueByExactKeyPath( + "premium", "proFeatures", "customMetadata", "author")); + assertEquals( + "bob", + template.getValueByExactKeyPath( + "premium", "proFeatures", "customMetadata", "creator")); + assertEquals( + "carol", + template.getValueByExactKeyPath( + "premium", "proFeatures", "customMetadata", "producer")); + } + + @Test + void migrateProFeaturesKeyCasing_withoutLegacyKeys_keepsTemplateDefaults() { + // No PascalCase keys present -> this migration step must be a no-op. + String alreadyCamel = + """ + premium: + proFeatures: + ssoAutoLogin: true + customMetadata: + author: dave + """; + YamlHelper template = new YamlHelper(LOAD_SETTINGS, CAMEL_CASE_TEMPLATE); + YamlHelper existing = new YamlHelper(LOAD_SETTINGS, alreadyCamel); + + new ConfigInitializer().migrateProFeaturesKeyCasing(existing, template); + + assertEquals( + "false", template.getValueByExactKeyPath("premium", "proFeatures", "ssoAutoLogin")); + assertEquals( + "username", + template.getValueByExactKeyPath( + "premium", "proFeatures", "customMetadata", "author")); + } +} diff --git a/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java index bf6be29e3..4a746f788 100644 --- a/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java @@ -2,6 +2,8 @@ package stirling.software.common.util; import static org.junit.jupiter.api.Assertions.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -12,9 +14,47 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import stirling.software.common.configuration.InstallationPathConfig; public class GeneralUtilsTest { + // Regression guard for the SSO auto-login persistence bug: the admin UI writes camelCase + // proFeatures keys, so saveKeyToSettings must match (and persist) them against the camelCase + // settings.yml.template. A case mismatch makes YamlHelper.updateValue silently no-op. + @Test + void saveKeyToSettings_persistsCamelCaseProFeatureKeys(@TempDir Path tempDir) throws Exception { + Path settings = tempDir.resolve("settings.yml"); + Files.writeString( + settings, + """ + premium: + proFeatures: + ssoAutoLogin: false + customMetadata: + author: username + """); + + try (MockedStatic mocked = + Mockito.mockStatic(InstallationPathConfig.class)) { + mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(settings.toString()); + + GeneralUtils.saveKeyToSettings("premium.proFeatures.ssoAutoLogin", true); + GeneralUtils.saveKeyToSettings("premium.proFeatures.customMetadata.author", "alice"); + } + + YamlHelper reloaded = new YamlHelper(settings); + assertEquals( + "true", reloaded.getValueByExactKeyPath("premium", "proFeatures", "ssoAutoLogin")); + assertEquals( + "alice", + reloaded.getValueByExactKeyPath( + "premium", "proFeatures", "customMetadata", "author")); + } + @Test void testParsePageListWithAll() { List result = GeneralUtils.parsePageList(new String[] {"all"}, 5, false); diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index a82f4d0f9..d3684b4da 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -94,8 +94,8 @@ premium: key: 00000000-0000-0000-0000-000000000000 enabled: false # Enable license key checks for pro/enterprise features proFeatures: - SSOAutoLogin: false - CustomMetadata: + ssoAutoLogin: false + customMetadata: autoUpdateMetadata: false author: username creator: Stirling-PDF diff --git a/app/core/src/test/java/stirling/software/common/configuration/ConfigInitializerRestartTest.java b/app/core/src/test/java/stirling/software/common/configuration/ConfigInitializerRestartTest.java new file mode 100644 index 000000000..467ab0179 --- /dev/null +++ b/app/core/src/test/java/stirling/software/common/configuration/ConfigInitializerRestartTest.java @@ -0,0 +1,93 @@ +package stirling.software.common.configuration; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mockStatic; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; + +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.YamlHelper; + +/** + * End-to-end check of the container-restart path. {@link ConfigInitializer#ensureConfigExists()} is + * what runs on every startup, merging the on-disk settings.yml with the bundled + * settings.yml.template. These tests exercise it against the real template on the classpath to + * prove admin-saved proFeatures values survive a restart - the bug behind "the SSO auto-login + * button resets every time the container resets". + */ +class ConfigInitializerRestartTest { + + private static String read(Path settings, String... keyPath) throws IOException { + return String.valueOf(new YamlHelper(settings).getValueByExactKeyPath(keyPath)); + } + + @Test + void ssoAutoLoginAndCustomMetadata_persistAcrossRestart(@TempDir Path tmp) throws Exception { + Path settings = tmp.resolve("settings.yml"); + Path custom = tmp.resolve("custom_settings.yml"); + + try (MockedStatic paths = + mockStatic(InstallationPathConfig.class)) { + paths.when(InstallationPathConfig::getSettingsPath).thenReturn(settings.toString()); + paths.when(InstallationPathConfig::getCustomSettingsPath).thenReturn(custom.toString()); + + ConfigInitializer init = new ConfigInitializer(); + + // First boot: settings.yml created from the bundled template (camelCase, default off). + init.ensureConfigExists(); + assertEquals("false", read(settings, "premium", "proFeatures", "ssoAutoLogin")); + + // Admin enables SSO auto-login and edits custom metadata via the exact save path the + // admin settings controller uses. + GeneralUtils.saveKeyToSettings("premium.proFeatures.ssoAutoLogin", true); + GeneralUtils.saveKeyToSettings("premium.proFeatures.customMetadata.author", "acme"); + + // Container restart: ensureConfigExists merges the saved file with the template again. + init.ensureConfigExists(); + + assertEquals("true", read(settings, "premium", "proFeatures", "ssoAutoLogin")); + assertEquals( + "acme", read(settings, "premium", "proFeatures", "customMetadata", "author")); + } + } + + @Test + void legacyPascalCaseConfig_isMigratedAndPreservedOnRestart(@TempDir Path tmp) + throws Exception { + Path settings = tmp.resolve("settings.yml"); + Path custom = tmp.resolve("custom_settings.yml"); + + try (MockedStatic paths = + mockStatic(InstallationPathConfig.class)) { + paths.when(InstallationPathConfig::getSettingsPath).thenReturn(settings.toString()); + paths.when(InstallationPathConfig::getCustomSettingsPath).thenReturn(custom.toString()); + + ConfigInitializer init = new ConfigInitializer(); + + // Seed a full settings.yml as an OLD install would have written it: PascalCase keys + // with + // SSO auto-login enabled. + init.ensureConfigExists(); + String legacy = + Files.readString(settings) + .replace("ssoAutoLogin: false", "SSOAutoLogin: true") + .replace("customMetadata:", "CustomMetadata:"); + Files.writeString(settings, legacy); + + // Upgrade restart. + init.ensureConfigExists(); + + // Value carried forward onto the new camelCase key; the legacy PascalCase key is gone. + assertEquals("true", read(settings, "premium", "proFeatures", "ssoAutoLogin")); + assertNull( + new YamlHelper(settings) + .getValueByExactKeyPath("premium", "proFeatures", "SSOAutoLogin")); + } + } +} diff --git a/testing/allEndpointsRemovedSettings.yml b/testing/allEndpointsRemovedSettings.yml index eec19ecb2..52e85dd12 100644 --- a/testing/allEndpointsRemovedSettings.yml +++ b/testing/allEndpointsRemovedSettings.yml @@ -93,8 +93,8 @@ premium: key: 00000000-0000-0000-0000-000000000000 enabled: false # Enable license key checks for pro/enterprise features proFeatures: - SSOAutoLogin: false - CustomMetadata: + ssoAutoLogin: false + customMetadata: autoUpdateMetadata: false author: username creator: Stirling-PDF