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 data() { + return Arrays.asList(new Object[][] { + {USER1_PASSWORD, USER1_HASHED_PASSWORD, true}, + {USER2_PASSWORD, USER2_HASHED_PASSWORD, true}, + {USER3_PASSWORD, USER3_PASSWORD, true}, + {USER1_PASSWORD, USER2_PASSWORD, false}, + {USER3_PASSWORD, USER2_HASHED_PASSWORD, false} + }); + } + + private String password; + private String storedPassword; + private boolean match; + + public HashProcessorTest(String password, String storedPassword, boolean match) { + this.password = password; + this.storedPassword = storedPassword; + this.match = match; + } + + @Test + public void testPasswordVerification() throws Exception { + HashProcessor processor = PasswordMaskingUtil.getHashProcessor(storedPassword); + boolean result = processor.compare(password.toCharArray(), storedPassword); + assertEquals(match, result); + } +} diff --git a/artemis-distribution/src/main/assembly/dep.xml b/artemis-distribution/src/main/assembly/dep.xml index d21f1e0d6f..aa3635e7f6 100644 --- a/artemis-distribution/src/main/assembly/dep.xml +++ b/artemis-distribution/src/main/assembly/dep.xml @@ -89,6 +89,8 @@ commons-beanutils:commons-beanutils commons-logging:commons-logging commons-collections:commons-collections + org.apache.commons:commons-configuration2 + org.apache.commons:commons-lang3 org.fusesource.hawtbuf:hawtbuf org.jgroups:jgroups org.apache.geronimo.specs:geronimo-json_1.0_spec diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java index d120a98a28..957bb8ab47 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java @@ -32,14 +32,16 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import org.apache.activemq.artemis.utils.HashProcessor; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.jboss.logging.Logger; public class PropertiesLoginModule extends PropertiesLoader implements LoginModule { private static final Logger logger = Logger.getLogger(PropertiesLoginModule.class); - private static final String USER_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.user"; - private static final String ROLE_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.role"; + public static final String USER_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.user"; + public static final String ROLE_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.role"; private Subject subject; private CallbackHandler callbackHandler; @@ -49,6 +51,7 @@ public class PropertiesLoginModule extends PropertiesLoader implements LoginModu private String user; private final Set principals = new HashSet<>(); private boolean loginSucceeded; + private HashProcessor hashProcessor; @Override public void initialize(Subject subject, @@ -90,10 +93,21 @@ public class PropertiesLoginModule extends PropertiesLoader implements LoginModu if (password == null) { throw new FailedLoginException("User does exist"); } - if (!password.equals(new String(tmpPassword))) { - throw new FailedLoginException("Password does not match"); + + //password is hashed + try { + hashProcessor = PasswordMaskingUtil.getHashProcessor(password); + + if (!hashProcessor.compare(tmpPassword, password)) { + throw new FailedLoginException("Password does not match"); + } + loginSucceeded = true; + } catch (Exception e) { + if (debug) { + logger.debug("Exception getting a hash processor", e); + } + throw new FailedLoginException("Failed to get hash processor"); } - loginSucceeded = true; if (debug) { logger.debug("login " + user); diff --git a/docs/user-manual/en/configuration-index.md b/docs/user-manual/en/configuration-index.md index 65ef931906..240d6db8dc 100644 --- a/docs/user-manual/en/configuration-index.md +++ b/docs/user-manual/en/configuration-index.md @@ -396,29 +396,14 @@ will have to be in masked form. ### Masking passwords in artemis-users.properties Apache ActiveMQ Artemis's built-in security manager uses plain properties files -where the user passwords are specified in plaintext forms by default. To -mask those parameters the following two properties need to be set -in the 'bootstrap.xml' file. +where the user passwords are specified in hash forms by default. -`mask-password` -- If set to "true" all the passwords are masked. -Default is false. +Please use Artemis CLI command to add a password. For example -`password-codec` -- Class name and its parameters for the Decoder used -to decode the masked password. Ignored if `mask-password` 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: - -```xml -true -org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;key=hello world +```sh + ./artemis user add --username guest --password guest --role admin ``` -When so configured, the Apache ActiveMQ Artemis security manager will initialize a -DefaultSensitiveStringCodec with the parameters "key"-\>"hello world", -then use it to decode all the masked passwords in this configuration -file. - ### Choosing a decoder for password masking As described in the previous sections, all password masking requires a