From 252c9903ed5d32790c597e08fe2bdef35b82eeda Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Sun, 29 Sep 2024 17:14:45 +0200 Subject: [PATCH] Simplify and tidy up config (#74) Tidy up model. Relocation: drop all that mumbo-jumbo. In general simplified SecUtil. --- .../secdispatcher/SecDispatcher.java | 28 ++++++- .../internal/DefaultSecDispatcher.java | 36 ++++++-- .../secdispatcher/internal/SecUtil.java | 84 ++++++++++++------- src/main/mdo/settings-security.mdo | 46 ++++------ .../secdispatcher/internal/SecUtilTest.java | 60 ++++++------- 5 files changed, 154 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java index 53c64a8..04ef9dd 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java @@ -13,9 +13,12 @@ package org.codehaus.plexus.components.secdispatcher; +import java.io.IOException; import java.util.Map; import java.util.Set; +import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity; + /** * This component decrypts a string, passed to it * @@ -26,7 +29,7 @@ public interface SecDispatcher { * The default path of configuration. *

* The character {@code ~} (tilde) may be present as first character ONLY and is - * interpreted as "user home". + * interpreted as "user.home" system property, and it MUST be followed by path separator. */ String DEFAULT_CONFIGURATION = "~/.m2/settings-security.xml"; @@ -53,7 +56,7 @@ public interface SecDispatcher { Set availableCiphers(); /** - * encrypt given plaintext string + * Encrypt given plaintext string. * * @param str the plaintext to encrypt * @param attr the attributes, may be {@code null} @@ -63,11 +66,28 @@ public interface SecDispatcher { String encrypt(String str, Map attr) throws SecDispatcherException; /** - * decrypt given encrypted string + * Decrypt given encrypted string. * * @param str the encrypted string - * @return plaintext string + * @return decrypted string * @throws SecDispatcherException in case of problem */ String decrypt(String str) throws SecDispatcherException; + + /** + * Reads the effective configuration, eventually creating new instance if not present. + * + * @param createIfMissing If {@code true}, it will create a new empty instance + * @return the configuration, of {@code null} if it does not exist in {@code createIfMissing} is {@code false} + * @throws IOException In case of IO problem + */ + SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException; + + /** + * Writes the effective configuration. + * + * @param configuration The configuration to write, may not be {@code null} + * @throws IOException In case of IO problem + */ + void writeConfiguration(SettingsSecurity configuration) throws IOException; } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java index 38da707..131c0de 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java @@ -17,6 +17,9 @@ import javax.inject.Named; import javax.inject.Singleton; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -115,6 +118,21 @@ public String decrypt(String str) throws SecDispatcherException { } } + @Override + public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException { + SettingsSecurity configuration = SecUtil.read(getConfigurationPath()); + if (configuration == null && createIfMissing) { + configuration = new SettingsSecurity(); + } + return configuration; + } + + @Override + public void writeConfiguration(SettingsSecurity configuration) throws IOException { + requireNonNull(configuration, "configuration is null"); + SecUtil.write(getConfigurationPath(), configuration, true); + } + private Map prepareDispatcherConfig(String type) { HashMap dispatcherConf = new HashMap<>(); SettingsSecurity sec = getConfiguration(false); @@ -167,14 +185,22 @@ private boolean isEncryptedString(String str) { return cipher.isEncryptedString(str); } - private SettingsSecurity getConfiguration(boolean mandatory) throws SecDispatcherException { + private Path getConfigurationPath() { String location = System.getProperty(SYSTEM_PROPERTY_CONFIGURATION_LOCATION, getConfigurationFile()); location = location.charAt(0) == '~' ? System.getProperty("user.home") + location.substring(1) : location; - SettingsSecurity sec = SecUtil.read(location, true); - if (mandatory && sec == null) - throw new SecDispatcherException("Please check that configuration file on path " + location + " exists"); + return Paths.get(location); + } - return sec; + private SettingsSecurity getConfiguration(boolean mandatory) throws SecDispatcherException { + Path path = getConfigurationPath(); + try { + SettingsSecurity sec = SecUtil.read(path); + if (mandatory && sec == null) + throw new SecDispatcherException("Please check that configuration file on path " + path + " exists"); + return sec; + } catch (IOException e) { + throw new SecDispatcherException(e.getMessage(), e); + } } private String getMasterPassword(SettingsSecurity sec, boolean mandatory) throws SecDispatcherException { diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java index ebf400e..0338683 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java @@ -17,19 +17,23 @@ import java.io.IOException; import java.io.InputStream; -import java.net.URL; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.NoSuchFileException; -import java.nio.file.Paths; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import org.codehaus.plexus.components.secdispatcher.model.Config; import org.codehaus.plexus.components.secdispatcher.model.ConfigProperty; import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity; import org.codehaus.plexus.components.secdispatcher.model.io.stax.SecurityConfigurationStaxReader; +import org.codehaus.plexus.components.secdispatcher.model.io.stax.SecurityConfigurationStaxWriter; import static java.util.Objects.requireNonNull; @@ -40,44 +44,25 @@ * @version $Id$ * */ -public class SecUtil { +public final class SecUtil { + private SecUtil() {} - public static final String PROTOCOL_DELIM = "://"; - public static final int PROTOCOL_DELIM_LEN = PROTOCOL_DELIM.length(); - public static final String[] URL_PROTOCOLS = - new String[] {"http", "https", "dav", "file", "davs", "webdav", "webdavs", "dav+http", "dav+https"}; - - public static SettingsSecurity read(String location, boolean cycle) throws SecDispatcherException { - if (location == null) throw new SecDispatcherException("location to read from is null"); + /** + * Reads the configuration model up, optionally resolving relocation too. + */ + public static SettingsSecurity read(Path configurationFile) throws IOException { + requireNonNull(configurationFile, "configurationFile must not be null"); SettingsSecurity sec; try { - try (InputStream in = toStream(location)) { + try (InputStream in = Files.newInputStream(configurationFile)) { sec = new SecurityConfigurationStaxReader().read(in); } - if (cycle && sec.getRelocation() != null) return read(sec.getRelocation(), true); return sec; } catch (NoSuchFileException e) { return null; - } catch (IOException e) { - throw new SecDispatcherException("IO Problem", e); } catch (XMLStreamException e) { - throw new SecDispatcherException("Parsing error", e); - } - } - - private static InputStream toStream(String resource) throws IOException { - requireNonNull(resource, "resource is null"); - int ind = resource.indexOf(PROTOCOL_DELIM); - if (ind > 1) { - String protocol = resource.substring(0, ind); - resource = resource.substring(ind + PROTOCOL_DELIM_LEN); - for (String p : URL_PROTOCOLS) { - if (protocol.regionMatches(true, 0, p, 0, p.length())) { - return new URL(p + PROTOCOL_DELIM + resource).openStream(); - } - } + throw new IOException("Parsing error", e); } - return Files.newInputStream(Paths.get(resource)); } public static Map getConfig(SettingsSecurity sec, String name) { @@ -102,4 +87,41 @@ public static Map getConfig(SettingsSecurity sec, String name) { } return null; } + + private static final boolean IS_WINDOWS = + System.getProperty("os.name", "unknown").startsWith("Windows"); + + public static void write(Path target, SettingsSecurity configuration, boolean doBackup) throws IOException { + requireNonNull(target, "file must not be null"); + requireNonNull(configuration, "configuration must not be null"); + Path parent = requireNonNull(target.getParent(), "target must have parent"); + Files.createDirectories(parent); + Path tempFile = parent.resolve(target.getFileName() + "." + + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp"); + + configuration.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion()); + configuration.setModelEncoding(StandardCharsets.UTF_8.name()); + + try { + try (OutputStream tempOut = Files.newOutputStream(tempFile)) { + new SecurityConfigurationStaxWriter().write(tempOut, configuration); + } + + if (doBackup && Files.isRegularFile(target)) { + Files.copy(target, parent.resolve(target.getFileName() + ".bak"), StandardCopyOption.REPLACE_EXISTING); + } + if (IS_WINDOWS) { + try (InputStream is = Files.newInputStream(tempFile); + OutputStream os = Files.newOutputStream(target)) { + is.transferTo(os); + } + } else { + Files.move(tempFile, target, StandardCopyOption.REPLACE_EXISTING); + } + } catch (XMLStreamException e) { + throw new IOException("XML Processing error", e); + } finally { + Files.deleteIfExists(tempFile); + } + } } diff --git a/src/main/mdo/settings-security.mdo b/src/main/mdo/settings-security.mdo index d671f57..336c179 100644 --- a/src/main/mdo/settings-security.mdo +++ b/src/main/mdo/settings-security.mdo @@ -19,31 +19,34 @@ xml.schemaLocation="https://codehaus-plexus.github.io/xsd/plexus-sec-dispatcher-${version}.xsd"> settings-security - SecurityConfiguration SecurityConfiguration - + package org.codehaus.plexus.components.secdispatcher.model - - + SettingsSecurity 1.0.0+ - master 1.0.0/2.1.0 String encrypted master password - + + relocation + 1.0.0/2.1.0 + String + false + Relocates configuration to given reference. Reference if relative, will be resolved from the relocated configuration directory + modelVersion 3.0.0+ @@ -51,59 +54,45 @@ true The version of the model - masterSource 3.0.0+ String true - The URI describing the source of the master password + The masterSource describes the source of the master password - masterCipher 3.0.0+ String true - The Cipher to be used + The Cipher to be used for master password - - - relocation - 1.0.0+ - String - false - reference to the location of the security file - - configurations 1.0.0+ - named configurations + Optional named Dispatcher configurations false Config * - Config 1.0.0+ - Named configuration + Named Dispatcher configuration - name String true 1.0.0+ - name of this configuration + Name of Dispatcher configuration is meant for - properties 1.0.0+ @@ -113,17 +102,13 @@ * - - ConfigProperty 1.0.0+ generic property - name/value pair - - name String @@ -131,7 +116,6 @@ 1.0.0+ name of this property - value String @@ -139,9 +123,7 @@ 1.0.0+ value of this property - - diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java index 8afc4f1..f98dccb 100644 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java @@ -13,20 +13,21 @@ package org.codehaus.plexus.components.secdispatcher.internal; -import java.io.OutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Map; import org.codehaus.plexus.components.secdispatcher.model.Config; import org.codehaus.plexus.components.secdispatcher.model.ConfigProperty; import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity; -import org.codehaus.plexus.components.secdispatcher.model.io.stax.SecurityConfigurationStaxWriter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @@ -40,46 +41,49 @@ public class SecUtilTest { String _propName = "pname"; String _propVal = "pval"; - private void saveSec(String masterSource) throws Exception { - SettingsSecurity sec = new SettingsSecurity(); + private void saveSec(String masterSource) throws IOException { + saveSec("./target/sec.xml", masterSource); + } - sec.setRelocation(null); + private void saveSec(String path, String masterSource) throws IOException { + SettingsSecurity sec = new SettingsSecurity(); sec.setMasterSource(masterSource); - ConfigProperty cp = new ConfigProperty(); cp.setName(_propName); cp.setValue(_propVal); - Config conf = new Config(); conf.setName(_confName); conf.addProperty(cp); - sec.addConfiguration(conf); - - try (OutputStream fos = Files.newOutputStream(Paths.get("./target/sec1.xml"))) { - new SecurityConfigurationStaxWriter().write(fos, sec); - } + SecUtil.write(Paths.get(path), sec, false); } @BeforeEach - public void prepare() throws Exception { - System.setProperty(DefaultSecDispatcher.SYSTEM_PROPERTY_CONFIGURATION_LOCATION, "./target/sec.xml"); - SettingsSecurity sec = new SettingsSecurity(); - sec.setRelocation("./target/sec1.xml"); - try (OutputStream fos = Files.newOutputStream(Paths.get("./target/sec.xml"))) { - new SecurityConfigurationStaxWriter().write(fos, sec); - } + void prepare() throws IOException { saveSec("magic:mighty"); } @Test - void testReadWithRelocation() throws Exception { - SettingsSecurity sec = SecUtil.read("./target/sec.xml", true); - assertNotNull(sec); - assertEquals("magic:mighty", sec.getMasterSource()); - Map conf = SecUtil.getConfig(sec, _confName); - assertNotNull(conf); - assertNotNull(conf.get(_propName)); - assertEquals(_propVal, conf.get(_propName)); + void readWrite() throws IOException { + Path path = Path.of("./target/sec.xml"); + SettingsSecurity config = SecUtil.read(path); + assertNotNull(config); + assertEquals(SettingsSecurity.class.getPackage().getSpecificationVersion(), config.getModelVersion()); + assertEquals(StandardCharsets.UTF_8.name(), config.getModelEncoding()); + assertEquals("magic:mighty", config.getMasterSource()); + SecUtil.write(path, config, false); + } + + @Test + void readWriteWithBackup() throws IOException { + Path path = Path.of("./target/sec.xml"); + SettingsSecurity config = SecUtil.read(path); + assertNotNull(config); + assertEquals(SettingsSecurity.class.getPackage().getSpecificationVersion(), config.getModelVersion()); + assertEquals(StandardCharsets.UTF_8.name(), config.getModelEncoding()); + assertEquals("magic:mighty", config.getMasterSource()); + SecUtil.write(path, config, true); + assertTrue(Files.exists(path)); + assertTrue(Files.exists(path.getParent().resolve(path.getFileName() + ".bak"))); } }