This closes #835

This commit is contained in:
Clebert Suconic 2016-11-02 15:53:00 -04:00
commit 9f7fc88363
37 changed files with 1825 additions and 153 deletions

View File

@ -30,6 +30,8 @@
<properties>
<activemq.basedir>${project.basedir}/..</activemq.basedir>
<winsw.version>1.18</winsw.version>
<commons.config.version>2.1</commons.config.version>
<commons.lang.version>3.0</commons.lang.version>
</properties>
<dependencies>
@ -67,6 +69,16 @@
<groupId>io.airlift</groupId>
<artifactId>airline</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>${commons.config.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang.version}</version>
</dependency>
<dependency>
<groupId>com.sun.winsw</groupId>
<artifactId>winsw</artifactId>

View File

@ -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<Action> builder(File artemisInstance) {
String instance = artemisInstance != null ? artemisInstance.getAbsolutePath() : System.getProperty("artemis.instance");
Cli.CliBuilder<Action> builder = Cli.<Action>builder("artemis").withDescription("ActiveMQ Artemis Command Line").withCommand(HelpAction.class).withCommand(Producer.class).withCommand(Consumer.class).withCommand(Browse.class).withDefaultCommand(HelpAction.class);
Cli.CliBuilder<Action> builder = Cli.<Action>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)").

View File

@ -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;
}

View File

@ -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<String, String> 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;
}
}

View File

@ -58,7 +58,7 @@ public class CreateDestination extends DestinationAction {
}
private void createJmsTopic(final ActionContext context) throws Exception {
performJmsManagement(brokerURL, user, password, new ManagementCallback<Message>() {
performJmsManagement(new ManagementCallback<Message>() {
@Override
public void setUpInvocation(Message message) throws Exception {
JMSManagementHelper.putOperationInvocation(message, "jms.server", "createTopic", getName(), bindings);
@ -90,7 +90,7 @@ public class CreateDestination extends DestinationAction {
}
private void createCoreQueue(final ActionContext context) throws Exception {
performCoreManagement(brokerURL, user, password, new ManagementCallback<ClientMessage>() {
performCoreManagement(new ManagementCallback<ClientMessage>() {
@Override
public void setUpInvocation(ClientMessage message) throws Exception {
String address = getAddress();
@ -112,7 +112,7 @@ public class CreateDestination extends DestinationAction {
private void createJmsQueue(final ActionContext context) throws Exception {
performJmsManagement(brokerURL, user, password, new ManagementCallback<Message>() {
performJmsManagement(new ManagementCallback<Message>() {
@Override
public void setUpInvocation(Message message) throws Exception {

View File

@ -49,7 +49,7 @@ public class DeleteDestination extends DestinationAction {
}
private void deleteJmsTopic(final ActionContext context) throws Exception {
performJmsManagement(brokerURL, user, password, new ManagementCallback<Message>() {
performJmsManagement(new ManagementCallback<Message>() {
@Override
public void setUpInvocation(Message message) throws Exception {
JMSManagementHelper.putOperationInvocation(message, "jms.server", "destroyTopic", getName(), removeConsumers);
@ -74,7 +74,7 @@ public class DeleteDestination extends DestinationAction {
}
private void deleteJmsQueue(final ActionContext context) throws Exception {
performJmsManagement(brokerURL, user, password, new ManagementCallback<Message>() {
performJmsManagement(new ManagementCallback<Message>() {
@Override
public void setUpInvocation(Message message) throws Exception {
JMSManagementHelper.putOperationInvocation(message, "jms.server", "destroyQueue", getName(), removeConsumers);
@ -99,7 +99,7 @@ public class DeleteDestination extends DestinationAction {
}
private void deleteCoreQueue(final ActionContext context) throws Exception {
performCoreManagement(brokerURL, user, password, new ManagementCallback<ClientMessage>() {
performCoreManagement(new ManagementCallback<ClientMessage>() {
@Override
public void setUpInvocation(ClientMessage message) throws Exception {
ManagementHelper.putOperationInvocation(message, "core.server", "destroyQueue", getName());

View File

@ -31,13 +31,12 @@ import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.api.core.management.ManagementHelper;
import org.apache.activemq.artemis.api.jms.ActiveMQJMSClient;
import org.apache.activemq.artemis.api.jms.management.JMSManagementHelper;
import org.apache.activemq.artemis.cli.commands.InputAbstract;
import org.apache.activemq.artemis.core.client.impl.ServerLocatorImpl;
import org.apache.activemq.artemis.cli.commands.messages.ConnectionAbstract;
import org.apache.activemq.artemis.jms.client.ActiveMQConnection;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.apache.activemq.artemis.jms.client.ActiveMQSession;
public abstract class DestinationAction extends InputAbstract {
public abstract class DestinationAction extends ConnectionAbstract {
public static final String JMS_QUEUE = "jms-queue";
public static final String JMS_TOPIC = "topic";
@ -46,24 +45,12 @@ public abstract class DestinationAction extends InputAbstract {
@Option(name = "--type", description = "type of destination to be created (one of jms-queue, topic and core-queue, default jms-queue")
String destType = JMS_QUEUE;
@Option(name = "--url", description = "URL towards the broker. (default: tcp://localhost:61616)")
String brokerURL = "tcp://localhost:61616";
@Option(name = "--user", description = "User used to connect")
String user;
@Option(name = "--password", description = "Password used to connect")
String password;
@Option(name = "--name", description = "destination name")
String name;
public static void performJmsManagement(String brokerURL,
String user,
String password,
ManagementCallback<Message> cb) throws Exception {
public void performJmsManagement(ManagementCallback<Message> cb) throws Exception {
try (ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL, user, password);
try (ActiveMQConnectionFactory factory = createConnectionFactory();
ActiveMQConnection connection = (ActiveMQConnection) factory.createConnection();
ActiveMQSession session = (ActiveMQSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) {
@ -88,12 +75,10 @@ public abstract class DestinationAction extends InputAbstract {
}
}
public static void performCoreManagement(String brokerURL,
String user,
String password,
ManagementCallback<ClientMessage> cb) throws Exception {
public void performCoreManagement(ManagementCallback<ClientMessage> cb) throws Exception {
try (ServerLocator locator = ServerLocatorImpl.newLocator(brokerURL);
try (ActiveMQConnectionFactory factory = createConnectionFactory();
ServerLocator locator = factory.getServerLocator();
ClientSessionFactory sessionFactory = locator.createSessionFactory();
ClientSession session = sessionFactory.createSession(user, password, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)) {
session.start();

View File

@ -40,7 +40,7 @@ public class Browse extends DestAbstract {
System.out.println("Consumer:: filter = " + filter);
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL, user, password);
ActiveMQConnectionFactory factory = createConnectionFactory();
Destination dest = ActiveMQDestination.createDestination(this.destination, ActiveMQDestination.QUEUE_TYPE);
try (Connection connection = factory.createConnection()) {

View File

@ -0,0 +1,68 @@
/**
* 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.messages;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.JMSSecurityException;
import io.airlift.airline.Option;
import org.apache.activemq.artemis.cli.commands.InputAbstract;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
public class ConnectionAbstract extends InputAbstract {
@Option(name = "--url", description = "URL towards the broker. (default: tcp://localhost:61616)")
protected String brokerURL = "tcp://localhost:61616";
@Option(name = "--user", description = "User used to connect")
protected String user;
@Option(name = "--password", description = "Password used to connect")
protected String password;
protected ActiveMQConnectionFactory createConnectionFactory() throws Exception {
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerURL, user, password);
try {
Connection connection = cf.createConnection();
connection.close();
return cf;
} catch (JMSSecurityException e) {
// if a security exception will get the user and password through an input
context.err.println("Connection failed::" + e.getMessage());
userPassword();
return new ActiveMQConnectionFactory(brokerURL, user, password);
} catch (JMSException e) {
// if a connection exception will ask for the URL, user and password
context.err.println("Connection failed::" + e.getMessage());
brokerURL = input("--url", "Type in the broker URL for a retry (e.g. tcp://localhost:61616)", brokerURL);
userPassword();
return new ActiveMQConnectionFactory(brokerURL, user, password);
}
}
private void userPassword() {
if (user == null) {
user = input("--user", "Type the username for a retry", null);
}
if (password == null) {
password = inputPassword("--password", "Type the password for a retry", null);
}
}
}

View File

@ -49,7 +49,7 @@ public class Consumer extends DestAbstract {
System.out.println("Consumer:: filter = " + filter);
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL, user, password);
ActiveMQConnectionFactory factory = createConnectionFactory();
Destination dest = ActiveMQDestination.createDestination(this.destination, ActiveMQDestination.QUEUE_TYPE);
try (Connection connection = factory.createConnection()) {

View File

@ -18,12 +18,8 @@
package org.apache.activemq.artemis.cli.commands.messages;
import io.airlift.airline.Option;
import org.apache.activemq.artemis.cli.commands.ActionAbstract;
public class DestAbstract extends ActionAbstract {
@Option(name = "--url", description = "URL towards the broker. (default: tcp://localhost:61616)")
String brokerURL = "tcp://localhost:61616";
public class DestAbstract extends ConnectionAbstract {
@Option(name = "--destination", description = "Destination to be used. it could be prefixed with queue:// or topic:: (Default: queue://TEST")
String destination = "queue://TEST";
@ -31,12 +27,6 @@ public class DestAbstract extends ActionAbstract {
@Option(name = "--message-count", description = "Number of messages to act on (Default: 1000)")
int messageCount = 1000;
@Option(name = "--user", description = "User used to connect")
String user;
@Option(name = "--password", description = "Password used to connect")
String password;
@Option(name = "--sleep", description = "Time wait between each message")
int sleep = 0;

View File

@ -50,7 +50,7 @@ public class Producer extends DestAbstract {
public Object execute(ActionContext context) throws Exception {
super.execute(context);
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL, user, password);
ActiveMQConnectionFactory factory = createConnectionFactory();
Destination dest = ActiveMQDestination.createDestination(this.destination, ActiveMQDestination.QUEUE_TYPE);
try (Connection connection = factory.createConnection()) {

View File

@ -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.activemq.artemis.util.FileBasedSecStoreConfig;
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 PasswordAction {
@Option(name = "--plaintext", description = "using plaintext (Default false)")
boolean plaintext = false;
@Override
public Object execute(ActionContext context) throws Exception {
super.execute(context);
checkInputUser();
checkInputPassword();
checkInputRole();
String hash = plaintext ? password : HashUtil.tryHash(context, password);
add(hash, StringUtils.split(role, ","));
return 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.");
}
}

View File

@ -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<String> commands = new ArrayList<>(1);
commands.add("user");
help(global, commands);
return null;
}
}

View File

@ -0,0 +1,53 @@
/*
* 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 java.util.List;
import io.airlift.airline.Command;
import org.apache.activemq.artemis.cli.commands.ActionContext;
import org.apache.activemq.artemis.util.FileBasedSecStoreConfig;
/**
* 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;
}
/**
* list a single user or all users
* if username is not specified
*/
protected void list() throws Exception {
FileBasedSecStoreConfig config = getConfiguration();
List<String> result = config.listUser(username);
for (String str : result) {
context.out.println(str);
}
}
}

View File

@ -0,0 +1,37 @@
/**
* 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;
public class PasswordAction extends UserAction {
@Option(name = "--password", description = "the password (Default: input)")
String password;
protected void checkInputPassword() {
if (password == null) {
password = inputPassword("--password", "Please provide the password:", null);
}
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@ -0,0 +1,45 @@
/*
* 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;
import org.apache.activemq.artemis.util.FileBasedSecStoreConfig;
/**
* 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);
checkInputUser();
remove();
return null;
}
protected void remove() throws Exception {
FileBasedSecStoreConfig config = getConfiguration();
config.removeUser(username);
config.save();
context.out.println("User removed.");
}
}

View File

@ -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.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.activemq.artemis.util.FileBasedSecStoreConfig;
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 PasswordAction {
@Option(name = "--plaintext", description = "using plaintext (Default false)")
boolean plaintext = false;
@Override
public Object execute(ActionContext context) throws Exception {
super.execute(context);
checkInputUser();
checkInputPassword();
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;
}
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");
}
}

View File

@ -0,0 +1,85 @@
/*
* 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 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 = "--role", description = "user's role(s), comma separated")
String role;
@Option(name = "--user", description = "The user name (Default: input)")
String username = null;
@Option(name = "--entry", description = "The appConfigurationEntry (default: activemq)")
String entry = "activemq";
protected void checkInputUser() {
if (username == null) {
username = input("--user", "Please provider the userName:", null);
}
}
public void setRole(String role) {
this.role = role;
}
public void checkInputRole() {
if (role == null) {
role = input("--role", "type a comma separated list of roles", null);
}
}
protected FileBasedSecStoreConfig getConfiguration() throws Exception {
Configuration securityConfig = Configuration.getConfiguration();
AppConfigurationEntry[] entries = securityConfig.getAppConfigurationEntry(entry);
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;
}
}

View File

@ -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;
}
}

View File

@ -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<PropertiesConfiguration> userBuilder;
private FileBasedConfigurationBuilder<PropertiesConfiguration> 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<String> listUser(String username) {
List<String> 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<String> 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<String> iter = roleConfig.getKeys();
StringBuilder builder = new StringBuilder();
boolean first = true;
while (iter.hasNext()) {
String role = iter.next();
List<String> 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<String> 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<String> iterKeys = roleConfig.getKeys();
List<Pair<String, List<String>>> updateMap = new ArrayList<>();
while (iterKeys.hasNext()) {
String theRole = iterKeys.next();
List<String> userList = roleConfig.getList(String.class, theRole);
List<String> newList = new ArrayList<>();
boolean roleChaned = false;
for (String value : userList) {
//each value may be comma separated.
List<String> 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<Pair<String, List<String>>> iterUpdate = updateMap.iterator();
while (iterUpdate.hasNext()) {
Pair<String, List<String>> entry = iterUpdate.next();
roleConfig.clearProperty(entry.getA());
if (entry.getB().size() > 0) {
roleConfig.addProperty(entry.getA(), entry.getB());
}
}
}
}

View File

@ -14,4 +14,5 @@
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
${role}=${user}
${role} = ${user}

View File

@ -14,4 +14,5 @@
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
${user}=${password}
${user} = ${password}

View File

@ -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<PropertiesConfiguration> 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<String> userList = StringUtil.splitStringList(storedUsers, ",");
assertTrue(userList.contains(user));
}
}
private boolean checkPassword(String user, String password, File userFile) throws Exception {
Configurations configs = new Configurations();
FileBasedConfigurationBuilder<PropertiesConfiguration> 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);
}
}

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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<String> {
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<String, String> 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<String, String> 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<String> {
System.exit(-1);
}
DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
Map<String, String> 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<String, String> params;
CodecAlgorithm(Map<String, String> 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<String, String> 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<String, String> 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);
}
}
}

View File

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

View File

@ -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());
}
}

View File

@ -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<String, HashProcessor> 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 <full qualified class name> is required. key/value pairs are optional
*/
public static SensitiveDataCodec<String> getCodec(String codecDesc) throws ActiveMQException {
SensitiveDataCodec<String> codecInstance = null;
SensitiveDataCodec<String> 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<String> getDefaultCodec() {
public static DefaultSensitiveStringCodec getDefaultCodec() {
return new DefaultSensitiveStringCodec();
}
}

View File

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

View File

@ -29,5 +29,7 @@ public interface SensitiveDataCodec<T> {
T decode(Object mask) throws Exception;
void init(Map<String, String> params);
T encode(Object secret) throws Exception;
void init(Map<String, String> params) throws Exception;
}

View File

@ -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<String> codec = PasswordMaskingUtil.getDefaultCodec();
assertTrue(codec instanceof DefaultSensitiveStringCodec);
}
@Test
public void testOnewayAlgorithm() throws Exception {
DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
Map<String, String> 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<String, String> 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);
}
}

View File

@ -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<Object[]> 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);
}
}

View File

@ -89,6 +89,8 @@
<include>commons-beanutils:commons-beanutils</include>
<include>commons-logging:commons-logging</include>
<include>commons-collections:commons-collections</include>
<include>org.apache.commons:commons-configuration2</include>
<include>org.apache.commons:commons-lang3</include>
<include>org.fusesource.hawtbuf:hawtbuf</include>
<include>org.jgroups:jgroups</include>
<include>org.apache.geronimo.specs:geronimo-json_1.0_spec</include>

View File

@ -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<Principal> 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);

View File

@ -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
<mask-password>true</mask-password>
<password-codec>org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;key=hello world</password-codec>
```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