mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Fix SSO auto-login and custom metadata settings not persisting on restart (#6468)
This commit is contained in:
+27
-5
@@ -80,6 +80,7 @@ public class ConfigInitializer {
|
|||||||
YamlHelper settingsFile = new YamlHelper(settingTempPath);
|
YamlHelper settingsFile = new YamlHelper(settingTempPath);
|
||||||
|
|
||||||
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
|
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
|
||||||
|
migrateProFeaturesKeyCasing(settingsFile, settingsTemplateFile);
|
||||||
|
|
||||||
boolean changesMade =
|
boolean changesMade =
|
||||||
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
|
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
|
||||||
@@ -116,31 +117,52 @@ public class ConfigInitializer {
|
|||||||
}
|
}
|
||||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin") != null) {
|
if (yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin") != null) {
|
||||||
template.updateValue(
|
template.updateValue(
|
||||||
List.of("premium", "proFeatures", "SSOAutoLogin"),
|
List.of("premium", "proFeatures", "ssoAutoLogin"),
|
||||||
yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin"));
|
yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin"));
|
||||||
}
|
}
|
||||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "autoUpdateMetadata")
|
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "autoUpdateMetadata")
|
||||||
!= null) {
|
!= null) {
|
||||||
template.updateValue(
|
template.updateValue(
|
||||||
List.of("premium", "proFeatures", "CustomMetadata", "autoUpdateMetadata"),
|
List.of("premium", "proFeatures", "customMetadata", "autoUpdateMetadata"),
|
||||||
yaml.getValueByExactKeyPath(
|
yaml.getValueByExactKeyPath(
|
||||||
"enterpriseEdition", "CustomMetadata", "autoUpdateMetadata"));
|
"enterpriseEdition", "CustomMetadata", "autoUpdateMetadata"));
|
||||||
}
|
}
|
||||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author") != null) {
|
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author") != null) {
|
||||||
template.updateValue(
|
template.updateValue(
|
||||||
List.of("premium", "proFeatures", "CustomMetadata", "author"),
|
List.of("premium", "proFeatures", "customMetadata", "author"),
|
||||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author"));
|
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author"));
|
||||||
}
|
}
|
||||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator") != null) {
|
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator") != null) {
|
||||||
template.updateValue(
|
template.updateValue(
|
||||||
List.of("premium", "proFeatures", "CustomMetadata", "creator"),
|
List.of("premium", "proFeatures", "customMetadata", "creator"),
|
||||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator"));
|
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator"));
|
||||||
}
|
}
|
||||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer")
|
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer")
|
||||||
!= null) {
|
!= null) {
|
||||||
template.updateValue(
|
template.updateValue(
|
||||||
List.of("premium", "proFeatures", "CustomMetadata", "producer"),
|
List.of("premium", "proFeatures", "customMetadata", "producer"),
|
||||||
yaml.getValueByExactKeyPath("enterpriseEdition", "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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+95
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package stirling.software.common.util;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -12,9 +14,47 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
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 {
|
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<InstallationPathConfig> 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
|
@Test
|
||||||
void testParsePageListWithAll() {
|
void testParsePageListWithAll() {
|
||||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"all"}, 5, false);
|
List<Integer> result = GeneralUtils.parsePageList(new String[] {"all"}, 5, false);
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ premium:
|
|||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
enabled: false # Enable license key checks for pro/enterprise features
|
enabled: false # Enable license key checks for pro/enterprise features
|
||||||
proFeatures:
|
proFeatures:
|
||||||
SSOAutoLogin: false
|
ssoAutoLogin: false
|
||||||
CustomMetadata:
|
customMetadata:
|
||||||
autoUpdateMetadata: false
|
autoUpdateMetadata: false
|
||||||
author: username
|
author: username
|
||||||
creator: Stirling-PDF
|
creator: Stirling-PDF
|
||||||
|
|||||||
+93
@@ -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<InstallationPathConfig> 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<InstallationPathConfig> 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,8 +93,8 @@ premium:
|
|||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
enabled: false # Enable license key checks for pro/enterprise features
|
enabled: false # Enable license key checks for pro/enterprise features
|
||||||
proFeatures:
|
proFeatures:
|
||||||
SSOAutoLogin: false
|
ssoAutoLogin: false
|
||||||
CustomMetadata:
|
customMetadata:
|
||||||
autoUpdateMetadata: false
|
autoUpdateMetadata: false
|
||||||
author: username
|
author: username
|
||||||
creator: Stirling-PDF
|
creator: Stirling-PDF
|
||||||
|
|||||||
Reference in New Issue
Block a user