diff --git a/artemis-cli/pom.xml b/artemis-cli/pom.xml
index e8c8358920..0589955494 100644
--- a/artemis-cli/pom.xml
+++ b/artemis-cli/pom.xml
@@ -30,6 +30,8 @@
${project.basedir}/..
1.18
+ 2.1
+ 3.0
@@ -67,6 +69,16 @@
io.airlift
airline
+
+ org.apache.commons
+ commons-configuration2
+ ${commons.config.version}
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons.lang.version}
+
com.sun.winsw
winsw
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java
index 16dcd03f0b..17c4457010 100644
--- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java
@@ -27,6 +27,7 @@ import org.apache.activemq.artemis.cli.commands.ActionContext;
import org.apache.activemq.artemis.cli.commands.Create;
import org.apache.activemq.artemis.cli.commands.HelpAction;
import org.apache.activemq.artemis.cli.commands.Kill;
+import org.apache.activemq.artemis.cli.commands.Mask;
import org.apache.activemq.artemis.cli.commands.Run;
import org.apache.activemq.artemis.cli.commands.Stop;
import org.apache.activemq.artemis.cli.commands.destination.CreateDestination;
@@ -42,6 +43,11 @@ import org.apache.activemq.artemis.cli.commands.tools.HelpData;
import org.apache.activemq.artemis.cli.commands.tools.PrintData;
import org.apache.activemq.artemis.cli.commands.tools.XmlDataExporter;
import org.apache.activemq.artemis.cli.commands.tools.XmlDataImporter;
+import org.apache.activemq.artemis.cli.commands.user.AddUser;
+import org.apache.activemq.artemis.cli.commands.user.HelpUser;
+import org.apache.activemq.artemis.cli.commands.user.ListUser;
+import org.apache.activemq.artemis.cli.commands.user.RemoveUser;
+import org.apache.activemq.artemis.cli.commands.user.ResetUser;
/**
* Artemis is the main CLI entry point for managing/running a broker.
@@ -120,7 +126,7 @@ public class Artemis {
private static Cli.CliBuilder builder(File artemisInstance) {
String instance = artemisInstance != null ? artemisInstance.getAbsolutePath() : System.getProperty("artemis.instance");
- Cli.CliBuilder builder = Cli.builder("artemis").withDescription("ActiveMQ Artemis Command Line").withCommand(HelpAction.class).withCommand(Producer.class).withCommand(Consumer.class).withCommand(Browse.class).withDefaultCommand(HelpAction.class);
+ Cli.CliBuilder builder = Cli.builder("artemis").withDescription("ActiveMQ Artemis Command Line").withCommand(HelpAction.class).withCommand(Producer.class).withCommand(Consumer.class).withCommand(Browse.class).withCommand(Mask.class).withDefaultCommand(HelpAction.class);
builder.withGroup("destination").withDescription("Destination tools group (create|delete) (example ./artemis destination create)").
withDefaultCommand(HelpDestination.class).withCommands(CreateDestination.class, DeleteDestination.class);
@@ -128,6 +134,8 @@ public class Artemis {
if (instance != null) {
builder.withGroup("data").withDescription("data tools group (print|exp|imp|exp|encode|decode|compact) (example ./artemis data print)").
withDefaultCommand(HelpData.class).withCommands(PrintData.class, XmlDataExporter.class, XmlDataImporter.class, DecodeJournal.class, EncodeJournal.class, CompactJournal.class);
+ builder.withGroup("user").withDescription("default file-based user management (add|rm|list|reset) (example ./artemis user list)").
+ withDefaultCommand(HelpUser.class).withCommands(ListUser.class, AddUser.class, RemoveUser.class, ResetUser.class);
builder = builder.withCommands(Run.class, Stop.class, Kill.class);
} else {
builder.withGroup("data").withDescription("data tools group (print) (example ./artemis data print)").
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java
index be788cdea3..5bd55e9509 100644
--- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java
@@ -38,6 +38,7 @@ import io.airlift.airline.Arguments;
import io.airlift.airline.Command;
import io.airlift.airline.Option;
import org.apache.activemq.artemis.cli.CLIException;
+import org.apache.activemq.artemis.cli.commands.util.HashUtil;
import org.apache.activemq.artemis.cli.commands.util.SyncCalculation;
import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType;
import org.apache.activemq.artemis.jlibaio.LibaioContext;
@@ -415,9 +416,11 @@ public class Create extends InputAbstract {
public String getPassword() {
if (password == null) {
- this.password = inputPassword("--password", "Please provide the default password:", "admin");
+ password = inputPassword("--password", "Please provide the default password:", "admin");
}
+ password = HashUtil.tryHash(context, password);
+
return password;
}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Mask.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Mask.java
new file mode 100644
index 0000000000..7b845a2e5b
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Mask.java
@@ -0,0 +1,101 @@
+/*
+ * 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.cli.commands;
+
+import io.airlift.airline.Arguments;
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
+import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+@Command(name = "mask", description = "mask a password and print it out")
+public class Mask implements Action {
+
+ @Arguments(description = "The password to be masked", required = true)
+ String password;
+
+ @Option(name = "--hash", description = "whether to use hash (one-way), default false")
+ boolean hash = false;
+
+ @Option(name = "--key", description = "the key (Blowfish) to mask a password")
+ String key;
+
+ private DefaultSensitiveStringCodec codec;
+
+ @Override
+ public Object execute(ActionContext context) throws Exception {
+ Map params = new HashMap<>();
+
+ if (hash) {
+ params.put(DefaultSensitiveStringCodec.ALGORITHM, DefaultSensitiveStringCodec.ONE_WAY);
+ }
+
+ if (key != null) {
+ if (hash) {
+ context.out.println("Option --key ignored in case of hashing");
+ } else {
+ params.put(DefaultSensitiveStringCodec.BLOWFISH_KEY, key);
+ }
+ }
+
+ codec = PasswordMaskingUtil.getDefaultCodec();
+ codec.init(params);
+
+ String masked = codec.encode(password);
+ context.out.println("result: " + masked);
+ return masked;
+ }
+
+ @Override
+ public boolean isVerbose() {
+ return false;
+ }
+
+ @Override
+ public void setHomeValues(File brokerHome, File brokerInstance) {
+ }
+
+ @Override
+ public String getBrokerInstance() {
+ return null;
+ }
+
+ @Override
+ public String getBrokerHome() {
+ return null;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setHash(boolean hash) {
+ this.hash = hash;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public DefaultSensitiveStringCodec getCodec() {
+ return codec;
+ }
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/AddUser.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/AddUser.java
new file mode 100644
index 0000000000..cbb8f60420
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/AddUser.java
@@ -0,0 +1,62 @@
+/*
+ * 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.cli.commands.user;
+
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
+import org.apache.activemq.artemis.cli.commands.ActionContext;
+import org.apache.activemq.artemis.cli.commands.util.HashUtil;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Adding a new user, example:
+ * ./artemis user add --username guest --role admin --password ***
+ */
+@Command(name = "add", description = "Add a new user")
+public class AddUser extends UserAction {
+
+ @Option(name = "--password", description = "the password (Default: input)")
+ String password;
+
+ @Option(name = "--role", description = "user's role(s), comma separated", required = true)
+ String role;
+
+ @Option(name = "--plaintext", description = "using plaintext (Default false)")
+ boolean plaintext = false;
+
+ @Override
+ public Object execute(ActionContext context) throws Exception {
+ super.execute(context);
+
+ if (password == null) {
+ password = inputPassword("--password", "Please provide the password:", null);
+ }
+
+ String hash = plaintext ? password : HashUtil.tryHash(context, password);
+ add(hash, StringUtils.split(role, ","));
+
+ return null;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/HelpUser.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/HelpUser.java
new file mode 100644
index 0000000000..7a898bead6
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/HelpUser.java
@@ -0,0 +1,55 @@
+/*
+ * 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.cli.commands.user;
+
+import io.airlift.airline.Help;
+import org.apache.activemq.artemis.cli.commands.Action;
+import org.apache.activemq.artemis.cli.commands.ActionContext;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class HelpUser extends Help implements Action {
+
+ @Override
+ public boolean isVerbose() {
+ return false;
+ }
+
+ @Override
+ public void setHomeValues(File brokerHome, File brokerInstance) {
+ }
+
+ @Override
+ public String getBrokerInstance() {
+ return null;
+ }
+
+ @Override
+ public String getBrokerHome() {
+ return null;
+ }
+
+ @Override
+ public Object execute(ActionContext context) throws Exception {
+ List commands = new ArrayList<>(1);
+ commands.add("user");
+ help(global, commands);
+ return null;
+ }
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ListUser.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ListUser.java
new file mode 100644
index 0000000000..cb3ff39e2b
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ListUser.java
@@ -0,0 +1,38 @@
+/*
+ * 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.cli.commands.user;
+
+import io.airlift.airline.Command;
+import org.apache.activemq.artemis.cli.commands.ActionContext;
+
+/**
+ * list existing users, example:
+ * ./artemis user list --username guest
+ */
+@Command(name = "list", description = "List existing user(s)")
+public class ListUser extends UserAction {
+
+ @Override
+ public Object execute(ActionContext context) throws Exception {
+ super.execute(context);
+
+ list();
+
+ return null;
+ }
+
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/RemoveUser.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/RemoveUser.java
new file mode 100644
index 0000000000..172a76d08a
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/RemoveUser.java
@@ -0,0 +1,36 @@
+/*
+ * 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.cli.commands.user;
+
+import io.airlift.airline.Command;
+import org.apache.activemq.artemis.cli.commands.ActionContext;
+
+/**
+ * Remove a user, example:
+ * ./artemis user rm --username guest
+ */
+@Command(name = "rm", description = "Remove an existing user")
+public class RemoveUser extends UserAction {
+
+ @Override
+ public Object execute(ActionContext context) throws Exception {
+ super.execute(context);
+ remove();
+ return null;
+ }
+
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ResetUser.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ResetUser.java
new file mode 100644
index 0000000000..27da6c74eb
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ResetUser.java
@@ -0,0 +1,65 @@
+/*
+ * 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.cli.commands.user;
+
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
+import org.apache.activemq.artemis.cli.commands.ActionContext;
+import org.apache.activemq.artemis.cli.commands.util.HashUtil;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Reset a user's password or roles, example:
+ * ./artemis user reset --username guest --role admin --password ***
+ */
+@Command(name = "reset", description = "Reset user's password or roles")
+public class ResetUser extends UserAction {
+
+ @Option(name = "--password", description = "the password (Default: input)")
+ String password;
+
+ @Option(name = "--role", description = "user's role(s), comma separated")
+ String role;
+
+ @Option(name = "--plaintext", description = "using plaintext (Default false)")
+ boolean plaintext = false;
+
+ @Override
+ public Object execute(ActionContext context) throws Exception {
+ super.execute(context);
+
+ if (password != null) {
+ password = plaintext ? password : HashUtil.tryHash(context, password);
+ }
+
+ String[] roles = null;
+ if (role != null) {
+ roles = StringUtils.split(role, ",");
+ }
+
+ reset(password, roles);
+ return null;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/UserAction.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/UserAction.java
new file mode 100644
index 0000000000..918338e371
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/UserAction.java
@@ -0,0 +1,107 @@
+/*
+ * 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.cli.commands.user;
+
+import io.airlift.airline.Option;
+import org.apache.activemq.artemis.cli.commands.InputAbstract;
+import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule;
+import org.apache.activemq.artemis.util.FileBasedSecStoreConfig;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import java.io.File;
+import java.util.List;
+
+import static org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule.ROLE_FILE_PROP_NAME;
+import static org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule.USER_FILE_PROP_NAME;
+
+public abstract class UserAction extends InputAbstract {
+
+ @Option(name = "--user", description = "The user name")
+ String username = null;
+
+ /**
+ * Adding a new user
+ * @param hash the password
+ * @param role the role
+ * @throws IllegalArgumentException if user exists
+ */
+ protected void add(String hash, String... role) throws Exception {
+ FileBasedSecStoreConfig config = getConfiguration();
+ config.addNewUser(username, hash, role);
+ config.save();
+ context.out.println("User added successfully.");
+ }
+
+ /**
+ * list a single user or all users
+ * if username is not specified
+ */
+ protected void list() throws Exception {
+ FileBasedSecStoreConfig config = getConfiguration();
+ List result = config.listUser(username);
+ for (String str : result) {
+ context.out.println(str);
+ }
+ }
+
+ protected void remove() throws Exception {
+ FileBasedSecStoreConfig config = getConfiguration();
+ config.removeUser(username);
+ config.save();
+ context.out.println("User removed.");
+ }
+
+ protected void reset(String password, String[] roles) throws Exception {
+ if (password == null && roles == null) {
+ context.err.println("Nothing to update.");
+ return;
+ }
+ FileBasedSecStoreConfig config = getConfiguration();
+ config.updateUser(username, password, roles);
+ config.save();
+ context.out.println("User updated");
+ }
+
+ private FileBasedSecStoreConfig getConfiguration() throws Exception {
+
+ Configuration securityConfig = Configuration.getConfiguration();
+ AppConfigurationEntry[] entries = securityConfig.getAppConfigurationEntry("activemq");
+
+ for (AppConfigurationEntry entry : entries) {
+ if (entry.getLoginModuleName().equals(PropertiesLoginModule.class.getName())) {
+ String userFileName = (String) entry.getOptions().get(USER_FILE_PROP_NAME);
+ String roleFileName = (String) entry.getOptions().get(ROLE_FILE_PROP_NAME);
+
+ File etcDir = new File(getBrokerInstance(), "etc");
+ File userFile = new File(etcDir, userFileName);
+ File roleFile = new File(etcDir, roleFileName);
+
+ if (!userFile.exists() || !roleFile.exists()) {
+ throw new IllegalArgumentException("Couldn't find user file or role file!");
+ }
+
+ return new FileBasedSecStoreConfig(userFile, roleFile);
+ }
+ }
+ throw new IllegalArgumentException("Failed to load security file");
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/HashUtil.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/HashUtil.java
new file mode 100644
index 0000000000..67b9e448d1
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/HashUtil.java
@@ -0,0 +1,41 @@
+/*
+ * 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.cli.commands.util;
+
+import org.apache.activemq.artemis.cli.commands.ActionContext;
+import org.apache.activemq.artemis.utils.HashProcessor;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+
+public class HashUtil {
+
+ private static final HashProcessor HASH_PROCESSOR = PasswordMaskingUtil.getHashProcessor();
+
+ //calculate the hash for plaintext.
+ //any exception will cause plaintext returned unchanged.
+ public static String tryHash(ActionContext context, String plaintext) {
+
+ try {
+ String hash = HASH_PROCESSOR.hash(plaintext);
+ return hash;
+ } catch (Exception e) {
+ context.err.println("Warning: Failed to calculate hash value for password using " + HASH_PROCESSOR);
+ context.err.println("Reason: " + e.getMessage());
+ e.printStackTrace();
+ }
+ return plaintext;
+ }
+}
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/util/FileBasedSecStoreConfig.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/util/FileBasedSecStoreConfig.java
new file mode 100644
index 0000000000..c3f30b3986
--- /dev/null
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/util/FileBasedSecStoreConfig.java
@@ -0,0 +1,222 @@
+/*
+ * 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.util;
+
+import org.apache.activemq.artemis.api.core.Pair;
+import org.apache.activemq.artemis.utils.StringUtil;
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
+import org.apache.commons.configuration2.builder.fluent.Configurations;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class FileBasedSecStoreConfig {
+
+ private static final String LICENSE_HEADER =
+ "## ---------------------------------------------------------------------------\n" +
+ "## Licensed to the Apache Software Foundation (ASF) under one or more\n" +
+ "## contributor license agreements. See the NOTICE file distributed with\n" +
+ "## this work for additional information regarding copyright ownership.\n" +
+ "## The ASF licenses this file to You under the Apache License, Version 2.0\n" +
+ "## (the \"License\"); you may not use this file except in compliance with\n" +
+ "## the License. You may obtain a copy of the License at\n" +
+ "##\n" +
+ "## http://www.apache.org/licenses/LICENSE-2.0\n" +
+ "##\n" +
+ "## Unless required by applicable law or agreed to in writing, software\n" +
+ "## distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
+ "## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
+ "## See the License for the specific language governing permissions and\n" +
+ "## limitations under the License.\n" +
+ "## ---------------------------------------------------------------------------\n";
+ private FileBasedConfigurationBuilder userBuilder;
+ private FileBasedConfigurationBuilder roleBuilder;
+ private PropertiesConfiguration userConfig;
+ private PropertiesConfiguration roleConfig;
+
+ public FileBasedSecStoreConfig(File userFile, File roleFile) throws Exception {
+ Configurations configs = new Configurations();
+ userBuilder = configs.propertiesBuilder(userFile);
+ roleBuilder = configs.propertiesBuilder(roleFile);
+ userConfig = userBuilder.getConfiguration();
+ roleConfig = roleBuilder.getConfiguration();
+
+ String roleHeader = roleConfig.getLayout().getHeaderComment();
+ String userHeader = userConfig.getLayout().getHeaderComment();
+
+ if (userHeader == null) {
+ if (userConfig.isEmpty()) {
+ //clean and reset header
+ userConfig.clear();
+ userConfig.setHeader(LICENSE_HEADER);
+ }
+ }
+
+ if (roleHeader == null) {
+ if (roleConfig.isEmpty()) {
+ //clean and reset header
+ roleConfig.clear();
+ roleConfig.setHeader(LICENSE_HEADER);
+ }
+ }
+ }
+
+ public void addNewUser(String username, String hash, String... roles) throws Exception {
+ if (userConfig.getString(username) != null) {
+ throw new IllegalArgumentException("User already exist: " + username);
+ }
+ userConfig.addProperty(username, hash);
+ addRoles(username, roles);
+ }
+
+ public void save() throws Exception {
+ userBuilder.save();
+ roleBuilder.save();
+ }
+
+ public void removeUser(String username) throws Exception {
+ if (userConfig.getProperty(username) == null) {
+ throw new IllegalArgumentException("user " + username + " doesn't exist.");
+ }
+ userConfig.clearProperty(username);
+ removeRoles(username);
+ }
+
+ public List listUser(String username) {
+ List result = new ArrayList<>();
+ result.add("--- \"user\"(roles) ---\n");
+
+ int totalUsers = 0;
+ if (username != null) {
+ String roles = findRoles(username);
+ result.add("\"" + username + "\"(" + roles + ")");
+ totalUsers++;
+ } else {
+ Iterator iter = userConfig.getKeys();
+ while (iter.hasNext()) {
+ String keyUser = iter.next();
+ String roles = findRoles(keyUser);
+ result.add("\"" + keyUser + "\"(" + roles + ")");
+ totalUsers++;
+ }
+ }
+ result.add("\n Total: " + totalUsers);
+ return result;
+ }
+
+ private String findRoles(String uname) {
+ Iterator iter = roleConfig.getKeys();
+ StringBuilder builder = new StringBuilder();
+ boolean first = true;
+ while (iter.hasNext()) {
+ String role = iter.next();
+ List names = roleConfig.getList(String.class, role);
+ for (String value : names) {
+ //each value may be a comma separated list
+ String[] items = value.split(",");
+ for (String item : items) {
+ if (item.equals(uname)) {
+ if (!first) {
+ builder.append(",");
+ }
+ builder.append(role);
+ first = false;
+ }
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+
+ public void updateUser(String username, String password, String[] roles) {
+ String oldPassword = (String) userConfig.getProperty(username);
+ if (oldPassword == null) {
+ throw new IllegalArgumentException("user " + username + " doesn't exist.");
+ }
+
+ if (password != null) {
+ userConfig.setProperty(username, password);
+ }
+
+ if (roles != null && roles.length > 0) {
+
+ removeRoles(username);
+ addRoles(username, roles);
+ }
+ }
+
+ private void addRoles(String username, String[] roles) {
+ for (String role : roles) {
+ List users = roleConfig.getList(String.class, role);
+ if (users == null) {
+ users = new ArrayList<>();
+ }
+ users.add(username);
+ roleConfig.setProperty(role, StringUtil.joinStringList(users, ","));
+ }
+ }
+
+ private void removeRoles(String username) {
+
+ Iterator iterKeys = roleConfig.getKeys();
+
+ List>> updateMap = new ArrayList<>();
+ while (iterKeys.hasNext()) {
+ String theRole = iterKeys.next();
+
+ List userList = roleConfig.getList(String.class, theRole);
+ List newList = new ArrayList<>();
+
+ boolean roleChaned = false;
+ for (String value : userList) {
+ //each value may be comma separated.
+ List update = new ArrayList<>();
+ String[] items = value.split(",");
+ boolean found = false;
+ for (String item : items) {
+ if (!item.equals(username)) {
+ update.add(item);
+ } else {
+ found = true;
+ roleChaned = true;
+ }
+ }
+ if (found) {
+ if (update.size() > 0) {
+ newList.add(StringUtil.joinStringList(update, ","));
+ }
+ }
+ }
+ if (roleChaned) {
+ updateMap.add(new Pair(theRole, newList));
+ }
+ }
+ //do update
+ Iterator>> iterUpdate = updateMap.iterator();
+ while (iterUpdate.hasNext()) {
+ Pair> entry = iterUpdate.next();
+ roleConfig.clearProperty(entry.getA());
+ if (entry.getB().size() > 0) {
+ roleConfig.addProperty(entry.getA(), entry.getB());
+ }
+ }
+ }
+}
diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles.properties b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles.properties
index c9443dd9db..74f4266ef4 100644
--- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles.properties
+++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles.properties
@@ -14,4 +14,5 @@
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
-${role}=${user}
\ No newline at end of file
+
+${role} = ${user}
\ No newline at end of file
diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-users.properties b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-users.properties
index 81462f844d..b437025307 100644
--- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-users.properties
+++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-users.properties
@@ -14,4 +14,5 @@
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
-${user}=${password}
\ No newline at end of file
+
+${user} = ${password}
\ No newline at end of file
diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java
index 2359f1d324..3d89aa8e9d 100644
--- a/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java
+++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java
@@ -26,6 +26,7 @@ import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.api.core.SimpleString;
@@ -35,16 +36,29 @@ import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.cli.Artemis;
import org.apache.activemq.artemis.cli.CLIException;
+import org.apache.activemq.artemis.cli.commands.ActionContext;
import org.apache.activemq.artemis.cli.commands.Create;
+import org.apache.activemq.artemis.cli.commands.Mask;
import org.apache.activemq.artemis.cli.commands.Run;
import org.apache.activemq.artemis.cli.commands.tools.LockAbstract;
+import org.apache.activemq.artemis.cli.commands.user.AddUser;
+import org.apache.activemq.artemis.cli.commands.user.ListUser;
+import org.apache.activemq.artemis.cli.commands.user.RemoveUser;
+import org.apache.activemq.artemis.cli.commands.user.ResetUser;
import org.apache.activemq.artemis.cli.commands.util.SyncCalculation;
import org.apache.activemq.artemis.core.client.impl.ServerLocatorImpl;
import org.apache.activemq.artemis.jlibaio.LibaioContext;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.apache.activemq.artemis.jms.client.ActiveMQDestination;
import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoader;
+import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;
import org.apache.activemq.artemis.utils.ThreadLeakCheckRule;
+import org.apache.activemq.artemis.utils.HashProcessor;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+import org.apache.activemq.artemis.utils.StringUtil;
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
+import org.apache.commons.configuration2.builder.fluent.Configurations;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -55,6 +69,11 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
/**
* Test to validate that the CLI doesn't throw improper exceptions when invoked.
*/
@@ -134,7 +153,7 @@ public class ArtemisTest {
System.out.println("TotalAvg = " + totalAvg);
long nanoTime = SyncCalculation.toNanos(totalAvg, writes);
System.out.println("nanoTime avg = " + nanoTime);
- Assert.assertEquals(0, LibaioContext.getTotalMaxIO());
+ assertEquals(0, LibaioContext.getTotalMaxIO());
}
@@ -146,47 +165,47 @@ public class ArtemisTest {
File instance1 = new File(temporaryFolder.getRoot(), "instance1");
Artemis.main("create", instance1.getAbsolutePath(), "--silent", "--no-fsync");
File bootstrapFile = new File(new File(instance1, "etc"), "bootstrap.xml");
- Assert.assertTrue(bootstrapFile.exists());
+ assertTrue(bootstrapFile.exists());
Document config = parseXml(bootstrapFile);
Element webElem = (Element) config.getElementsByTagName("web").item(0);
String bindAttr = webElem.getAttribute("bind");
String bindStr = "http://localhost:" + Create.HTTP_PORT;
- Assert.assertEquals(bindAttr, bindStr);
+ assertEquals(bindAttr, bindStr);
//no any of those
- Assert.assertFalse(webElem.hasAttribute("keyStorePath"));
- Assert.assertFalse(webElem.hasAttribute("keyStorePassword"));
- Assert.assertFalse(webElem.hasAttribute("clientAuth"));
- Assert.assertFalse(webElem.hasAttribute("trustStorePath"));
- Assert.assertFalse(webElem.hasAttribute("trustStorePassword"));
+ assertFalse(webElem.hasAttribute("keyStorePath"));
+ assertFalse(webElem.hasAttribute("keyStorePassword"));
+ assertFalse(webElem.hasAttribute("clientAuth"));
+ assertFalse(webElem.hasAttribute("trustStorePath"));
+ assertFalse(webElem.hasAttribute("trustStorePassword"));
//instance2: https
File instance2 = new File(temporaryFolder.getRoot(), "instance2");
Artemis.main("create", instance2.getAbsolutePath(), "--silent", "--ssl-key", "etc/keystore", "--ssl-key-password", "password1", "--no-fsync");
bootstrapFile = new File(new File(instance2, "etc"), "bootstrap.xml");
- Assert.assertTrue(bootstrapFile.exists());
+ assertTrue(bootstrapFile.exists());
config = parseXml(bootstrapFile);
webElem = (Element) config.getElementsByTagName("web").item(0);
bindAttr = webElem.getAttribute("bind");
bindStr = "https://localhost:" + Create.HTTP_PORT;
- Assert.assertEquals(bindAttr, bindStr);
+ assertEquals(bindAttr, bindStr);
String keyStr = webElem.getAttribute("keyStorePath");
- Assert.assertEquals("etc/keystore", keyStr);
+ assertEquals("etc/keystore", keyStr);
String keyPass = webElem.getAttribute("keyStorePassword");
- Assert.assertEquals("password1", keyPass);
+ assertEquals("password1", keyPass);
- Assert.assertFalse(webElem.hasAttribute("clientAuth"));
- Assert.assertFalse(webElem.hasAttribute("trustStorePath"));
- Assert.assertFalse(webElem.hasAttribute("trustStorePassword"));
+ assertFalse(webElem.hasAttribute("clientAuth"));
+ assertFalse(webElem.hasAttribute("trustStorePath"));
+ assertFalse(webElem.hasAttribute("trustStorePassword"));
//instance3: https with clientAuth
File instance3 = new File(temporaryFolder.getRoot(), "instance3");
Artemis.main("create", instance3.getAbsolutePath(), "--silent", "--ssl-key", "etc/keystore", "--ssl-key-password", "password1", "--use-client-auth", "--ssl-trust", "etc/truststore", "--ssl-trust-password", "password2", "--no-fsync");
bootstrapFile = new File(new File(instance3, "etc"), "bootstrap.xml");
- Assert.assertTrue(bootstrapFile.exists());
+ assertTrue(bootstrapFile.exists());
byte[] contents = Files.readAllBytes(bootstrapFile.toPath());
String cfgText = new String(contents);
@@ -197,19 +216,298 @@ public class ArtemisTest {
bindAttr = webElem.getAttribute("bind");
bindStr = "https://localhost:" + Create.HTTP_PORT;
- Assert.assertEquals(bindAttr, bindStr);
+ assertEquals(bindAttr, bindStr);
keyStr = webElem.getAttribute("keyStorePath");
- Assert.assertEquals("etc/keystore", keyStr);
+ assertEquals("etc/keystore", keyStr);
keyPass = webElem.getAttribute("keyStorePassword");
- Assert.assertEquals("password1", keyPass);
+ assertEquals("password1", keyPass);
String clientAuthAttr = webElem.getAttribute("clientAuth");
- Assert.assertEquals("true", clientAuthAttr);
+ assertEquals("true", clientAuthAttr);
String trustPathAttr = webElem.getAttribute("trustStorePath");
- Assert.assertEquals("etc/truststore", trustPathAttr);
+ assertEquals("etc/truststore", trustPathAttr);
String trustPass = webElem.getAttribute("trustStorePassword");
- Assert.assertEquals("password2", trustPass);
+ assertEquals("password2", trustPass);
+ }
+
+ @Test
+ public void testUserCommand() throws Exception {
+ Run.setEmbedded(true);
+ File instance1 = new File(temporaryFolder.getRoot(), "instance_user");
+ System.setProperty("java.security.auth.login.config", instance1.getAbsolutePath() + "/etc/login.config");
+ Artemis.main("create", instance1.getAbsolutePath(), "--silent");
+ System.setProperty("artemis.instance", instance1.getAbsolutePath());
+
+ File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties");
+ File roleFile = new File(instance1.getAbsolutePath() + "/etc/artemis-roles.properties");
+
+ ListUser listCmd = new ListUser();
+ TestActionContext context = new TestActionContext();
+ listCmd.execute(context);
+
+ String result = context.getStdout();
+ System.out.println("output1:\n" + result);
+
+ //default only one user admin with role amq
+ assertTrue(result.contains("\"admin\"(amq)"));
+ checkRole("admin", roleFile, "amq");
+
+ //add a simple user
+ AddUser addCmd = new AddUser();
+ addCmd.setUsername("guest");
+ addCmd.setPassword("guest123");
+ addCmd.setRole("admin");
+ addCmd.execute(new TestActionContext());
+
+ //verify use list cmd
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output2:\n" + result);
+
+ assertTrue(result.contains("\"admin\"(amq)"));
+ assertTrue(result.contains("\"guest\"(admin)"));
+
+ checkRole("guest", roleFile, "admin");
+ assertTrue(checkPassword("guest", "guest123", userFile));
+
+ //add a user with 2 roles
+ addCmd = new AddUser();
+ addCmd.setUsername("scott");
+ addCmd.setPassword("tiger");
+ addCmd.setRole("admin,operator");
+ addCmd.execute(ActionContext.system());
+
+ //verify
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output3:\n" + result);
+
+ assertTrue(result.contains("\"admin\"(amq)"));
+ assertTrue(result.contains("\"guest\"(admin)"));
+ assertTrue(result.contains("\"scott\"(admin,operator)"));
+
+ checkRole("scott", roleFile, "admin", "operator");
+ assertTrue(checkPassword("scott", "tiger", userFile));
+
+ //add an existing user
+ addCmd = new AddUser();
+ addCmd.setUsername("scott");
+ addCmd.setPassword("password");
+ addCmd.setRole("visitor");
+ try {
+ addCmd.execute(ActionContext.system());
+ fail("should throw an exception if adding a existing user");
+ } catch (IllegalArgumentException expected) {
+ }
+
+ //check existing users are intact
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output4:\n" + result);
+
+ assertTrue(result.contains("\"admin\"(amq)"));
+ assertTrue(result.contains("\"guest\"(admin)"));
+ assertTrue(result.contains("\"scott\"(admin,operator)"));
+
+ //remove a user
+ RemoveUser rmCmd = new RemoveUser();
+ rmCmd.setUsername("guest");
+ rmCmd.execute(ActionContext.system());
+
+ //check
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output5:\n" + result);
+
+ assertTrue(result.contains("\"admin\"(amq)"));
+ assertFalse(result.contains("\"guest\"(admin)"));
+ assertTrue(result.contains("\"scott\"(admin,operator)") || result.contains("\"scott\"(operator,admin)"));
+ assertTrue(result.contains("Total: 2"));
+
+ //remove another
+ rmCmd = new RemoveUser();
+ rmCmd.setUsername("scott");
+ rmCmd.execute(ActionContext.system());
+
+ //check
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output6:\n" + result);
+
+ assertTrue(result.contains("\"admin\"(amq)"));
+ assertFalse(result.contains("\"guest\"(admin)"));
+ assertFalse(result.contains("\"scott\"(admin,operator)") || result.contains("\"scott\"(operator,admin)"));
+ assertTrue(result.contains("Total: 1"));
+
+ //remove non-exist
+ rmCmd = new RemoveUser();
+ rmCmd.setUsername("alien");
+ try {
+ rmCmd.execute(ActionContext.system());
+ fail("should throw exception when removing a non-existing user");
+ } catch (IllegalArgumentException expected) {
+ }
+
+ //check
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output7:\n" + result);
+ assertTrue(result.contains("\"admin\"(amq)"));
+ assertTrue(result.contains("Total: 1"));
+
+ //now remove last
+ rmCmd = new RemoveUser();
+ rmCmd.setUsername("admin");
+ rmCmd.execute(ActionContext.system());
+
+ //check
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output8:\n" + result);
+
+ assertTrue(result.contains("Total: 0"));
+ }
+
+ @Test
+ public void testUserCommandReset() throws Exception {
+ Run.setEmbedded(true);
+ File instance1 = new File(temporaryFolder.getRoot(), "instance_user");
+ System.setProperty("java.security.auth.login.config", instance1.getAbsolutePath() + "/etc/login.config");
+ Artemis.main("create", instance1.getAbsolutePath(), "--silent");
+ System.setProperty("artemis.instance", instance1.getAbsolutePath());
+
+ File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties");
+ File roleFile = new File(instance1.getAbsolutePath() + "/etc/artemis-roles.properties");
+
+ ListUser listCmd = new ListUser();
+ TestActionContext context = new TestActionContext();
+ listCmd.execute(context);
+
+ String result = context.getStdout();
+ System.out.println("output1:\n" + result);
+
+ //default only one user admin with role amq
+ assertTrue(result.contains("\"admin\"(amq)"));
+
+ //remove a user
+ RemoveUser rmCmd = new RemoveUser();
+ rmCmd.setUsername("admin");
+ rmCmd.execute(ActionContext.system());
+
+ //check
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output8:\n" + result);
+
+ assertTrue(result.contains("Total: 0"));
+
+ //add some users
+ AddUser addCmd = new AddUser();
+ addCmd.setUsername("guest");
+ addCmd.setPassword("guest123");
+ addCmd.setRole("admin");
+ addCmd.execute(new TestActionContext());
+
+ addCmd.setUsername("user1");
+ addCmd.setPassword("password1");
+ addCmd.setRole("admin,manager");
+ addCmd.execute(new TestActionContext());
+ assertTrue(checkPassword("user1", "password1", userFile));
+
+ addCmd.setUsername("user2");
+ addCmd.setPassword("password2");
+ addCmd.setRole("admin,manager,master");
+ addCmd.execute(new TestActionContext());
+
+ addCmd.setUsername("user3");
+ addCmd.setPassword("password3");
+ addCmd.setRole("system,master");
+ addCmd.execute(new TestActionContext());
+
+ //verify use list cmd
+ context = new TestActionContext();
+ listCmd.execute(context);
+ result = context.getStdout();
+ System.out.println("output2:\n" + result);
+
+ assertTrue(result.contains("Total: 4"));
+ assertTrue(result.contains("\"guest\"(admin)"));
+ assertTrue(result.contains("\"user1\"(admin,manager)"));
+ assertTrue(result.contains("\"user2\"(admin,manager,master)"));
+ assertTrue(result.contains("\"user3\"(master,system)"));
+
+ checkRole("user1", roleFile, "admin", "manager");
+
+ //reset password
+ context = new TestActionContext();
+ ResetUser resetCommand = new ResetUser();
+ resetCommand.setUsername("user1");
+ resetCommand.setPassword("newpassword1");
+ resetCommand.execute(context);
+
+ checkRole("user1", roleFile, "admin", "manager");
+ assertFalse(checkPassword("user1", "password1", userFile));
+ assertTrue(checkPassword("user1", "newpassword1", userFile));
+
+ //reset role
+ resetCommand.setUsername("user2");
+ resetCommand.setRole("manager,master,operator");
+ resetCommand.execute(new TestActionContext());
+
+ checkRole("user2", roleFile, "manager", "master", "operator");
+
+ //reset both
+ resetCommand.setUsername("user3");
+ resetCommand.setPassword("newpassword3");
+ resetCommand.setRole("admin,system");
+ resetCommand.execute(new ActionContext());
+
+ checkRole("user3", roleFile, "admin", "system");
+ assertTrue(checkPassword("user3", "newpassword3", userFile));
+ }
+
+ @Test
+ public void testMaskCommand() throws Exception {
+
+ String password1 = "password";
+ String encrypt1 = "3a34fd21b82bf2a822fa49a8d8fa115d";
+ String newKey = "artemisfun";
+ String encrypt2 = "-2b8e3b47950b9b481a6f3100968e42e9";
+
+
+ TestActionContext context = new TestActionContext();
+ Mask mask = new Mask();
+ mask.setPassword(password1);
+
+ String result = (String) mask.execute(context);
+ System.out.println(context.getStdout());
+ assertEquals(encrypt1, result);
+
+ context = new TestActionContext();
+ mask = new Mask();
+ mask.setPassword(password1);
+ mask.setHash(true);
+ result = (String) mask.execute(context);
+ System.out.println(context.getStdout());
+ DefaultSensitiveStringCodec codec = mask.getCodec();
+ codec.verify(password1.toCharArray(), result);
+
+ context = new TestActionContext();
+ mask = new Mask();
+ mask.setPassword(password1);
+ mask.setKey(newKey);
+ result = (String) mask.execute(context);
+ System.out.println(context.getStdout());
+ assertEquals(encrypt2, result);
}
@Test
@@ -251,11 +549,11 @@ public class ArtemisTest {
ClientSession coreSession = factory.createSession("admin", "admin", false, true, true, false, 0)) {
for (String str : queues.split(",")) {
ClientSession.QueueQuery queryResult = coreSession.queueQuery(SimpleString.toSimpleString("jms.queue." + str));
- Assert.assertTrue("Couldn't find queue " + str, queryResult.isExists());
+ assertTrue("Couldn't find queue " + str, queryResult.isExists());
}
for (String str : topics.split(",")) {
ClientSession.QueueQuery queryResult = coreSession.queueQuery(SimpleString.toSimpleString("jms.topic." + str));
- Assert.assertTrue("Couldn't find topic " + str, queryResult.isExists());
+ assertTrue("Couldn't find topic " + str, queryResult.isExists());
}
}
@@ -266,8 +564,8 @@ public class ArtemisTest {
}
Artemis.internalExecute("data", "print", "--f");
- Assert.assertEquals(Integer.valueOf(100), Artemis.internalExecute("producer", "--message-count", "100", "--verbose", "--user", "admin", "--password", "admin"));
- Assert.assertEquals(Integer.valueOf(100), Artemis.internalExecute("consumer", "--verbose", "--break-on-null", "--receive-timeout", "100", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(100), Artemis.internalExecute("producer", "--message-count", "100", "--verbose", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(100), Artemis.internalExecute("consumer", "--verbose", "--break-on-null", "--receive-timeout", "100", "--user", "admin", "--password", "admin"));
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = cf.createConnection("admin", "admin");
@@ -288,20 +586,20 @@ public class ArtemisTest {
connection.close();
cf.close();
- Assert.assertEquals(Integer.valueOf(1), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--filter", "fruit='banana'", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(1), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--filter", "fruit='banana'", "--user", "admin", "--password", "admin"));
- Assert.assertEquals(Integer.valueOf(100), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--filter", "fruit='orange'", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(100), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--filter", "fruit='orange'", "--user", "admin", "--password", "admin"));
- Assert.assertEquals(Integer.valueOf(101), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(101), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--user", "admin", "--password", "admin"));
// should only receive 10 messages on browse as I'm setting messageCount=10
- Assert.assertEquals(Integer.valueOf(10), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--message-count", "10", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(10), Artemis.internalExecute("browser", "--txt-size", "50", "--verbose", "--message-count", "10", "--user", "admin", "--password", "admin"));
// Nothing was consumed until here as it was only browsing, check it's receiving again
- Assert.assertEquals(Integer.valueOf(1), Artemis.internalExecute("consumer", "--txt-size", "50", "--verbose", "--break-on-null", "--receive-timeout", "100", "--filter", "fruit='banana'", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(1), Artemis.internalExecute("consumer", "--txt-size", "50", "--verbose", "--break-on-null", "--receive-timeout", "100", "--filter", "fruit='banana'", "--user", "admin", "--password", "admin"));
// Checking it was acked before
- Assert.assertEquals(Integer.valueOf(100), Artemis.internalExecute("consumer", "--txt-size", "50", "--verbose", "--break-on-null", "--receive-timeout", "100", "--user", "admin", "--password", "admin"));
+ assertEquals(Integer.valueOf(100), Artemis.internalExecute("consumer", "--txt-size", "50", "--verbose", "--break-on-null", "--receive-timeout", "100", "--user", "admin", "--password", "admin"));
} finally {
stopServer();
}
@@ -312,7 +610,7 @@ public class ArtemisTest {
Artemis.main(args);
} catch (Exception e) {
e.printStackTrace();
- Assert.fail("Exception caught " + e.getMessage());
+ fail("Exception caught " + e.getMessage());
}
}
@@ -322,8 +620,8 @@ public class ArtemisTest {
private void stopServer() throws Exception {
Artemis.internalExecute("stop");
- Assert.assertTrue(Run.latchRunning.await(5, TimeUnit.SECONDS));
- Assert.assertEquals(0, LibaioContext.getTotalMaxIO());
+ assertTrue(Run.latchRunning.await(5, TimeUnit.SECONDS));
+ assertEquals(0, LibaioContext.getTotalMaxIO());
}
private static Document parseXml(File xmlFile) throws ParserConfigurationException, IOException, SAXException {
@@ -332,4 +630,27 @@ public class ArtemisTest {
return domBuilder.parse(xmlFile);
}
+ private void checkRole(String user, File roleFile, String... roles) throws Exception {
+ Configurations configs = new Configurations();
+ FileBasedConfigurationBuilder roleBuilder = configs.propertiesBuilder(roleFile);
+ PropertiesConfiguration roleConfig = roleBuilder.getConfiguration();
+
+ for (String r : roles) {
+ String storedUsers = (String) roleConfig.getProperty(r);
+
+ System.out.println("users in role: " + r + " ; " + storedUsers);
+ List userList = StringUtil.splitStringList(storedUsers, ",");
+ assertTrue(userList.contains(user));
+ }
+ }
+
+ private boolean checkPassword(String user, String password, File userFile) throws Exception {
+ Configurations configs = new Configurations();
+ FileBasedConfigurationBuilder userBuilder = configs.propertiesBuilder(userFile);
+ PropertiesConfiguration userConfig = userBuilder.getConfiguration();
+ String storedPassword = (String) userConfig.getProperty(user);
+ HashProcessor processor = PasswordMaskingUtil.getHashProcessor(storedPassword);
+ return processor.compare(password.toCharArray(), storedPassword);
+ }
+
}
diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/TestActionContext.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/TestActionContext.java
new file mode 100644
index 0000000000..0a2da116e4
--- /dev/null
+++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/TestActionContext.java
@@ -0,0 +1,50 @@
+/*
+ * 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.ActionContext;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+public class TestActionContext extends ActionContext {
+
+ public ByteArrayOutputStream stdout;
+ public ByteArrayOutputStream stderr;
+ private int bufferSize;
+
+ public TestActionContext(int bufferSize) {
+ this.bufferSize = bufferSize;
+ this.stdout = new ByteArrayOutputStream(bufferSize);
+ this.stderr = new ByteArrayOutputStream(bufferSize);
+ this.in = System.in;
+ this.out = new PrintStream(stdout);
+ this.err = new PrintStream(stderr);
+ }
+
+ public TestActionContext() {
+ this(4096);
+ }
+
+ public String getStdout() {
+ return stdout.toString();
+ }
+
+ public String getStderr() {
+ return stderr.toString();
+ }
+}
diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ByteUtil.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ByteUtil.java
index c9143f52be..c22f37ed2e 100644
--- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ByteUtil.java
+++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ByteUtil.java
@@ -134,6 +134,14 @@ public class ByteUtil {
return buffer.array();
}
+ public static byte[] hexToBytes(String hexStr) {
+ byte[] bytes = new byte[hexStr.length() / 2];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) Integer.parseInt(hexStr.substring(2 * i, 2 * i + 2), 16);
+ }
+ return bytes;
+ }
+
public static String readLine(ActiveMQBuffer buffer) {
StringBuilder sb = new StringBuilder("");
char c = buffer.readChar();
diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java
index 02bec5f1ca..227a60b122 100644
--- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java
+++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java
@@ -16,15 +16,19 @@
*/
package org.apache.activemq.artemis.utils;
-import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
-import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
+import java.util.Properties;
/**
* A DefaultSensitiveDataCodec
@@ -34,52 +38,39 @@ import java.util.Map;
* file to use a masked password but doesn't give a
* codec implementation.
*
- * The decode() and encode() method is copied originally from
- * JBoss AS code base.
+ * It supports one-way hash (digest) and two-way (encrypt-decrpt) algorithms
+ * The two-way uses "Blowfish" algorithm
+ * The one-way uses "PBKDF2" hash algorithm
*/
public class DefaultSensitiveStringCodec implements SensitiveDataCodec {
- private byte[] internalKey = "clusterpassword".getBytes();
+ public static final String ALGORITHM = "algorithm";
+ public static final String BLOWFISH_KEY = "key";
+ public static final String ONE_WAY = "one-way";
+ public static final String TWO_WAY = "two-way";
+
+ private CodecAlgorithm algorithm = new BlowfishAlgorithm(Collections.EMPTY_MAP);
@Override
- public String decode(Object secret) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
- SecretKeySpec key = new SecretKeySpec(internalKey, "Blowfish");
-
- BigInteger n = new BigInteger((String) secret, 16);
- byte[] encoding = n.toByteArray();
-
- // JBAS-3457: fix leading zeros
- if (encoding.length % 8 != 0) {
- int length = encoding.length;
- int newLength = ((length / 8) + 1) * 8;
- int pad = newLength - length; // number of leading zeros
- byte[] old = encoding;
- encoding = new byte[newLength];
- System.arraycopy(old, 0, encoding, pad, old.length);
- }
-
- Cipher cipher = Cipher.getInstance("Blowfish");
- cipher.init(Cipher.DECRYPT_MODE, key);
- byte[] decode = cipher.doFinal(encoding);
-
- return new String(decode);
- }
-
- public Object encode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
- SecretKeySpec key = new SecretKeySpec(internalKey, "Blowfish");
-
- Cipher cipher = Cipher.getInstance("Blowfish");
- cipher.init(Cipher.ENCRYPT_MODE, key);
- byte[] encoding = cipher.doFinal(secret.getBytes());
- BigInteger n = new BigInteger(encoding);
- return n.toString(16);
+ public String decode(Object secret) throws Exception {
+ return algorithm.decode((String) secret);
}
@Override
- public void init(Map params) {
- String key = params.get("key");
- if (key != null) {
- updateKey(key);
+ public String encode(Object secret) throws Exception {
+ return algorithm.encode((String) secret);
+ }
+
+ @Override
+ public void init(Map params) throws Exception {
+ String algorithm = params.get(ALGORITHM);
+ if (algorithm == null || algorithm.equals(TWO_WAY)) {
+ //two way
+ this.algorithm = new BlowfishAlgorithm(params);
+ } else if (algorithm.equals(ONE_WAY)) {
+ this.algorithm = new PBKDF2Algorithm(params);
+ } else {
+ throw new IllegalArgumentException("Invalid algorithm: " + algorithm);
}
}
@@ -96,12 +87,149 @@ public class DefaultSensitiveStringCodec implements SensitiveDataCodec {
System.exit(-1);
}
DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
+ Map params = new HashMap<>();
+ Properties properties = System.getProperties();
+ for (final String name: properties.stringPropertyNames()) {
+ params.put(name, properties.getProperty(name));
+ }
+ codec.init(params);
Object encode = codec.encode(args[0]);
+
System.out.println("Encoded password (without quotes): \"" + encode + "\"");
}
- private void updateKey(String key) {
- this.internalKey = key.getBytes();
+ public boolean verify(char[] inputValue, String storedValue) {
+ return algorithm.verify(inputValue, storedValue);
}
+ private abstract class CodecAlgorithm {
+
+ protected Map params;
+
+ CodecAlgorithm(Map params) {
+ this.params = params;
+ }
+
+ public abstract String decode(String secret) throws Exception;
+ public abstract String encode(String secret) throws Exception;
+
+ public boolean verify(char[] inputValue, String storedValue) {
+ return false;
+ }
+ }
+
+ private class BlowfishAlgorithm extends CodecAlgorithm {
+
+ private byte[] internalKey = "clusterpassword".getBytes();
+
+ BlowfishAlgorithm(Map params) {
+ super(params);
+ String key = params.get(BLOWFISH_KEY);
+ if (key != null) {
+ updateKey(key);
+ }
+ }
+
+ private void updateKey(String key) {
+ this.internalKey = key.getBytes();
+ }
+
+ @Override
+ public String decode(String secret) throws Exception {
+ SecretKeySpec key = new SecretKeySpec(internalKey, "Blowfish");
+
+ BigInteger n = new BigInteger((String) secret, 16);
+ byte[] encoding = n.toByteArray();
+
+ if (encoding.length % 8 != 0) {
+ int length = encoding.length;
+ int newLength = ((length / 8) + 1) * 8;
+ int pad = newLength - length; // number of leading zeros
+ byte[] old = encoding;
+ encoding = new byte[newLength];
+ System.arraycopy(old, 0, encoding, pad, old.length);
+ }
+
+ Cipher cipher = Cipher.getInstance("Blowfish");
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ byte[] decode = cipher.doFinal(encoding);
+
+ return new String(decode);
+ }
+
+ @Override
+ public String encode(String secret) throws Exception {
+ SecretKeySpec key = new SecretKeySpec(internalKey, "Blowfish");
+
+ Cipher cipher = Cipher.getInstance("Blowfish");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] encoding = cipher.doFinal(secret.getBytes());
+ BigInteger n = new BigInteger(encoding);
+ return n.toString(16);
+ }
+ }
+
+ private class PBKDF2Algorithm extends CodecAlgorithm {
+ private static final String SEPERATOR = ":";
+ private String sceretKeyAlgorithm = "PBKDF2WithHmacSHA1";
+ private String randomScheme = "SHA1PRNG";
+ private int keyLength = 64 * 8;
+ private int saltLength = 32;
+ private int iterations = 1024;
+ private SecretKeyFactory skf;
+
+ PBKDF2Algorithm(Map params) throws NoSuchAlgorithmException {
+ super(params);
+ skf = SecretKeyFactory.getInstance(sceretKeyAlgorithm);
+ }
+
+ @Override
+ public String decode(String secret) throws Exception {
+ throw new IllegalArgumentException("Algorithm doesn't support decoding");
+ }
+
+ public byte[] getSalt() throws NoSuchAlgorithmException {
+ byte[] salt = new byte[this.saltLength];
+
+ SecureRandom sr = SecureRandom.getInstance(this.randomScheme);
+ sr.nextBytes(salt);
+ return salt;
+ }
+
+ @Override
+ public String encode(String secret) throws Exception {
+ char[] chars = secret.toCharArray();
+ byte[] salt = getSalt();
+
+ StringBuilder builder = new StringBuilder();
+ builder.append(iterations).append(SEPERATOR).append(ByteUtil.bytesToHex(salt)).append(SEPERATOR);
+
+ PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, keyLength);
+
+ byte[] hash = skf.generateSecret(spec).getEncoded();
+ String hexValue = ByteUtil.bytesToHex(hash);
+ builder.append(hexValue);
+
+ return builder.toString();
+ }
+
+ @Override
+ public boolean verify(char[] plainChars, String storedValue) {
+ String[] parts = storedValue.split(SEPERATOR);
+ int originalIterations = Integer.parseInt(parts[0]);
+ byte[] salt = ByteUtil.hexToBytes(parts[1]);
+ byte[] originalHash = ByteUtil.hexToBytes(parts[2]);
+
+ PBEKeySpec spec = new PBEKeySpec(plainChars, salt, originalIterations, originalHash.length * 8);
+ byte[] newHash;
+
+ try {
+ newHash = skf.generateSecret(spec).getEncoded();
+ } catch (InvalidKeySpecException e) {
+ return false;
+ }
+
+ return Arrays.equals(newHash, originalHash);
+ }
+ }
}
diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/HashProcessor.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/HashProcessor.java
new file mode 100644
index 0000000000..0b44132296
--- /dev/null
+++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/HashProcessor.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+
+/**
+ * Used to process Hash text for passwords
+ */
+public interface HashProcessor {
+
+ /**
+ * produce hash text from plain text
+ * @param plainText Plain text input
+ * @return the Hash value of the input plain text
+ * @throws Exception
+ */
+ String hash(String plainText) throws Exception;
+
+ /**
+ * compare the plain char array against the hash value
+ * @param inputValue value of the plain text
+ * @param storedHash the existing hash value
+ * @return true if the char array matches the hash value,
+ * otherwise false.
+ */
+ boolean compare(char[] inputValue, String storedHash);
+}
diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/NoHashProcessor.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/NoHashProcessor.java
new file mode 100644
index 0000000000..67e7f6a208
--- /dev/null
+++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/NoHashProcessor.java
@@ -0,0 +1,35 @@
+/*
+ * 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 java.util.Arrays;
+
+/**
+ * A hash processor that just does plain text comparison
+ */
+public class NoHashProcessor implements HashProcessor {
+
+ @Override
+ public String hash(String plainText) throws Exception {
+ return plainText;
+ }
+
+ @Override
+ public boolean compare(char[] inputValue, String storedHash) {
+ return Arrays.equals(inputValue, storedHash.toCharArray());
+ }
+}
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 2ef0daabae..bee3861f21 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
@@ -27,6 +27,64 @@ import org.apache.activemq.artemis.logs.ActiveMQUtilBundle;
public class PasswordMaskingUtil {
+ private static final String PLAINTEXT_PROCESSOR = "plaintext";
+ private static final String SECURE_PROCESSOR = "secure";
+
+ private static final Map processors = new HashMap<>();
+
+ //stored password takes 2 forms, ENC() or plain text
+ public static HashProcessor getHashProcessor(String storedPassword) throws Exception {
+
+ if (!isEncoded(storedPassword)) {
+ return getPlaintextProcessor();
+ }
+ return getSecureProcessor();
+ }
+
+ private static boolean isEncoded(String storedPassword) {
+ if (storedPassword == null) {
+ return true;
+ }
+
+ if (storedPassword.startsWith("ENC(") && storedPassword.endsWith(")")) {
+ return true;
+ }
+ return false;
+ }
+
+ public static HashProcessor getHashProcessor() {
+ HashProcessor processor = null;
+ try {
+ processor = getSecureProcessor();
+ } catch (Exception e) {
+ processor = getPlaintextProcessor();
+ }
+ return processor;
+ }
+
+ public static HashProcessor getPlaintextProcessor() {
+ synchronized (processors) {
+ HashProcessor plain = processors.get(PLAINTEXT_PROCESSOR);
+ if (plain == null) {
+ plain = new NoHashProcessor();
+ processors.put(PLAINTEXT_PROCESSOR, plain);
+ }
+ return plain;
+ }
+ }
+
+ public static HashProcessor getSecureProcessor() throws Exception {
+ synchronized (processors) {
+ HashProcessor processor = processors.get(SECURE_PROCESSOR);
+ if (processor == null) {
+ DefaultSensitiveStringCodec codec = (DefaultSensitiveStringCodec) getCodec("org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;algorithm=one-way");
+ processor = new SecureHashProcessor(codec);
+ processors.put(SECURE_PROCESSOR, processor);
+ }
+ return processor;
+ }
+ }
+
/*
* Loading the codec class.
*
@@ -37,7 +95,7 @@ public class PasswordMaskingUtil {
* Where only is required. key/value pairs are optional
*/
public static SensitiveDataCodec getCodec(String codecDesc) throws ActiveMQException {
- SensitiveDataCodec codecInstance = null;
+ SensitiveDataCodec codecInstance;
// semi colons
String[] parts = codecDesc.split(";");
@@ -70,13 +128,19 @@ public class PasswordMaskingUtil {
throw ActiveMQUtilBundle.BUNDLE.invalidProperty(parts[i]);
props.put(keyVal[0], keyVal[1]);
}
- codecInstance.init(props);
+ try {
+ codecInstance.init(props);
+ } catch (Exception e) {
+ throw new ActiveMQException("Fail to init codec", e, ActiveMQExceptionType.SECURITY_EXCEPTION);
+ }
}
return codecInstance;
}
- public static SensitiveDataCodec getDefaultCodec() {
+ public static DefaultSensitiveStringCodec getDefaultCodec() {
return new DefaultSensitiveStringCodec();
}
+
+
}
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
new file mode 100644
index 0000000000..81db051471
--- /dev/null
+++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * Hash function
+ */
+public class SecureHashProcessor implements HashProcessor {
+
+ private static final String BEGIN_HASH = "ENC(";
+ private static final String END_HASH = ")";
+
+ private DefaultSensitiveStringCodec codec;
+
+ public SecureHashProcessor(DefaultSensitiveStringCodec codec) {
+ this.codec = codec;
+ }
+
+ @Override
+ public String hash(String plainText) throws Exception {
+ return BEGIN_HASH + codec.encode(plainText) + END_HASH;
+ }
+
+ @Override
+ //storedValue must take form of ENC(...)
+ public boolean compare(char[] inputValue, String storedValue) {
+ String storedHash = storedValue.substring(4, storedValue.length() - 2);
+ 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 b1bfd2b74b..d5859763b2 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
@@ -29,5 +29,7 @@ public interface SensitiveDataCodec {
T decode(Object mask) throws Exception;
- void init(Map params);
+ T encode(Object secret) throws Exception;
+
+ void init(Map params) throws Exception;
}
diff --git a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodecTest.java b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodecTest.java
new file mode 100644
index 0000000000..393558d809
--- /dev/null
+++ b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodecTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class DefaultSensitiveStringCodecTest {
+
+ @Test
+ public void testDefaultAlgorithm() throws Exception {
+ SensitiveDataCodec codec = PasswordMaskingUtil.getDefaultCodec();
+ assertTrue(codec instanceof DefaultSensitiveStringCodec);
+ }
+
+ @Test
+ public void testOnewayAlgorithm() throws Exception {
+ DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
+ Map params = new HashMap<>();
+ params.put(DefaultSensitiveStringCodec.ALGORITHM, DefaultSensitiveStringCodec.ONE_WAY);
+ codec.init(params);
+
+ String plainText = "some_password";
+ String maskedText = codec.encode(plainText);
+ System.out.println("encoded value: " + maskedText);
+
+ //one way can't decode
+ try {
+ codec.decode(maskedText);
+ fail("one way algorithm can't decode");
+ } catch (IllegalArgumentException expected) {
+ }
+
+ assertTrue(codec.verify(plainText.toCharArray(), maskedText));
+
+ String otherPassword = "some_other_password";
+ assertFalse(codec.verify(otherPassword.toCharArray(), maskedText));
+ }
+
+ @Test
+ public void testTwowayAlgorithm() throws Exception {
+ DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
+ Map params = new HashMap<>();
+ params.put(DefaultSensitiveStringCodec.ALGORITHM, DefaultSensitiveStringCodec.TWO_WAY);
+ codec.init(params);
+
+ String plainText = "some_password";
+ String maskedText = codec.encode(plainText);
+ System.out.println("encoded value: " + maskedText);
+
+ String decoded = codec.decode(maskedText);
+ System.out.println("encoded value: " + maskedText);
+
+ assertEquals("decoded result not match: " + decoded, decoded, plainText);
+ }
+}
diff --git a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/HashProcessorTest.java b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/HashProcessorTest.java
new file mode 100644
index 0000000000..83199b5159
--- /dev/null
+++ b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/HashProcessorTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public class HashProcessorTest {
+
+ private static final String USER1_PASSWORD = "password";
+ private static final String USER1_HASHED_PASSWORD = "ENC(1024:973A466A489ABFDED3D4B3D181DC77F410F2FC6E87432809A46B72B294147D76:C999ECA8A85387E1FFB14E4FE5CECD17948BA80BA04318A9BE4C3E34B7FE2925F43AB6BC9DFE0D9855DA67439AEEB9850351BC4D5D3AEC6A6903C42B8EB4ED1E)";
+
+ private static final String USER2_PASSWORD = "manager";
+ private static final String USER2_HASHED_PASSWORD = "ENC(1024:48018CDB1B5925DA2CC51DBD6F7E8C5FF156C22C03C6C69720C56F8BE76A1D48:0A0F68C2C01F46D347C6C51D641291A4608EDA50A873ED122909D9134B7A757C14176F0C033F0BD3CE35B3C373D5B652650CDE5FFBBB0F286D4495CEFEEDB166)";
+
+ private static final String USER3_PASSWORD = "artemis000";
+
+ @Parameterized.Parameters(name = "{index}: testing password {0}")
+ public static Collection