Skip to content

Commit

Permalink
#28427 fix override old apps serial version to force a match (#28434)
Browse files Browse the repository at this point in the history
* #28427  fix override old apps serial version to foce a match

* #28427 change to debug level
  • Loading branch information
fabrizzio-dotCMS authored May 6, 2024
1 parent 13c8dda commit 31f820f
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 2 deletions.
3 changes: 1 addition & 2 deletions dotCMS/src/main/java/com/dotcms/security/apps/AppsUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -366,7 +365,7 @@ public static Map<String, List<AppSecrets>> importSecrets(final Path incomingFil
private static AppsSecretsImportExport readObject(final Path importFile)
throws IOException, ClassNotFoundException {
try(InputStream inputStream = Files.newInputStream(importFile)){
return (AppsSecretsImportExport)new ObjectInputStream(inputStream).readObject();
return (AppsSecretsImportExport) new VersionOverrideObjectInputStream(inputStream).readObject();
}
}

Expand Down
2 changes: 2 additions & 0 deletions dotCMS/src/main/java/com/dotcms/security/apps/Secret.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
@JsonDeserialize(builder = Secret.Builder.class)
public final class Secret extends AbstractProperty<char[]> {

private static final long serialVersionUID = 1L;

public Secret(final char[] value,
final Boolean hidden,
final Type type,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.dotcms.security.apps;

import com.dotmarketing.util.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

/**
* This class is used to override the default ObjectInputStream class to handle version mismatch
* in case of any mismatch in the serialVersionUID of the class we override the class descriptor with the local class descriptor
*/
public class VersionOverrideObjectInputStream extends ObjectInputStream {

/**
* Constructor
* @param in InputStream
* @throws IOException IOException
*/
public VersionOverrideObjectInputStream(InputStream in) throws IOException {
super(in);
}

/**
* This method is used to read the class descriptor and override the class descriptor in case of any mismatch in the serialVersionUID
* @return ObjectStreamClass
* @throws IOException IOException
* @throws ClassNotFoundException ClassNotFoundException
*/
@Override
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass resultClassDescriptor = super.readClassDescriptor(); // initially streams descriptor
Class<?> localClass; // the class in the local JVM that this descriptor represents.
try {
localClass = Class.forName(resultClassDescriptor.getName());
} catch (ClassNotFoundException e) {
Logger.error("No local class for " + resultClassDescriptor.getName(), e);
return resultClassDescriptor;
}
final ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass);
if (localClassDescriptor != null) { // only if class implements serializable
final long localSUID = localClassDescriptor.getSerialVersionUID();
final long streamSUID = resultClassDescriptor.getSerialVersionUID();
if (streamSUID != localSUID) { // check for serialVersionUID mismatch.
final String overrideMessage = String.format(
"Overriding serialized class version mismatch: " +
"local serialVersionUID = %d stream serialVersionUID = %d",
localSUID, streamSUID);
final Exception e = new InvalidClassException(overrideMessage);
// Report the error and override the class descriptor
Logger.debug(this,"Version mismatch on serialized objects, Will still attempting deserialization .", e);
resultClassDescriptor = localClassDescriptor; // Use local class descriptor for deserialization
}
}
return resultClassDescriptor;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.Key;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -406,4 +412,71 @@ public void Test_hiddenSecret_overrideWithEnVar() {
assertArrayEquals(OVERRIDDEN_VALUE, secret2.get().envVarValue);
}

/**
* Given scenario: A secret is created with a key and a set of secrets
* Expected Result: The secret is created exported and imported back.
* @throws DotDataException
* @throws IOException
*/
@Test
public void testExportThenImportSecrets() throws DotDataException, IOException {

final Key key = AppsUtil.generateKey(AppsUtil.loadPass(()->"lol"));

final AppSecrets secrets = AppSecrets.builder()
.withKey("key")
.withHiddenSecret("hidden1", "value1")
.withHiddenSecret("hidden2", "value2")
.withSecret("secret1", "value3")
.withSecret("secret2", Secret.builder().withType(Type.STRING).withValue("value4").withEnvValue("env".toCharArray()).build())
.build();

final Map<String, List<AppSecrets>> appSecrets = Map.of("SYSTEM_HOST", List.of(secrets));
final AppsSecretsImportExport importExport = new AppsSecretsImportExport(appSecrets);
final Path in = AppsUtil.exportSecret(importExport, key);

final Map<String, List<AppSecrets>> importSecrets = AppsUtil.importSecrets(in, key);
assertNotNull(importSecrets);
assertEquals(1, importSecrets.size());
final List<AppSecrets> appSecretsList = importSecrets.get("SYSTEM_HOST");
assertNotNull(appSecretsList);
assertEquals(1, appSecretsList.size());
final AppSecrets importedSecrets = appSecretsList.get(0);
assertEquals("key", importedSecrets.getKey());
assertEquals(4, importedSecrets.getSecrets().size());
assertEquals("value1", new String(importedSecrets.getSecrets().get("hidden1").getValue()));
assertEquals("value2", new String(importedSecrets.getSecrets().get("hidden2").getValue()));
assertEquals("value3", new String(importedSecrets.getSecrets().get("secret1").getValue()));
assertEquals("value4", new String(importedSecrets.getSecrets().get("secret2").getValue()));
assertEquals("env", new String(importedSecrets.getSecrets().get("secret2").getEnvVarValue()));
}

/**
* Given scenario: Lets import an old version of the secrets
* Expected Result: The secret is imported back.
* @throws DotDataException
* @throws IOException
* @throws URISyntaxException
*/
@Test
public void testImportOldVersionSecrets()
throws DotDataException, IOException, URISyntaxException {
final URL resource = Thread.currentThread().getContextClassLoader()
.getResource("apps/googleTranslationApp.export");
assertNotNull("resource is null ", resource);
final Path in = Path.of(resource.toURI());
final Key key = AppsUtil.generateKey(AppsUtil.loadPass(()->"testdotcms1234"));
final Map<String, List<AppSecrets>> importSecrets = AppsUtil.importSecrets(in, key);
assertNotNull(importSecrets);
assertEquals(1, importSecrets.size());
final List<AppSecrets> appSecretsList = importSecrets.get("SYSTEM_HOST");
assertNotNull(appSecretsList);
assertEquals(1, appSecretsList.size());
final AppSecrets importedSecrets = appSecretsList.get(0);
assertEquals("dotGoogleTranslate-config", importedSecrets.getKey());
assertEquals(1, importedSecrets.getSecrets().size());
assertEquals("testgoogleAPIAPP", new String(importedSecrets.getSecrets().get("apiKey").getValue()));

}

}
Binary file not shown.

0 comments on commit 31f820f

Please sign in to comment.