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")));
}
}