Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#28427 fix override old apps serial version to force a match #28434

Merged
merged 3 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading