diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/HashUtilTest.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/HashUtilTest.java new file mode 100644 index 0000000000..09a5208223 --- /dev/null +++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/HashUtilTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.cli.test; + +import org.apache.activemq.artemis.cli.commands.util.HashUtil; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class HashUtilTest { + + @Test + public void testDefaultHashFormat() throws Exception { + final String password = "helloworld"; + String hash = HashUtil.tryHash(new TestActionContext(), password); + String hashStr = PasswordMaskingUtil.unwrap(hash); + System.out.println("hashString: " + hashStr); + String[] parts = hashStr.split(":"); + assertEquals(3, parts.length); + //first part should be able to convert to an int + Integer.parseInt(parts[0]); + //second and third parts are all hex values + checkHexBytes(parts[1], parts[2]); + } + + private void checkHexBytes(String... parts) throws Exception { + for (String p : parts) { + assertTrue(p.matches("^[0-9A-F]+$")); + } + } +} diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java index dbc20c54c0..be560465f1 100644 --- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java @@ -28,14 +28,54 @@ import org.apache.activemq.artemis.logs.ActiveMQUtilBundle; public final class PasswordMaskingUtil { + public static final String BEGIN_ENC = "ENC("; + public static final String END_ENC = ")"; + private PasswordMaskingUtil() { } + /** + * This method deals with password masking and returns the password in its plain text form. + * @param maskPassword : explicit mask flag. If it's true, the password is interpreted as + * masked. If it is false, the password is interpreted as plain text. + * if it is null, the password will be interpreted as masked if the + * password is wrapped in ENC(), or as plain text otherwise. + * @param password : the original value of password string + * @param codecClass : the codec used to decode the password. Only when the password is interpreted + * as masked will this codec be used. Ignored otherwise. + * @return + */ + public static String resolveMask(Boolean maskPassword, String password, String codecClass) throws Exception { + String plainText = password; + if (maskPassword == null) { + if (isEncMasked(password)) { + //masked + String bareMaskedPassword = unwrap(password); + plainText = getCodec(codecClass).decode(bareMaskedPassword); + } + } else if (maskPassword) { + plainText = getCodec(codecClass).decode(password); + } + return plainText; + } + + public static boolean isEncMasked(String password) { + return (password.startsWith(BEGIN_ENC) && password.endsWith(END_ENC)); + } + + //remove ENC() from the password body + public static String unwrap(String password) { + return password.substring(4, password.length() - 1); + } + + public static String wrap(String password) { + return BEGIN_ENC + password + END_ENC; + } + private static final class LazyPlainTextProcessorHolder { private LazyPlainTextProcessorHolder() { - } private static final HashProcessor INSTANCE = new NoHashProcessor(); @@ -83,7 +123,7 @@ public final class PasswordMaskingUtil { } private static boolean isEncoded(String storedPassword) { - return storedPassword == null || (storedPassword.startsWith(SecureHashProcessor.BEGIN_HASH) && storedPassword.endsWith(SecureHashProcessor.END_HASH)); + return storedPassword == null || (isEncMasked(storedPassword)); } public static HashProcessor getHashProcessor() { @@ -107,6 +147,10 @@ public final class PasswordMaskingUtil { public static SensitiveDataCodec getCodec(String codecDesc) throws ActiveMQException { SensitiveDataCodec codecInstance; + if (codecDesc == null) { + return getDefaultCodec(); + } + // semi colons String[] parts = codecDesc.split(";"); if (parts.length < 1) diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java index 8e9cd5c0f9..a17cfd4a2e 100644 --- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java @@ -18,9 +18,6 @@ package org.apache.activemq.artemis.utils; public class SecureHashProcessor implements HashProcessor { - public static final String BEGIN_HASH = "ENC("; - public static final String END_HASH = ")"; - private DefaultSensitiveStringCodec codec; public SecureHashProcessor(DefaultSensitiveStringCodec codec) { @@ -29,13 +26,13 @@ public class SecureHashProcessor implements HashProcessor { @Override public String hash(String plainText) throws Exception { - return BEGIN_HASH + codec.encode(plainText) + END_HASH; + return PasswordMaskingUtil.wrap(codec.encode(plainText)); } @Override //storedValue must take form of ENC(...) public boolean compare(char[] inputValue, String storedValue) { - String storedHash = storedValue.substring(4, storedValue.length() - 2); + String storedHash = storedValue.substring(4, storedValue.length() - 1); return codec.verify(inputValue, storedHash); } } diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java index d5859763b2..cbd17e5ad0 100644 --- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java @@ -31,5 +31,6 @@ public interface SensitiveDataCodec { T encode(Object secret) throws Exception; - void init(Map params) throws Exception; + default void init(Map params) throws Exception { + }; } diff --git a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/MaskPasswordResolvingTest.java b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/MaskPasswordResolvingTest.java new file mode 100644 index 0000000000..5a99ccc689 --- /dev/null +++ b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/MaskPasswordResolvingTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.artemis.utils; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class MaskPasswordResolvingTest { + + private static final String plainPassword = "password"; + private static final String defaultMaskPassword = "defaultmasked"; + private static final String customizedCodecPassword = "secret"; + private static final String oldDefaultMaskedPassword = "oldmasked"; + private static final String oldCustomizedCodecPassword = "secret"; + private static final String oldExplicitPlainPassword = "PASSWORD"; + + @Parameterized.Parameters(name = "mask({0})password({1})codec({2})") + public static Collection params() { + return Arrays.asList(new Object[][]{{null, plainPassword, null}, + {null, "ENC(3bdfd94fe8cdf710e7fefa72f809ea90)", null}, + {null, "ENC(momsword)", "org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec"}, + {true, "662d05f5a83f9e073af6b8dc081d34aa", null}, + {true, "momsword", "org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec"}, + {false, oldExplicitPlainPassword, null}, + {false, oldExplicitPlainPassword, "org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec"}}); + } + + private Boolean maskPassword; + private String password; + private String codec; + + public MaskPasswordResolvingTest(Boolean maskPassword, String password, String codec) { + this.maskPassword = maskPassword; + this.password = password; + this.codec = codec; + } + + @Test + public void testPasswordResolving() throws Exception { + String resolved = PasswordMaskingUtil.resolveMask(maskPassword, password, codec); + System.out.println("resolved: " + resolved); + checkResult(resolved); + } + + private void checkResult(String resolved) throws Exception { + if (this.maskPassword == null) { + if (PasswordMaskingUtil.isEncMasked(this.password)) { + if (this.codec != null) { + assertEquals(customizedCodecPassword, resolved); + } else { + assertEquals(defaultMaskPassword, resolved); + } + } else { + assertEquals(plainPassword, resolved); + } + } else { + if (this.maskPassword) { + if (this.codec != null) { + assertEquals(oldCustomizedCodecPassword, resolved); + } else { + assertEquals(oldDefaultMaskedPassword, resolved); + } + } else { + assertEquals(oldExplicitPlainPassword, resolved); + } + } + } + + public static class SimplePasswordCodec implements SensitiveDataCodec { + + private Map passwordBook = new HashMap<>(); + + public SimplePasswordCodec() { + passwordBook.put("momsword", "secret"); + passwordBook.put("youneverknow", "keypass"); + passwordBook.put("youcanguess", "trustpass"); + } + + @Override + public String decode(Object mask) throws Exception { + String password = passwordBook.get(mask); + if (password == null) { + throw new IllegalArgumentException("I don't know the password " + mask); + } + return password; + } + + @Override + public String encode(Object secret) throws Exception { + return null; + } + } +} diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java index 9b5d75d322..fecf71c259 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java @@ -179,7 +179,7 @@ public final class ActiveMQDefaultConfiguration { private static String DEFAULT_CLUSTER_PASSWORD = "CHANGE ME!!"; // This option controls whether passwords in server configuration need be masked. If set to "true" the passwords are masked. - private static boolean DEFAULT_MASK_PASSWORD = false; + private static Boolean DEFAULT_MASK_PASSWORD = null; // true means that the management API is available via JMX private static boolean DEFAULT_JMX_MANAGEMENT_ENABLED = true; @@ -622,7 +622,7 @@ public final class ActiveMQDefaultConfiguration { /** * This option controls whether passwords in server configuration need be masked. If set to "true" the passwords are masked. */ - public static boolean isDefaultMaskPassword() { + public static Boolean isDefaultMaskPassword() { return DEFAULT_MASK_PASSWORD; } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java index 64107b93d7..bb88e6d22b 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java @@ -178,9 +178,6 @@ public interface ActiveMQClientMessageBundle { @Message(id = 119043, value = "Invalid argument null handler") IllegalArgumentException nullHandler(); - @Message(id = 119044, value = "No available codec to decode password!") - IllegalArgumentException noCodec(); - @Message(id = 119045, value = "the first node to be compared is null") IllegalArgumentException firstNodeNull(); @@ -214,9 +211,6 @@ public interface ActiveMQClientMessageBundle { @Message(id = 119055, value = "Element {0} requires a valid Long value, but ''{1}'' cannot be parsed as a Long", format = Message.Format.MESSAGE_FORMAT) IllegalArgumentException mustBeLong(Node element, String value); - @Message(id = 119056, value = "Failed to get decoder") - IllegalArgumentException failedToGetDecoder(@Cause Exception e); - @Message(id = 119057, value = "Error decoding password") IllegalArgumentException errordecodingPassword(@Cause Exception e); diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java index a205c191cf..4367318ba3 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java @@ -20,7 +20,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.core.client.ActiveMQClientLogger; import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle; @@ -164,25 +163,9 @@ public class ConfigurationHelper { String value = prop.toString(); Boolean useMask = (Boolean) props.get(defaultMaskPassword); - if (useMask == null || (!useMask)) { - return value; - } - final String classImpl = (String) props.get(defaultPasswordCodec); - - if (classImpl == null) { - throw ActiveMQClientMessageBundle.BUNDLE.noCodec(); - } - - SensitiveDataCodec codec = null; try { - codec = PasswordMaskingUtil.getCodec(classImpl); - } catch (ActiveMQException e1) { - throw ActiveMQClientMessageBundle.BUNDLE.failedToGetDecoder(e1); - } - - try { - return codec.decode(value); + return PasswordMaskingUtil.resolveMask(useMask, value, classImpl); } catch (Exception e) { throw ActiveMQClientMessageBundle.BUNDLE.errordecodingPassword(e); } diff --git a/artemis-dto/pom.xml b/artemis-dto/pom.xml index 05de601472..f875957d9a 100644 --- a/artemis-dto/pom.xml +++ b/artemis-dto/pom.xml @@ -31,6 +31,14 @@ ${project.basedir}/.. + + + org.apache.activemq + artemis-commons + ${project.version} + + + diff --git a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java index 4553e0aa6e..3e1526184f 100644 --- a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java +++ b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java @@ -16,6 +16,8 @@ */ package org.apache.activemq.artemis.dto; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; @@ -37,21 +39,44 @@ public class WebServerDTO extends ComponentDTO { public Boolean clientAuth; @XmlAttribute - public String keyStorePath; + public String passwordCodec; @XmlAttribute - public String keyStorePassword; + public String keyStorePath; @XmlAttribute public String trustStorePath; - @XmlAttribute - public String trustStorePassword; - @XmlElementRef public List apps; + @XmlAttribute + private String keyStorePassword; + + @XmlAttribute + private String trustStorePassword; + public WebServerDTO() { componentClassName = "org.apache.activemq.artemis.component.WebServerComponent"; } + + public String getKeyStorePassword() throws Exception { + return getPassword(this.keyStorePassword); + } + + private String getPassword(String password) throws Exception { + return PasswordMaskingUtil.resolveMask(null, password, this.passwordCodec); + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getTrustStorePassword() throws Exception { + return getPassword(this.trustStorePassword); + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } } diff --git a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java index b67b0b3c85..4d9b04e4d7 100644 --- a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java +++ b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java @@ -69,9 +69,7 @@ import org.apache.activemq.artemis.jms.server.ActiveMQJMSServerBundle; import org.apache.activemq.artemis.service.extensions.ServiceUtils; import org.apache.activemq.artemis.service.extensions.xa.recovery.ActiveMQRegistry; import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConfig; -import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; public final class JMSBridgeImpl implements JMSBridge { @@ -168,7 +166,7 @@ public final class JMSBridgeImpl implements JMSBridge { private ObjectName objectName; - private boolean useMaskedPassword = false; + private Boolean useMaskedPassword; private String passwordCodec; @@ -440,25 +438,16 @@ public final class JMSBridgeImpl implements JMSBridge { } private void initPasswords() throws ActiveMQException { - if (useMaskedPassword) { - SensitiveDataCodec codecInstance = new DefaultSensitiveStringCodec(); - - if (passwordCodec != null) { - codecInstance = PasswordMaskingUtil.getCodec(passwordCodec); + try { + if (this.sourcePassword != null) { + sourcePassword = PasswordMaskingUtil.resolveMask(useMaskedPassword, sourcePassword, passwordCodec); } - try { - if (this.sourcePassword != null) { - sourcePassword = codecInstance.decode(sourcePassword); - } - - if (this.targetPassword != null) { - targetPassword = codecInstance.decode(targetPassword); - } - } catch (Exception e) { - throw ActiveMQJMSServerBundle.BUNDLE.errorDecodingPassword(e); + if (this.targetPassword != null) { + targetPassword = PasswordMaskingUtil.resolveMask(useMaskedPassword, targetPassword, passwordCodec); } - + } catch (Exception e) { + throw ActiveMQJMSServerBundle.BUNDLE.errorDecodingPassword(e); } } diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java index dffd5b3afb..e56e80d991 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java @@ -20,9 +20,7 @@ import java.io.Serializable; import java.util.Hashtable; import org.apache.activemq.artemis.api.core.ActiveMQException; -import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; /** * The RA default properties - these are set in the ra.xml file @@ -66,14 +64,12 @@ public class ActiveMQRAProperties extends ConnectionFactoryProperties implements private boolean useJNDI; - private boolean useMaskedPassword = false; + private Boolean useMaskedPassword = null; private String passwordCodec; private boolean initialized = false; - private transient SensitiveDataCodec codecInstance; - /** * Class used to get a JChannel */ @@ -217,11 +213,11 @@ public class ActiveMQRAProperties extends ConnectionFactoryProperties implements this.setupInterval = setupInterval; } - public boolean isUseMaskedPassword() { + public Boolean isUseMaskedPassword() { return useMaskedPassword; } - public void setUseMaskedPassword(boolean useMaskedPassword) { + public void setUseMaskedPassword(Boolean useMaskedPassword) { this.useMaskedPassword = useMaskedPassword; } @@ -243,27 +239,19 @@ public class ActiveMQRAProperties extends ConnectionFactoryProperties implements if (initialized) return; - if (useMaskedPassword) { - codecInstance = new DefaultSensitiveStringCodec(); - - if (passwordCodec != null) { - codecInstance = PasswordMaskingUtil.getCodec(passwordCodec); - } - + if (password != null) { try { - if (password != null) { - password = codecInstance.decode(password); - } + password = PasswordMaskingUtil.resolveMask(useMaskedPassword, password, passwordCodec); } catch (Exception e) { throw ActiveMQRABundle.BUNDLE.errorDecodingPassword(e); } - } + initialized = true; } - public SensitiveDataCodec getCodecInstance() { - return codecInstance; + public String getCodec() { + return passwordCodec; } public String getJgroupsChannelLocatorClass() { diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java index a43d37b128..684792eb01 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java @@ -59,7 +59,6 @@ import org.apache.activemq.artemis.ra.inflow.ActiveMQActivationSpec; import org.apache.activemq.artemis.ra.recovery.RecoveryManager; import org.apache.activemq.artemis.service.extensions.ServiceUtils; import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConfig; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; import org.jboss.logging.Logger; import org.jgroups.JChannel; @@ -2037,8 +2036,8 @@ public class ActiveMQResourceAdapter implements ResourceAdapter, Serializable { managedConnectionFactories.add(activeMQRAManagedConnectionFactory); } - public SensitiveDataCodec getCodecInstance() { - return raProperties.getCodecInstance(); + public String getCodec() { + return raProperties.getCodec(); } public synchronized void closeConnectionFactory(ConnectionFactoryProperties properties) { diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java index dc4d648a9b..97498a2679 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java @@ -57,7 +57,7 @@ import org.apache.activemq.artemis.ra.ActiveMQRaUtils; import org.apache.activemq.artemis.ra.ActiveMQResourceAdapter; import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConfig; import org.apache.activemq.artemis.utils.FutureLatch; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.jboss.logging.Logger; /** @@ -148,16 +148,12 @@ public class ActiveMQActivation { logger.trace("constructor(" + ra + ", " + endpointFactory + ", " + spec + ")"); } - if (ra.isUseMaskedPassword()) { - String pass = spec.getOwnPassword(); - if (pass != null) { - SensitiveDataCodec codec = ra.getCodecInstance(); - - try { - spec.setPassword(codec.decode(pass)); - } catch (Exception e) { - throw new ResourceException(e); - } + String pass = spec.getOwnPassword(); + if (pass != null) { + try { + spec.setPassword(PasswordMaskingUtil.resolveMask(ra.isUseMaskedPassword(), pass, ra.getCodec())); + } catch (Exception e) { + throw new ResourceException(e); } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java index 6f44bbb5cc..f052248545 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java @@ -1000,12 +1000,12 @@ public interface Configuration { /** * Sets if passwords should be masked or not. True means the passwords should be masked. */ - Configuration setMaskPassword(boolean maskPassword); + Configuration setMaskPassword(Boolean maskPassword); /** * If passwords are masked. True means the passwords are masked. */ - boolean isMaskPassword(); + Boolean isMaskPassword(); /* * Whether or not that ActiveMQ Artemis should use all protocols available on the classpath. If false only the core protocol will diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java index ff68f153b5..93085f9b24 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java @@ -249,7 +249,7 @@ public class ConfigurationImpl implements Configuration, Serializable { protected List connectorServiceConfigurations = new ArrayList<>(); - private boolean maskPassword = ActiveMQDefaultConfiguration.isDefaultMaskPassword(); + private Boolean maskPassword = ActiveMQDefaultConfiguration.isDefaultMaskPassword(); private transient String passwordCodec; @@ -1461,12 +1461,12 @@ public class ConfigurationImpl implements Configuration, Serializable { } @Override - public boolean isMaskPassword() { + public Boolean isMaskPassword() { return maskPassword; } @Override - public ConfigurationImpl setMaskPassword(boolean maskPassword) { + public ConfigurationImpl setMaskPassword(Boolean maskPassword) { this.maskPassword = maskPassword; return this; } @@ -1673,7 +1673,6 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; if (asyncConnectionExecutionEnabled != other.asyncConnectionExecutionEnabled) return false; - if (bindingsDirectory == null) { if (other.bindingsDirectory != null) return false; @@ -1689,11 +1688,13 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; } else if (!broadcastGroupConfigurations.equals(other.broadcastGroupConfigurations)) return false; + if (clusterConfigurations == null) { if (other.clusterConfigurations != null) return false; } else if (!clusterConfigurations.equals(other.clusterConfigurations)) return false; + if (clusterPassword == null) { if (other.clusterPassword != null) return false; @@ -1720,6 +1721,7 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; if (createJournalDir != other.createJournalDir) return false; + if (discoveryGroupConfigurations == null) { if (other.discoveryGroupConfigurations != null) return false; @@ -1801,8 +1803,15 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; } else if (!managementNotificationAddress.equals(other.managementNotificationAddress)) return false; - if (maskPassword != other.maskPassword) - return false; + + if (this.maskPassword == null) { + if (other.maskPassword != null) + return false; + } else { + if (!this.maskPassword.equals(other.maskPassword)) + return false; + } + if (maxConcurrentPageIO != other.maxConcurrentPageIO) return false; if (memoryMeasureInterval != other.memoryMeasureInterval) @@ -1824,7 +1833,6 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; } else if (!name.equals(other.name)) return false; - if (outgoingInterceptorClassNames == null) { if (other.outgoingInterceptorClassNames != null) return false; @@ -1881,6 +1889,7 @@ public class ConfigurationImpl implements Configuration, Serializable { if (journalDatasync != other.journalDatasync) { return false; } + if (globalMaxSize != null && !globalMaxSize.equals(other.globalMaxSize)) { return false; } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java index d778aa5643..b33a41988c 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java @@ -22,7 +22,6 @@ import java.util.Set; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; @Deprecated public class FileSecurityConfiguration extends SecurityConfiguration { @@ -31,7 +30,7 @@ public class FileSecurityConfiguration extends SecurityConfiguration { private final String rolesUrl; - private boolean maskPassword; + private Boolean maskPassword; private String passwordCodec; @@ -65,14 +64,7 @@ public class FileSecurityConfiguration extends SecurityConfiguration { if (started) { return; } - SensitiveDataCodec codec = null; - if (maskPassword) { - if (passwordCodec != null) { - codec = PasswordMaskingUtil.getDefaultCodec(); - } else { - codec = PasswordMaskingUtil.getCodec(passwordCodec); - } - } + URL theUsersUrl = getClass().getClassLoader().getResource(usersUrl); if (theUsersUrl == null) { @@ -94,9 +86,7 @@ public class FileSecurityConfiguration extends SecurityConfiguration { for (String username : keys) { String password = userProps.getProperty(username); - if (codec != null) { - password = codec.decode(password); - } + password = PasswordMaskingUtil.resolveMask(this.maskPassword, password, passwordCodec); addUser(username, password); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java index 9b9050b47d..14b002501b 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java @@ -80,7 +80,6 @@ import org.apache.activemq.artemis.utils.ByteUtil; import org.apache.activemq.artemis.utils.ClassloadingUtil; import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; import org.apache.activemq.artemis.utils.XMLConfigurationUtil; import org.apache.activemq.artemis.utils.XMLUtil; import org.apache.activemq.artemis.utils.critical.CriticalAnalyzerPolicy; @@ -339,7 +338,7 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { config.setManagementNotificationAddress(new SimpleString(getString(e, "management-notification-address", config.getManagementNotificationAddress().toString(), Validators.NOT_NULL_OR_EMPTY))); - config.setMaskPassword(getBoolean(e, "mask-password", false)); + config.setMaskPassword(getBoolean(e, "mask-password", null)); config.setPasswordCodec(getString(e, "password-codec", DefaultSensitiveStringCodec.class.getName(), Validators.NOT_NULL_OR_EMPTY)); @@ -369,15 +368,11 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { // parsing cluster password String passwordText = getString(e, "cluster-password", null, Validators.NO_CHECK); - final boolean maskText = config.isMaskPassword(); + final Boolean maskText = config.isMaskPassword(); if (passwordText != null) { - if (maskText) { - SensitiveDataCodec codec = PasswordMaskingUtil.getCodec(config.getPasswordCodec()); - config.setClusterPassword(codec.decode(passwordText)); - } else { - config.setClusterPassword(passwordText); - } + String resolvedPassword = PasswordMaskingUtil.resolveMask(maskText, passwordText, config.getPasswordCodec()); + config.setClusterPassword(resolvedPassword); } config.setClusterUser(getString(e, "cluster-user", config.getClusterUser(), Validators.NO_CHECK)); @@ -1164,12 +1159,10 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { Map params = configurations.get(0).getParams(); - if (mainConfig.isMaskPassword()) { - params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); + params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); - if (mainConfig.getPasswordCodec() != null) { - params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); - } + if (mainConfig.getPasswordCodec() != null) { + params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); } return configurations.get(0); @@ -1187,12 +1180,10 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { Map params = configurations.get(0).getParams(); - if (mainConfig.isMaskPassword()) { - params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); + params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); - if (mainConfig.getPasswordCodec() != null) { - params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); - } + if (mainConfig.getPasswordCodec() != null) { + params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); } return configurations.get(0); @@ -1720,9 +1711,6 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { NodeList clusterPassNodes = brNode.getElementsByTagName("password"); String password = null; - boolean maskPassword = mainConfig.isMaskPassword(); - - SensitiveDataCodec codec = null; if (clusterPassNodes.getLength() > 0) { Node passNode = clusterPassNodes.item(0); @@ -1730,10 +1718,7 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { } if (password != null) { - if (maskPassword) { - codec = PasswordMaskingUtil.getCodec(mainConfig.getPasswordCodec()); - password = codec.decode(password); - } + password = PasswordMaskingUtil.resolveMask(mainConfig.isMaskPassword(), password, mainConfig.getPasswordCodec()); } else { password = ActiveMQDefaultConfiguration.getDefaultClusterPassword(); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java index a1ba4e743a..e24f4f6dc2 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java @@ -58,6 +58,7 @@ import java.util.Queue; import java.util.Set; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.jboss.logging.Logger; public class LDAPLoginModule implements LoginModule { @@ -83,6 +84,8 @@ public class LDAPLoginModule implements LoginModule { private static final String SASL_LOGIN_CONFIG_SCOPE = "saslLoginConfigScope"; private static final String AUTHENTICATE_USER = "authenticateUser"; private static final String REFERRAL = "referral"; + private static final String MASK_PASSWORD = "maskPassword"; + private static final String PASSWORD_CODEC = "passwordCodec"; protected DirContext context; @@ -97,6 +100,8 @@ public class LDAPLoginModule implements LoginModule { private boolean isRoleAttributeSet = false; private String roleAttributeName = null; + private String codecClass = null; + @Override public void initialize(Subject subject, CallbackHandler callbackHandler, @@ -105,12 +110,38 @@ public class LDAPLoginModule implements LoginModule { this.subject = subject; this.handler = callbackHandler; - config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)), new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)), new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)), new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)), new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)), new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)), new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)), new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)), new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)), new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)), new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)), new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)), new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)), new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)), new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)), new LDAPLoginProperty(REFERRAL, (String) options.get(REFERRAL))}; + config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)), + new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)), + new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)), + new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)), + new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)), + new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)), + new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)), + new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)), + new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)), + new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)), + new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)), + new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)), + new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)), + new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)), + new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), + new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)), + new LDAPLoginProperty(REFERRAL, (String) options.get(REFERRAL))}; + if (isLoginPropertySet(AUTHENTICATE_USER)) { authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER)); } isRoleAttributeSet = isLoginPropertySet(ROLE_NAME); roleAttributeName = getLDAPPropertyValue(ROLE_NAME); + codecClass = (String) options.get(PASSWORD_CODEC); + } + + private String getPlainPassword(String password) { + try { + return PasswordMaskingUtil.resolveMask(null, password, codecClass); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to decode password", e); + } } @Override @@ -511,7 +542,7 @@ public class LDAPLoginModule implements LoginModule { context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); } if (isLoginPropertySet(CONNECTION_PASSWORD)) { - context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); + context.addToEnvironment(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD))); } else { context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); } @@ -575,7 +606,7 @@ public class LDAPLoginModule implements LoginModule { } if (isLoginPropertySet(CONNECTION_PASSWORD)) { - env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); + env.put(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD))); } else { throw new NamingException("Empty password is not allowed"); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java b/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java index 7ce52800c1..29cfef9d57 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java @@ -116,7 +116,7 @@ public class XMLConfigurationUtil { return getTextBytesAsLongBytes(e, name, def, validator).intValue(); } - public static final Boolean getBoolean(final Element e, final String name, final boolean def) { + public static final Boolean getBoolean(final Element e, final String name, final Boolean def) { NodeList nl = e.getElementsByTagName(name); if (nl.getLength() > 0) { return XMLUtil.parseBoolean(nl.item(0)); diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java index 94d64ecc43..564372deff 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java @@ -19,10 +19,12 @@ package org.apache.activemq.artemis.core.config.impl; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.BridgeConfiguration; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.config.FileDeploymentManager; import org.apache.activemq.artemis.core.config.HAPolicyConfiguration; @@ -32,6 +34,7 @@ import org.apache.activemq.artemis.core.deployers.impl.FileConfigurationParser; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.junit.Assert; import org.junit.Test; @@ -190,6 +193,85 @@ public class FileConfigurationParserTest extends ActiveMQTestBase { assertEquals("newpassword", config.getClusterPassword()); } + @Test + public void testParsingDefaultServerConfigWithENCMaskedPwd() throws Exception { + FileConfigurationParser parser = new FileConfigurationParser(); + + String configStr = firstPart + lastPart; + ByteArrayInputStream input = new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8)); + + Configuration config = parser.parseMainConfig(input); + + String clusterPassword = config.getClusterPassword(); + + assertEquals(ActiveMQDefaultConfiguration.getDefaultClusterPassword(), clusterPassword); + + //if we add cluster-password, it should be default plain text + String clusterPasswordPart = "ENC(5aec0780b12bf225a13ab70c6c76bc8e)"; + + configStr = firstPart + clusterPasswordPart + lastPart; + + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + assertEquals("helloworld", config.getClusterPassword()); + + //if we add mask, it should be able to decode correctly + DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); + String mask = (String) codec.encode("helloworld"); + + clusterPasswordPart = "" + PasswordMaskingUtil.wrap(mask) + ""; + + configStr = firstPart + clusterPasswordPart + lastPart; + + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + assertEquals("helloworld", config.getClusterPassword()); + + //if we change key, it should be able to decode correctly + codec = new DefaultSensitiveStringCodec(); + Map prop = new HashMap<>(); + prop.put("key", "newkey"); + codec.init(prop); + + mask = (String) codec.encode("newpassword"); + + clusterPasswordPart = "" + PasswordMaskingUtil.wrap(mask) + ""; + + String codecPart = "" + "org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec" + + ";key=newkey"; + + configStr = firstPart + clusterPasswordPart + codecPart + lastPart; + + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + assertEquals("newpassword", config.getClusterPassword()); + + configStr = firstPart + bridgePart + lastPart; + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + List bridgeConfigs = config.getBridgeConfigurations(); + assertEquals(1, bridgeConfigs.size()); + + BridgeConfiguration bconfig = bridgeConfigs.get(0); + + assertEquals("helloworld", bconfig.getPassword()); + } + + private static String bridgePart = "\n" + + " \n" + + " sausage-factory\n" + + " mincing-machine\n" + + " \n" + + " org.apache.activemq.artemis.jms.example.HatColourChangeTransformer\n" + + " -1\n" + + " bridge-user" + + " ENC(5aec0780b12bf225a13ab70c6c76bc8e)" + + " \n" + + " remote-connector\n" + + " \n" + + " \n" + + "\n"; + private static String firstPart = "" + "\n" + "ActiveMQ.main.config" + "\n" + "org.apache.activemq.artemis.integration.logging.Log4jLogDelegateFactory" + "\n" + diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleMaskPasswordTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleMaskPasswordTest.java new file mode 100644 index 0000000000..cfb9934b46 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleMaskPasswordTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.artemis.core.security.jaas; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.integ.AbstractLdapTestUnit; +import org.apache.directory.server.core.integ.FrameworkRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(FrameworkRunner.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)}) +@ApplyLdifFiles("test.ldif") +public class LDAPLoginModuleMaskPasswordTest extends AbstractLdapTestUnit { + + private final String loginConfigSysPropName = "java.security.auth.login.config"; + private String oldLoginConfig; + + @Before + public void setLoginConfigSysProperty() { + oldLoginConfig = System.getProperty(loginConfigSysPropName, null); + System.setProperty(loginConfigSysPropName, "src/test/resources/login.config"); + } + + @After + public void resetLoginConfigSysProperty() { + if (oldLoginConfig != null) { + System.setProperty(loginConfigSysPropName, oldLoginConfig); + } + } + + @Test + public void testLoginMaskedPassword() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginMaskedPassword", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + context.login(); + context.logout(); + } + + @Test + public void testLoginMaskedPasswordUnauthenticated() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginMaskedPassword", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("nosecret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + try { + context.login(); + } catch (FailedLoginException le) { + assertEquals(le.getMessage(), "Password does not match for user: first"); + return; + } + fail("Should have failed authenticating"); + } + + @Test + public void testLoginExternalCodec() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginExternalPasswordCodec", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + + context.login(); + context.logout(); + } + + @Test + public void testLoginExternalCodec2() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginExternalPasswordCodec2", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + + context.login(); + context.logout(); + } + + @Test + public void testLoginExternalCodecUnauthenticated() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginExternalPasswordCodec", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("nosecret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + try { + context.login(); + } catch (FailedLoginException le) { + assertEquals(le.getMessage(), "Password does not match for user: first"); + return; + } + fail("Should have failed authenticating"); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java index 1ab5dbaec5..eb6c9fa3c1 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java @@ -76,6 +76,13 @@ public class PropertiesLoginModuleTest extends Assert { assertEquals("Should have zero principals", 0, subject.getPrincipals().size()); } + @Test + public void testLoginMasked() throws LoginException { + LoginContext context = new LoginContext("PropertiesLogin", new UserPassHandler("third", "helloworld")); + context.login(); + context.logout(); + } + @Test public void testLoginReload() throws Exception { File targetPropDir = new File("target/loginReloadTest"); diff --git a/artemis-server/src/test/resources/login.config b/artemis-server/src/test/resources/login.config index 997bfe5bd6..8e531caf06 100644 --- a/artemis-server/src/test/resources/login.config +++ b/artemis-server/src/test/resources/login.config @@ -125,3 +125,62 @@ OpenLdapConfiguration { roleSearchSubtree=true ; }; + +LDAPLoginMaskedPassword { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(-41e444c3ed07d6dd)" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=cn + roleSearchMatching="(member=uid={1},ou=system)" + roleSearchSubtree=false + ; +}; + +LDAPLoginExternalPasswordCodec { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + passwordCodec="org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;key=helloworld" + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(-170b9ef34d79ed12)" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=dummyRoleName + roleSearchMatching="(uid={1})" + roleSearchSubtree=false + ; +}; + +LDAPLoginExternalPasswordCodec2 { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + passwordCodec="org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec" + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(momsword)" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=dummyRoleName + roleSearchMatching="(uid={1})" + roleSearchSubtree=false + ; +}; diff --git a/artemis-server/src/test/resources/users.properties b/artemis-server/src/test/resources/users.properties index 1087b0b3f1..c1a6b4739d 100644 --- a/artemis-server/src/test/resources/users.properties +++ b/artemis-server/src/test/resources/users.properties @@ -17,3 +17,4 @@ first=secret second=password +third=ENC(1024:439F45267508BB4F02150B9AAFB8D774AB7FCDDE9B19D096F318787487F7BD17:78818E53AEE8AF26A34A38C1498D1CDA5636861D2FE9804FEDE3656D0BC05696191A027F095DF109EC8F6385FAE9971915449EC808945A0F5907B29D5F9D44B7) \ No newline at end of file diff --git a/artemis-web/pom.xml b/artemis-web/pom.xml index a9be5c0351..4edc47aec1 100644 --- a/artemis-web/pom.xml +++ b/artemis-web/pom.xml @@ -68,6 +68,13 @@ artemis-commons ${project.version} + + org.apache.activemq + artemis-commons + ${project.version} + tests + test + io.netty netty-buffer diff --git a/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java b/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java index 9964c719c9..d85d621807 100644 --- a/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java +++ b/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java @@ -68,12 +68,12 @@ public class WebServerComponent implements ExternalComponent { if ("https".equals(scheme)) { SslContextFactory sslFactory = new SslContextFactory(); sslFactory.setKeyStorePath(webServerConfig.keyStorePath == null ? artemisInstance + "/etc/keystore.jks" : webServerConfig.keyStorePath); - sslFactory.setKeyStorePassword(webServerConfig.keyStorePassword == null ? "password" : webServerConfig.keyStorePassword); + sslFactory.setKeyStorePassword(webServerConfig.getKeyStorePassword() == null ? "password" : webServerConfig.getKeyStorePassword()); if (webServerConfig.clientAuth != null) { sslFactory.setNeedClientAuth(webServerConfig.clientAuth); if (webServerConfig.clientAuth) { sslFactory.setTrustStorePath(webServerConfig.trustStorePath); - sslFactory.setTrustStorePassword(webServerConfig.trustStorePassword); + sslFactory.setTrustStorePassword(webServerConfig.getTrustStorePassword()); } } diff --git a/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java b/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java index 00691ed498..11781473af 100644 --- a/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java +++ b/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java @@ -18,6 +18,7 @@ package org.apache.activemq.cli.test; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -43,9 +44,11 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.ssl.SslHandler; import io.netty.util.CharsetUtil; +import org.apache.activemq.artemis.cli.factory.xml.XmlBrokerFactoryHandler; import org.apache.activemq.artemis.component.WebServerComponent; import org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport; import org.apache.activemq.artemis.core.server.ActiveMQComponent; +import org.apache.activemq.artemis.dto.BrokerDTO; import org.apache.activemq.artemis.dto.WebServerDTO; import org.junit.After; import org.junit.Assert; @@ -163,7 +166,7 @@ public class WebServerComponentTest extends Assert { webServerDTO.bind = "https://localhost:0"; webServerDTO.path = "webapps"; webServerDTO.keyStorePath = "./src/test/resources/server.keystore"; - webServerDTO.keyStorePassword = "password"; + webServerDTO.setKeyStorePassword("password"); WebServerComponent webServerComponent = new WebServerComponent(); Assert.assertFalse(webServerComponent.isStarted()); @@ -174,7 +177,7 @@ public class WebServerComponentTest extends Assert { // Make the connection attempt. String keyStoreProvider = "JKS"; - SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.keyStorePassword, keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.keyStorePassword); + SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.getKeyStorePassword(), keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.getKeyStorePassword()); SSLEngine engine = context.createSSLEngine(); engine.setUseClientMode(true); @@ -215,10 +218,10 @@ public class WebServerComponentTest extends Assert { webServerDTO.bind = "https://localhost:0"; webServerDTO.path = "webapps"; webServerDTO.keyStorePath = "./src/test/resources/server.keystore"; - webServerDTO.keyStorePassword = "password"; + webServerDTO.setKeyStorePassword("password"); webServerDTO.clientAuth = true; webServerDTO.trustStorePath = "./src/test/resources/server.keystore"; - webServerDTO.trustStorePassword = "password"; + webServerDTO.setTrustStorePassword("password"); WebServerComponent webServerComponent = new WebServerComponent(); Assert.assertFalse(webServerComponent.isStarted()); @@ -229,7 +232,7 @@ public class WebServerComponentTest extends Assert { // Make the connection attempt. String keyStoreProvider = "JKS"; - SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.keyStorePassword, keyStoreProvider, webServerDTO.trustStorePath, webServerDTO.trustStorePassword); + SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.getKeyStorePassword(), keyStoreProvider, webServerDTO.trustStorePath, webServerDTO.getTrustStorePassword()); SSLEngine engine = context.createSSLEngine(); engine.setUseClientMode(true); @@ -264,6 +267,44 @@ public class WebServerComponentTest extends Assert { Assert.assertFalse(webServerComponent.isStarted()); } + @Test + public void testDefaultMaskPasswords() throws Exception { + File bootstrap = new File("./target/test-classes/bootstrap_web.xml"); + File brokerHome = new File("./target"); + XmlBrokerFactoryHandler xmlHandler = new XmlBrokerFactoryHandler(); + BrokerDTO broker = xmlHandler.createBroker(bootstrap.toURI(), brokerHome.getAbsolutePath(), brokerHome.getAbsolutePath(), brokerHome.toURI()); + assertNotNull(broker.web); + assertNull(broker.web.passwordCodec); + } + + @Test + public void testMaskPasswords() throws Exception { + final String keyPassword = "keypass"; + final String trustPassword = "trustpass"; + File bootstrap = new File("./target/test-classes/bootstrap_secure_web.xml"); + File brokerHome = new File("./target"); + XmlBrokerFactoryHandler xmlHandler = new XmlBrokerFactoryHandler(); + BrokerDTO broker = xmlHandler.createBroker(bootstrap.toURI(), brokerHome.getAbsolutePath(), brokerHome.getAbsolutePath(), brokerHome.toURI()); + assertNotNull(broker.web); + assertEquals(keyPassword, broker.web.getKeyStorePassword()); + assertEquals(trustPassword, broker.web.getTrustStorePassword()); + } + + @Test + public void testMaskPasswordCodec() throws Exception { + final String keyPassword = "keypass"; + final String trustPassword = "trustpass"; + File bootstrap = new File("./target/test-classes/bootstrap_web_codec.xml"); + File brokerHome = new File("./target"); + XmlBrokerFactoryHandler xmlHandler = new XmlBrokerFactoryHandler(); + BrokerDTO broker = xmlHandler.createBroker(bootstrap.toURI(), brokerHome.getAbsolutePath(), brokerHome.getAbsolutePath(), brokerHome.toURI()); + assertNotNull(broker.web); + assertNotNull("password codec not picked up!", broker.web.passwordCodec); + + assertEquals(keyPassword, broker.web.getKeyStorePassword()); + assertEquals(trustPassword, broker.web.getTrustStorePassword()); + } + class ClientHandler extends SimpleChannelInboundHandler { private CountDownLatch latch; diff --git a/artemis-web/src/test/resources/bootstrap_secure_web.xml b/artemis-web/src/test/resources/bootstrap_secure_web.xml new file mode 100644 index 0000000000..affa9abb79 --- /dev/null +++ b/artemis-web/src/test/resources/bootstrap_secure_web.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/artemis-web/src/test/resources/bootstrap_web.xml b/artemis-web/src/test/resources/bootstrap_web.xml new file mode 100644 index 0000000000..847a869245 --- /dev/null +++ b/artemis-web/src/test/resources/bootstrap_web.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/artemis-web/src/test/resources/bootstrap_web_codec.xml b/artemis-web/src/test/resources/bootstrap_web_codec.xml new file mode 100644 index 0000000000..7b5351531a --- /dev/null +++ b/artemis-web/src/test/resources/bootstrap_web_codec.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/user-manual/en/masking-passwords.md b/docs/user-manual/en/masking-passwords.md index 4f435247f0..32329bacdc 100644 --- a/docs/user-manual/en/masking-passwords.md +++ b/docs/user-manual/en/masking-passwords.md @@ -18,12 +18,33 @@ Apache ActiveMQ Artemis provides a default password encoder and decoder. Optiona users can use or implement their own encoder and decoder for masking the passwords. +In general, a masked password can be identified using one of two ways. The first one +is the ENC() syntax, i.e. any string value wrapped in ENC() is to be treated as +a masked password. For example + +`ENC(xyz)` + +The above indicates that the password is masked and the masked value is `xyz`. + +The ENC() syntax is the preferred way to indicating a masked password and is +universally supported in every password configuration in Artemis. + +The other way is to use a `mask-password` attribute to tell that a password +in a configuration file should be treated as 'masked'. For example: + +``` +true +xyz +``` +This method is now deprecated and exists only to maintain backward-compatibility. +Newer configurations may not support it. + ### Password Masking in Server Configuration File #### General Masking Configuration -The server configuration file (i.e. broker.xml )has a property that defines the -default masking behaviors over the entire file scope. +Besides supporting the ENC() syntax, the server configuration file (i.e. broker.xml) +has a property that defines the default masking behaviors over the entire file scope. `mask-password`: this boolean type property indicates if a password should be masked or not. Set it to "true" if you want your passwords @@ -38,6 +59,8 @@ will be used. ##### cluster-password +If it is specified in ENC() syntax it will be treated as masked, or + If `mask-password` is `true` the `cluster-password` will be treated as masked. ##### Passwords in connectors and acceptors @@ -55,16 +78,16 @@ and `activemq.passwordcodec` respectively. The Netty and InVM implementations will use these as needed and any other implementations will have access to these to use if they so wish. +The preferred way, however, is to use the ENC() syntax. + ##### Passwords in bridge configurations Core Bridges are configured in the server configuration file and so the masking of its `password` properties follows the same rules as that of -`cluster-password`. +`cluster-password`. It supports ENC() syntax. -#### Examples - -The following table summarizes the relations among the above-mentioned -properties +For using 'mask-password' property, the following table summarizes the +relations among the above-mentioned properties mask-password | cluster-password | acceptor/connector passwords | bridge password :------------- | :---------------- | :--------------------------- | :--------------- @@ -72,7 +95,9 @@ properties false | plain text | plain text | plain text true | masked | masked | masked -Examples +It is recommended that you use the ENC() syntax for new applications/deployments. + +#### Examples Note: In the following examples if related attributed or properties are absent, it means they are not specified in the configure file. @@ -87,6 +112,14 @@ This indicates the cluster password is a plain text value ("bbc"). example 2 +```xml +ENC(xyz) +``` + +This indicates the cluster password is a masked value ("xyz"). + +example 3 + ```xml true 80cf731af62c290 @@ -97,27 +130,75 @@ use its built-in decoder to decode it. All other passwords in the configuration file, Connectors, Acceptors and Bridges, will also use masked passwords. +#### Passwords in bootstrap.xml + +The broker embeds a web-server for hosting some web applications such as a +management console. It is configured in bootstrap.xml as a web +component. The web server can be secured using https protocol, and it can be +configured with a keystore password and/or truststore password which by +default are specified in plain text forms. + +To mask these passwords you need to use ENC() syntax. The `mask-password` is +not supported here. + +You can also set the `passwordCodec` attribute if you want to use a password codec +other than the default one. For example + +```xml + + + +``` + ### Masking passwords in ActiveMQ Artemis JCA ResourceAdapter and MDB activation configurations Both ra.xml and MDB activation configuration have a `password` property -that can be masked. They are controlled by the following two optional -Resource Adapter properties in ra.xml: +that can be masked preferably using ENC() syntax. + +Alternatively it can use a optional attribute in ra.xml to indicate that a password +is masked: `UseMaskedPassword` -- If setting to "true" the passwords are masked. Default is false. +There is another property in ra.xml that can specify a codec: + `PasswordCodec` -- Class name and its parameters for the Decoder used to decode the masked password. Ignored if UseMaskedPassword is false. The format of this property is a full qualified class name optionally followed by key/value pairs. It is the same format as that for JMS Bridges. Example: +Example 1 Using the ENC() syntax: + +```xml + + password + String + ENC(xyz) + + + PasswordCodec + java.lang.String + com.foo.ADecoder;key=helloworld + +``` + +Example 2 Using the "UseMaskedPassword" property: + ```xml UseMaskedPassword boolean true + + password + String + xyz + PasswordCodec java.lang.String @@ -150,6 +231,43 @@ Passwords in `artemis-users.properties` are automatically detected as hashed or by looking for the syntax `ENC()`. The `mask-password` parameter does not need to be `true` to use hashed passwords here. +### Masking password in JAAS login config file (login.config) + +Artemis supports LDAP login modules to be configured in JAAS configuration +file (default name is `login.config`). When connecting to a LDAP server usually +you need to supply a connection password in the config file. By default this +password is in plain text form. + +To mask it you need to configure the passwords in your login module +using ENC() syntax. To specify a codec using the following property: + +`passwordCodec` - the password codec class name. (the default codec +will be used if it is absent) + +For example: + +``` +LDAPLoginExternalPasswordCodec { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(-170b9ef34d79ed12)" + passwordCodec="org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;key=helloworld" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=dummyRoleName + roleSearchMatching="(uid={1})" + roleSearchSubtree=false + ; +}; +``` + ### Choosing a decoder for password masking As described in the previous sections, all password masking requires a @@ -206,8 +324,7 @@ pairs when configuring. For instance if your decoder needs say a Then configure your cluster-password like this: ```xml - true - masked_password + ENC(masked_password) ``` When Apache ActiveMQ Artemis reads the cluster-password it will initialize the diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java index c7a1474a8a..811cc29dba 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java @@ -41,6 +41,7 @@ import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConf import org.apache.activemq.artemis.tests.unit.ra.BootstrapContext; import org.apache.activemq.artemis.tests.unit.ra.MessageEndpointFactory; import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.junit.Test; public class ResourceAdapterTest extends ActiveMQRATestBase { @@ -565,7 +566,7 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { ActiveMQRATestBase.MyBootstrapContext ctx = new ActiveMQRATestBase.MyBootstrapContext(); DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); - String mask = (String) codec.encode("helloworld"); + String mask = codec.encode("helloworld"); qResourceAdapter.setUseMaskedPassword(true); qResourceAdapter.setPassword(mask); @@ -594,6 +595,41 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { assertTrue(endpoint.released); } + @Test + public void testMaskPasswordENC() throws Exception { + ActiveMQResourceAdapter qResourceAdapter = new ActiveMQResourceAdapter(); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + ActiveMQRATestBase.MyBootstrapContext ctx = new ActiveMQRATestBase.MyBootstrapContext(); + + DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); + String mask = codec.encode("helloworld"); + + qResourceAdapter.setPassword(PasswordMaskingUtil.wrap(mask)); + + qResourceAdapter.start(ctx); + + assertEquals("helloworld", qResourceAdapter.getPassword()); + + ActiveMQActivationSpec spec = new ActiveMQActivationSpec(); + spec.setResourceAdapter(qResourceAdapter); + spec.setUseJNDI(false); + spec.setDestinationType("javax.jms.Queue"); + spec.setDestination(MDBQUEUE); + + mask = codec.encode("mdbpassword"); + spec.setPassword(PasswordMaskingUtil.wrap(mask)); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + CountDownLatch latch = new CountDownLatch(1); + DummyMessageEndpoint endpoint = new DummyMessageEndpoint(latch); + DummyMessageEndpointFactory endpointFactory = new DummyMessageEndpointFactory(endpoint, false); + qResourceAdapter.endpointActivation(endpointFactory, spec); + + assertEquals("mdbpassword", spec.getPassword()); + + qResourceAdapter.stop(); + assertTrue(endpoint.released); + } + @Test public void testMaskPassword2() throws Exception { ActiveMQResourceAdapter qResourceAdapter = new ActiveMQResourceAdapter(); @@ -609,7 +645,7 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { prop.put("key", "anotherkey"); codec.init(prop); - String mask = (String) codec.encode("helloworld"); + String mask = codec.encode("helloworld"); qResourceAdapter.setPassword(mask); @@ -623,7 +659,7 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { spec.setDestinationType("javax.jms.Queue"); spec.setDestination(MDBQUEUE); - mask = (String) codec.encode("mdbpassword"); + mask = codec.encode("mdbpassword"); spec.setPassword(mask); qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); CountDownLatch latch = new CountDownLatch(1); @@ -637,6 +673,48 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { assertTrue(endpoint.released); } + @Test + public void testMaskPassword2ENC() throws Exception { + ActiveMQResourceAdapter qResourceAdapter = new ActiveMQResourceAdapter(); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + ActiveMQRATestBase.MyBootstrapContext ctx = new ActiveMQRATestBase.MyBootstrapContext(); + + qResourceAdapter.setPasswordCodec(DefaultSensitiveStringCodec.class.getName() + ";key=anotherkey"); + + DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); + Map prop = new HashMap<>(); + + prop.put("key", "anotherkey"); + codec.init(prop); + + String mask = codec.encode("helloworld"); + + qResourceAdapter.setPassword(PasswordMaskingUtil.wrap(mask)); + + qResourceAdapter.start(ctx); + + assertEquals("helloworld", qResourceAdapter.getPassword()); + + ActiveMQActivationSpec spec = new ActiveMQActivationSpec(); + spec.setResourceAdapter(qResourceAdapter); + spec.setUseJNDI(false); + spec.setDestinationType("javax.jms.Queue"); + spec.setDestination(MDBQUEUE); + + mask = codec.encode("mdbpassword"); + spec.setPassword(PasswordMaskingUtil.wrap(mask)); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + CountDownLatch latch = new CountDownLatch(1); + DummyMessageEndpoint endpoint = new DummyMessageEndpoint(latch); + DummyMessageEndpointFactory endpointFactory = new DummyMessageEndpointFactory(endpoint, false); + qResourceAdapter.endpointActivation(endpointFactory, spec); + + assertEquals("mdbpassword", spec.getPassword()); + + qResourceAdapter.stop(); + assertTrue(endpoint.released); + } + @Test public void testConnectionParameterStringParsing() throws Exception { ActiveMQResourceAdapter resourceAdapter = new ActiveMQResourceAdapter(); diff --git a/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java b/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java index 9007bb40cc..f6a08a05fb 100644 --- a/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java +++ b/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java @@ -287,6 +287,45 @@ public class JMSBridgeImplTest extends ActiveMQTestBase { * we receive only 1 message. The message is sent when the maxBatchTime * expires even if the maxBatchSize is not reached */ + @Test + public void testBridgeWithMaskPasswords() throws Exception { + + ConnectionFactoryFactory sourceCFF = JMSBridgeImplTest.newConnectionFactoryFactory(JMSBridgeImplTest.createConnectionFactory()); + ConnectionFactoryFactory targetCFF = JMSBridgeImplTest.newConnectionFactoryFactory(JMSBridgeImplTest.createConnectionFactory()); + DestinationFactory sourceDF = JMSBridgeImplTest.newDestinationFactory(ActiveMQJMSClient.createQueue(JMSBridgeImplTest.SOURCE)); + DestinationFactory targetDF = JMSBridgeImplTest.newDestinationFactory(ActiveMQJMSClient.createQueue(JMSBridgeImplTest.TARGET)); + TransactionManager tm = JMSBridgeImplTest.newTransactionManager(); + + JMSBridgeImpl bridge = new JMSBridgeImpl(); + Assert.assertNotNull(bridge); + + bridge.setSourceConnectionFactoryFactory(sourceCFF); + bridge.setSourceDestinationFactory(sourceDF); + bridge.setTargetConnectionFactoryFactory(targetCFF); + bridge.setTargetDestinationFactory(targetDF); + bridge.setFailureRetryInterval(10); + bridge.setMaxRetries(1); + bridge.setMaxBatchSize(1); + bridge.setMaxBatchTime(-1); + bridge.setTransactionManager(tm); + bridge.setQualityOfServiceMode(QualityOfServiceMode.AT_MOST_ONCE); + + bridge.setSourceUsername("sourceuser"); + bridge.setSourcePassword("ENC(5493dd76567ee5ec269d11823973462f)"); + bridge.setTargetUsername("targetuser"); + bridge.setTargetPassword("ENC(56a0db3b71043054269d11823973462f)"); + + Assert.assertFalse(bridge.isStarted()); + bridge.start(); + Assert.assertTrue(bridge.isStarted()); + + assertEquals("sourcepassword", bridge.getSourcePassword()); + assertEquals("targetpassword", bridge.getTargetPassword()); + + bridge.stop(); + Assert.assertFalse(bridge.isStarted()); + } + @Test public void testSendMessagesWhenMaxBatchTimeExpires() throws Exception { int maxBatchSize = 2;