ARTEMIS-2243 user/role ops for PropertiesLoginModule via mgmnt
authorJustin Bertram <jbertram@apache.org>
Wed, 30 Jan 2019 00:35:16 +0000 (18:35 -0600)
committerClebert Suconic <clebertsuconic@apache.org>
Thu, 7 Feb 2019 15:16:01 +0000 (10:16 -0500)
19 files changed:
artemis-cli/pom.xml
artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/AddUser.java
artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ListUser.java
artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/RemoveUser.java
artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/ResetUser.java
artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/UserAction.java
artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java
artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQServerControl.java
artemis-distribution/src/main/resources/bin/artemis
artemis-distribution/src/main/resources/bin/artemis.cmd
artemis-features/src/main/resources/features.xml
artemis-server/pom.xml
artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java
artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java
artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModuleConfigurator.java [moved from artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/user/FileBasedSecStoreConfig.java with 57% similarity]
docs/user-manual/en/security.md
pom.xml
tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ActiveMQServerControlTest.java
tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ActiveMQServerControlUsingCoreTest.java

index d0007bc..3d53545 100644 (file)
       </dependency>
       <dependency>
          <groupId>org.apache.commons</groupId>
-         <artifactId>commons-configuration2</artifactId>
-         <version>${commons.config.version}</version>
+         <artifactId>commons-lang3</artifactId>
       </dependency>
       <dependency>
          <groupId>org.apache.commons</groupId>
-         <artifactId>commons-lang3</artifactId>
-         <version>${commons.lang.version}</version>
+         <artifactId>commons-configuration2</artifactId>
+         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>com.sun.winsw</groupId>
index 37bd676..a105bcd 100644 (file)
@@ -20,6 +20,7 @@ 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.spi.core.security.jaas.PropertiesLoginModuleConfigurator;
 import org.apache.commons.lang3.StringUtils;
 
 /**
@@ -53,7 +54,7 @@ public class AddUser extends PasswordAction {
     * @throws IllegalArgumentException if user exists
     */
    private void add(String hash, String... role) throws Exception {
-      FileBasedSecStoreConfig config = getConfiguration();
+      PropertiesLoginModuleConfigurator config = new PropertiesLoginModuleConfigurator(entry, getBrokerEtc());
       config.addNewUser(username, hash, role);
       config.save();
       context.out.println("User added successfully.");
index c0fb979..b18deba 100644 (file)
  */
 package org.apache.activemq.artemis.cli.commands.user;
 
-import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import io.airlift.airline.Command;
 import org.apache.activemq.artemis.cli.commands.ActionContext;
+import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModuleConfigurator;
 
 /**
  * list existing users, example:
@@ -42,11 +44,24 @@ public class ListUser extends UserAction {
     * if username is not specified
     */
    private void list() throws Exception {
-      FileBasedSecStoreConfig config = getConfiguration();
-      List<String> result = config.listUser(username);
-      for (String str : result) {
-         context.out.println(str);
+      PropertiesLoginModuleConfigurator config = new PropertiesLoginModuleConfigurator(entry, getBrokerEtc());
+      Map<String, Set<String>> result = config.listUser(username);
+      StringBuilder logMessage = new StringBuilder("--- \"user\"(roles) ---\n");
+      int userCount = 0;
+      for (Map.Entry<String, Set<String>> entry : result.entrySet()) {
+         logMessage.append("\"").append(entry.getKey()).append("\"(");
+         int roleCount = 0;
+         for (String role : entry.getValue()) {
+            logMessage.append(role);
+            if (++roleCount < entry.getValue().size()) {
+               logMessage.append(",");
+            }
+         }
+         logMessage.append(")\n");
+         userCount++;
       }
+      logMessage.append("\n Total: ").append(userCount);
+      context.out.println(logMessage);
    }
 
 }
index a9dce8d..c49e36b 100644 (file)
@@ -18,6 +18,7 @@ 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.spi.core.security.jaas.PropertiesLoginModuleConfigurator;
 
 /**
  * Remove a user, example:
@@ -35,7 +36,7 @@ public class RemoveUser extends UserAction {
    }
 
    private void remove() throws Exception {
-      FileBasedSecStoreConfig config = getConfiguration();
+      PropertiesLoginModuleConfigurator config = new PropertiesLoginModuleConfigurator(entry, getBrokerEtc());
       config.removeUser(username);
       config.save();
       context.out.println("User removed.");
index 2e3e725..162aea6 100644 (file)
@@ -20,6 +20,7 @@ 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.spi.core.security.jaas.PropertiesLoginModuleConfigurator;
 import org.apache.commons.lang3.StringUtils;
 
 /**
@@ -57,7 +58,7 @@ public class ResetUser extends PasswordAction {
          context.err.println("Nothing to update.");
          return;
       }
-      FileBasedSecStoreConfig config = getConfiguration();
+      PropertiesLoginModuleConfigurator config = new PropertiesLoginModuleConfigurator(entry, getBrokerEtc());
       config.updateUser(username, password, roles);
       config.save();
       context.out.println("User updated");
index e4a2e45..d6f8ba9 100644 (file)
  */
 package org.apache.activemq.artemis.cli.commands.user;
 
-import javax.security.auth.login.AppConfigurationEntry;
-import javax.security.auth.login.Configuration;
-import java.io.File;
-
 import io.airlift.airline.Option;
 import org.apache.activemq.artemis.cli.commands.InputAbstract;
-import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule;
-
-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 {
 
@@ -50,30 +42,6 @@ public abstract class UserAction extends InputAbstract {
       }
    }
 
-   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(getBrokerEtc());
-            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;
    }
index 23493b9..cb89809 100644 (file)
@@ -23,6 +23,8 @@ import javax.jms.MessageConsumer;
 import javax.jms.MessageProducer;
 import javax.jms.Session;
 import javax.jms.TextMessage;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -34,20 +36,24 @@ import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Pattern;
 
+import org.apache.activemq.artemis.api.core.ActiveMQIllegalStateException;
+import org.apache.activemq.artemis.api.core.JsonUtil;
 import org.apache.activemq.artemis.api.core.Pair;
 import org.apache.activemq.artemis.api.core.RoutingType;
 import org.apache.activemq.artemis.api.core.SimpleString;
 import org.apache.activemq.artemis.api.core.client.ClientSession;
 import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
 import org.apache.activemq.artemis.api.core.client.ServerLocator;
+import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl;
 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.queue.StatQueue;
 import org.apache.activemq.artemis.cli.commands.Run;
+import org.apache.activemq.artemis.cli.commands.queue.StatQueue;
 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;
@@ -390,6 +396,173 @@ public class ArtemisTest extends CliTestBase {
    }
 
    @Test
+   public void testUserCommandViaManagement() 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", "--no-autotune", "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor");
+      System.setProperty("artemis.instance", instance1.getAbsolutePath());
+      Object result = Artemis.internalExecute("run");
+      ActiveMQServer activeMQServer = ((Pair<ManagementContext, ActiveMQServer>)result).getB();
+      ActiveMQServerControl activeMQServerControl = activeMQServer.getActiveMQServerControl();
+
+      File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties");
+      File roleFile = new File(instance1.getAbsolutePath() + "/etc/artemis-roles.properties");
+
+      //default only one user admin with role amq
+      String jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq");
+      checkRole("admin", roleFile, "amq");
+
+      //add a simple user
+      activeMQServerControl.addUser("guest", "guest123", "admin", true, "activemq");
+
+      //verify add
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin");
+      checkRole("guest", roleFile, "admin");
+      assertTrue(checkPassword("guest", "guest123", userFile));
+
+      //add a user with 2 roles
+      activeMQServerControl.addUser("scott", "tiger", "admin,operator", true, "activemq");
+
+      //verify add
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "operator");
+      checkRole("scott", roleFile, "admin", "operator");
+      assertTrue(checkPassword("scott", "tiger", userFile));
+
+      try {
+         activeMQServerControl.addUser("scott", "password", "visitor", true, "activemq");
+         fail("should throw an exception if adding a existing user");
+      } catch (IllegalArgumentException expected) {
+      }
+
+      //check existing users are intact
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq");
+      contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "operator");
+
+      //check listing with just one user
+      jsonResult = activeMQServerControl.listUser("admin", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq");
+      contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin", false);
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "admin", false);
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "operator", false);
+
+      //check listing with another single user
+      jsonResult = activeMQServerControl.listUser("guest", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq", false);
+      contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "admin", false);
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "operator", false);
+
+      //remove a user
+      activeMQServerControl.removeUser("guest", "activemq");
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq");
+      contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin", false);
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "operator");
+
+      //remove another
+      activeMQServerControl.removeUser("scott", "activemq");
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq");
+      contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin", false);
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "admin", false);
+      contains(JsonUtil.readJsonArray(jsonResult), "scott", "operator", false);
+
+      //remove non-exist
+      try {
+         activeMQServerControl.removeUser("alien", "activemq");
+         fail("should throw exception when removing a non-existing user");
+      } catch (IllegalArgumentException expected) {
+      }
+
+      //check
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq");
+
+      //now remove last
+      activeMQServerControl.removeUser("admin", "activemq");
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq", false);
+
+      stopServer();
+   }
+
+   @Test
+   public void testBadSecurityEntryNameViaManagement() 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", "--no-autotune", "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor");
+      System.setProperty("artemis.instance", instance1.getAbsolutePath());
+      Object result = Artemis.internalExecute("run");
+      ActiveMQServer activeMQServer = ((Pair<ManagementContext, ActiveMQServer>)result).getB();
+      ActiveMQServerControl activeMQServerControl = activeMQServer.getActiveMQServerControl();
+
+      try {
+         activeMQServerControl.listUser("", "activemqx");
+         fail();
+      } catch (ActiveMQIllegalStateException expected) {
+      }
+
+      stopServer();
+   }
+
+   @Test
+   public void testMissingUserFileViaManagement() 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", "--no-autotune", "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor");
+      System.setProperty("artemis.instance", instance1.getAbsolutePath());
+      Object result = Artemis.internalExecute("run");
+      ActiveMQServer activeMQServer = ((Pair<ManagementContext, ActiveMQServer>)result).getB();
+      ActiveMQServerControl activeMQServerControl = activeMQServer.getActiveMQServerControl();
+
+      File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties");
+      userFile.delete();
+      //      File roleFile = new File(instance1.getAbsolutePath() + "/etc/artemis-roles.properties");
+
+      try {
+         activeMQServerControl.listUser("", "activemq");
+         fail();
+      } catch (ActiveMQIllegalStateException expected) {
+      }
+
+      stopServer();
+   }
+
+   @Test
+   public void testMissingRoleFileViaManagement() 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", "--no-autotune", "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor");
+      System.setProperty("artemis.instance", instance1.getAbsolutePath());
+      Object result = Artemis.internalExecute("run");
+      ActiveMQServer activeMQServer = ((Pair<ManagementContext, ActiveMQServer>)result).getB();
+      ActiveMQServerControl activeMQServerControl = activeMQServer.getActiveMQServerControl();
+
+      File roleFile = new File(instance1.getAbsolutePath() + "/etc/artemis-roles.properties");
+      roleFile.delete();
+
+      try {
+         activeMQServerControl.listUser("", "activemq");
+         fail();
+      } catch (ActiveMQIllegalStateException expected) {
+      }
+
+      stopServer();
+   }
+
+   @Test
    public void testUserCommandReset() throws Exception {
       Run.setEmbedded(true);
       File instance1 = new File(temporaryFolder.getRoot(), "instance_user");
@@ -454,9 +627,9 @@ public class ArtemisTest extends CliTestBase {
 
       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)"));
+      assertTrue(Pattern.compile("\"user1\"\\((admin|manager),(admin|manager)\\)").matcher(result).find());
+      assertTrue(Pattern.compile("\"user2\"\\((admin|manager|master),(admin|manager|master),(admin|manager|master)\\)").matcher(result).find());
+      assertTrue(Pattern.compile("\"user3\"\\((master|system),(master|system)\\)").matcher(result).find());
 
       checkRole("user1", roleFile, "admin", "manager");
 
@@ -489,6 +662,72 @@ public class ArtemisTest extends CliTestBase {
    }
 
    @Test
+   public void testUserCommandResetViaManagement() 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", "--no-autotune", "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor");
+      System.setProperty("artemis.instance", instance1.getAbsolutePath());
+      Object result = Artemis.internalExecute("run");
+      ActiveMQServer activeMQServer = ((Pair<ManagementContext, ActiveMQServer>)result).getB();
+      ActiveMQServerControl activeMQServerControl = activeMQServer.getActiveMQServerControl();
+
+      File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties");
+      File roleFile = new File(instance1.getAbsolutePath() + "/etc/artemis-roles.properties");
+
+      //default only one user admin with role amq
+      String jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq");
+      checkRole("admin", roleFile, "amq");
+
+      //remove a user
+      activeMQServerControl.removeUser("admin", "activemq");
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq", false);
+
+      //add some users
+      activeMQServerControl.addUser("guest", "guest123", "admin", true, "activemq");
+      activeMQServerControl.addUser("user1", "password1", "admin,manager", true, "activemq");
+      assertTrue(checkPassword("user1", "password1", userFile));
+      activeMQServerControl.addUser("user2", "password2", "admin,manager,master", true, "activemq");
+      activeMQServerControl.addUser("user3", "password3", "system,master", true, "activemq");
+
+
+      //verify use list cmd
+      jsonResult = activeMQServerControl.listUser("", "activemq");
+      contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "user1", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "user1", "manager");
+      contains(JsonUtil.readJsonArray(jsonResult), "user2", "admin");
+      contains(JsonUtil.readJsonArray(jsonResult), "user2", "manager");
+      contains(JsonUtil.readJsonArray(jsonResult), "user2", "master");
+      contains(JsonUtil.readJsonArray(jsonResult), "user3", "master");
+      contains(JsonUtil.readJsonArray(jsonResult), "user3", "system");
+
+      checkRole("user1", roleFile, "admin", "manager");
+
+      //reset password
+      activeMQServerControl.resetUser("user1", "newpassword1", null, "activemq");
+
+      checkRole("user1", roleFile, "admin", "manager");
+      assertFalse(checkPassword("user1", "password1", userFile));
+      assertTrue(checkPassword("user1", "newpassword1", userFile));
+
+      //reset role
+      activeMQServerControl.resetUser("user2", null, "manager,master,operator", "activemq");
+
+      checkRole("user2", roleFile, "manager", "master", "operator");
+      assertTrue(checkPassword("user2", "password2", userFile));
+
+      //reset both
+      activeMQServerControl.resetUser("user3", "newpassword3", "admin,system", "activemq");
+
+      checkRole("user3", roleFile, "admin", "system");
+      assertTrue(checkPassword("user3", "newpassword3", userFile));
+      stopServer();
+   }
+
+   @Test
    public void testMaskCommand() throws Exception {
 
       String password1 = "password";
@@ -1074,4 +1313,33 @@ public class ArtemisTest extends CliTestBase {
       return processor.compare(password.toCharArray(), storedPassword);
    }
 
+   private void contains(JsonArray users, String username, String role) {
+      contains(users, username, role, true);
+   }
+
+   private void contains(JsonArray users, String username, String role, boolean contains) {
+      boolean userFound = false;
+      boolean roleFound = false;
+      for (int i = 0; i < users.size(); i++) {
+         JsonObject user = users.getJsonObject(i);
+         if (user.getString("username").equals(username)) {
+            userFound = true;
+            JsonArray roles = user.getJsonArray("roles");
+            for (int j = 0; j < roles.size(); j++) {
+               if (roles.getString(j).equals(role)) {
+                  roleFound = true;
+                  break;
+               }
+            }
+         }
+      }
+      if (contains) {
+         assertTrue("user " + username + " not found", userFound);
+         assertTrue("role " + role + " not found", roleFound);
+      } else {
+         assertFalse("user " + username + " found", userFound);
+         assertFalse("role " + role + " found", roleFound);
+      }
+   }
+
 }
index 06819b2..04c7e16 100644 (file)
@@ -1341,5 +1341,59 @@ public interface ActiveMQServerControl {
     */
    @Operation(desc = "Names of the cluster-connections deployed on this server", impact = MBeanOperationInfo.INFO)
    String[] getClusterConnectionNames();
+
+   /**
+    * Add a user (only applicable when using the JAAS PropertiesLoginModule)
+    *
+    * @param username
+    * @param password
+    * @param roles
+    * @param entryName
+    * @throws Exception
+    */
+   @Operation(desc = "add a user (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION)
+   void addUser(@Parameter(name = "username", desc = "Name of the user") String username,
+                @Parameter(name = "password", desc = "User's password") String password,
+                @Parameter(name = "roles (comma separated)", desc = "User's role") String roles,
+                @Parameter(name = "plaintext", desc = "whether or not to store the password in plaintext or hash it") boolean plaintext,
+                @Parameter(name = "entryName", desc = "Name of entry in login.config ('activemq' by default)") String entryName) throws Exception;
+
+   /**
+    * List the information about a user or all users if no username is supplied (only applicable when using the JAAS PropertiesLoginModule).
+    *
+    * @param username
+    * @param entryName
+    * @return JSON array of user & role information
+    * @throws Exception
+    */
+   @Operation(desc = "list info about a user or all users if no username is supplied (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION)
+   String listUser(@Parameter(name = "username", desc = "Name of the user; leave null to list all known users") String username,
+                   @Parameter(name = "entryName", desc = "Name of entry in login.config ('activemq' by default)") String entryName) throws Exception;
+
+   /**
+    * Remove a user (only applicable when using the JAAS PropertiesLoginModule).
+    *
+    * @param username
+    * @param entryName
+    * @throws Exception
+    */
+   @Operation(desc = "remove a user (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION)
+   void removeUser(@Parameter(name = "username", desc = "Name of the user") String username,
+                   @Parameter(name = "entryName", desc = "Name of entry in login.config ('activemq' by default)") String entryName) throws Exception;
+
+   /**
+    * Set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule).
+    *
+    * @param username
+    * @param password
+    * @param roles
+    * @param entryName
+    * @throws Exception
+    */
+   @Operation(desc = "set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION)
+   void resetUser(@Parameter(name = "username", desc = "Name of the user") String username,
+                  @Parameter(name = "password", desc = "User's password") String password,
+                  @Parameter(name = "roles (comma separated)", desc = "User's role") String roles,
+                  @Parameter(name = "entryName", desc = "Name of entry in login.config ('activemq' by default)") String entryName) throws Exception;
 }
 
index 4b5ebe5..6fb7385 100755 (executable)
@@ -46,7 +46,7 @@ fi
 
 # Set Defaults Properties
 JAVA_ARGS="-XX:+UseParallelGC -Xms512M -Xmx1024M"
-CLASSPATH="$ARTEMIS_HOME/lib/artemis-boot.jar"
+CLASSPATH="$ARTEMIS_HOME/lib/*"
 
 # OS specific support.
 cygwin=false;
index 66f4cdb..c43d1b8 100755 (executable)
@@ -50,7 +50,7 @@ set JAVA_ARGS=-XX:+UseParallelGC -Xms512M -Xmx1024M
 rem "Create full JVM Args"
 set JVM_ARGS=%JAVA_ARGS%
 if not "%ARTEMIS_CLUSTER_PROPS%"=="" set JVM_ARGS=%JVM_ARGS% %ARTEMIS_CLUSTER_PROPS%
-set JVM_ARGS=%JVM_ARGS% -classpath %ARTEMIS_HOME%\lib\artemis-boot.jar
+set JVM_ARGS=%JVM_ARGS% -classpath %ARTEMIS_HOME%\lib\*
 set JVM_ARGS=%JVM_ARGS% -Dartemis.home=%ARTEMIS_HOME%
 if not "%DEBUG_ARGS%"=="" set JVM_ARGS=%JVM_ARGS% %DEBUG_ARGS%
 
index 38764e9..d3bcae4 100644 (file)
@@ -62,6 +62,9 @@
                <configfile finalname="etc/artemis.xml">mvn:org.apache.activemq/artemis-features/${pom.version}/xml/artemis</configfile>
 
                <bundle dependency="true">mvn:org.apache.geronimo.specs/geronimo-jms_2.0_spec/${geronimo.jms.2.spec.version}</bundle>
+               <bundle dependency="true">mvn:org.apache.commons/commons-configuration2/${commons.config.version}</bundle>
+               <bundle dependency="true">mvn:org.apache.commons/commons-text/1.6</bundle>
+               <bundle dependency="true">mvn:org.apache.commons/commons-lang3/${commons.lang.version}</bundle>
 
                <bundle>mvn:org.apache.activemq/artemis-native/${pom.version}</bundle>
                <bundle>mvn:org.apache.activemq/artemis-server-osgi/${pom.version}</bundle>
index f24731b..d7b8f70 100644 (file)
          <artifactId>commons-beanutils</artifactId>
       </dependency>
       <dependency>
+         <groupId>org.apache.commons</groupId>
+         <artifactId>commons-configuration2</artifactId>
+      </dependency>
+      <dependency>
          <groupId>commons-io</groupId>
          <artifactId>commons-io</artifactId>
          <version>2.6</version>
index 302da89..c89dacb 100644 (file)
@@ -30,6 +30,7 @@ import javax.management.NotificationEmitter;
 import javax.management.NotificationFilter;
 import javax.management.NotificationListener;
 import javax.transaction.xa.Xid;
+import java.net.URL;
 import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -118,8 +119,10 @@ import org.apache.activemq.artemis.core.transaction.TransactionDetailFactory;
 import org.apache.activemq.artemis.core.transaction.impl.CoreTransactionDetail;
 import org.apache.activemq.artemis.core.transaction.impl.XidImpl;
 import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
+import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModuleConfigurator;
 import org.apache.activemq.artemis.utils.JsonLoader;
 import org.apache.activemq.artemis.utils.ListUtil;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
 import org.apache.activemq.artemis.utils.SecurityFormatter;
 import org.apache.activemq.artemis.utils.collections.TypedProperties;
 import org.jboss.logging.Logger;
@@ -2956,5 +2959,52 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active
       this.broadcaster.sendNotification(new Notification(type.toString(), this, notifSeq.incrementAndGet(), notification.toString()));
    }
 
+   @Override
+   public void addUser(String username, String password, String roles, boolean plaintext, String entryName) throws Exception {
+      PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(entryName);
+      config.addNewUser(username, plaintext ? password : PasswordMaskingUtil.getHashProcessor().hash(password), roles.split(","));
+      config.save();
+   }
+
+   @Override
+   public String listUser(String username, String entryName) throws Exception {
+      PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(entryName);
+      Map<String, Set<String>> info = config.listUser(username);
+      JsonArrayBuilder users = JsonLoader.createArrayBuilder();
+      for (Entry<String, Set<String>> entry : info.entrySet()) {
+         JsonObjectBuilder user = JsonLoader.createObjectBuilder();
+         user.add("username", entry.getKey());
+         JsonArrayBuilder roles = JsonLoader.createArrayBuilder();
+         for (String role : entry.getValue()) {
+            roles.add(role);
+         }
+         user.add("roles", roles);
+         users.add(user);
+      }
+      return users.build().toString();
+   }
+
+   @Override
+   public void removeUser(String username, String entryName) throws Exception {
+      PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(entryName);
+      config.removeUser(username);
+      config.save();
+   }
+
+   @Override
+   public void resetUser(String username, String password, String roles, String entryName) throws Exception {
+      PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(entryName);
+      config.updateUser(username, password, roles == null ? null : roles.split(","));
+      config.save();
+   }
+
+   private PropertiesLoginModuleConfigurator getPropertiesLoginModuleConfigurator(String entryName) throws Exception {
+      URL configurationUrl = server.getConfiguration().getConfigurationUrl();
+      if (configurationUrl == null) {
+         throw ActiveMQMessageBundle.BUNDLE.failedToLocateConfigURL();
+      }
+      String path = configurationUrl.getPath();
+      return new PropertiesLoginModuleConfigurator(entryName, path.substring(0, path.lastIndexOf("/")));
+   }
 }
 
index d649d8d..5cb8c60 100644 (file)
@@ -449,4 +449,24 @@ public interface ActiveMQMessageBundle {
    @Message(id = 119217, value = "Can't write to closed file: {0}", format = Message.Format.MESSAGE_FORMAT)
    ActiveMQIOErrorException cannotWriteToClosedFile(SequentialFile file);
 
+   @Message(id = 229218, value = "Failed to locate broker configuration URL")
+   ActiveMQIllegalStateException failedToLocateConfigURL();
+
+   @Message(id = 229219, value = "Failed to load security configuration")
+   ActiveMQIllegalStateException failedToLoadSecurityConfig();
+
+   @Message(id = 229220, value = "Failed to load user file: {0}", format = Message.Format.MESSAGE_FORMAT)
+   ActiveMQIllegalStateException failedToLoadUserFile(String path);
+
+   @Message(id = 229221, value = "Failed to load role file: {0}", format = Message.Format.MESSAGE_FORMAT)
+   ActiveMQIllegalStateException failedToLoadRoleFile(String path);
+
+   @Message(id = 229222, value = "Failed to find login module entry {0} from JAAS configuration", format = Message.Format.MESSAGE_FORMAT)
+   ActiveMQIllegalStateException failedToFindLoginModuleEntry(String entry);
+
+   @Message(id = 229223, value = "User {0} already exists", format = Message.Format.MESSAGE_FORMAT)
+   IllegalArgumentException userAlreadyExists(String user);
+
+   @Message(id = 229224, value = "User {0} does not exist", format = Message.Format.MESSAGE_FORMAT)
+   IllegalArgumentException userDoesNotExist(String user);
 }
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.activemq.artemis.cli.commands.user;
+package org.apache.activemq.artemis.spi.core.security.jaas;
 
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
 import java.io.File;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import org.apache.activemq.artemis.api.core.Pair;
+import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle;
 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;
 
-class FileBasedSecStoreConfig {
+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 class PropertiesLoginModuleConfigurator {
 
    private static final String LICENSE_HEADER =
            "## ---------------------------------------------------------------------------\n" +
@@ -51,80 +61,110 @@ class FileBasedSecStoreConfig {
    private PropertiesConfiguration userConfig;
    private PropertiesConfiguration roleConfig;
 
-   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);
-         }
+   public PropertiesLoginModuleConfigurator(String entryName, String brokerEtc) throws Exception {
+      if (entryName == null || entryName.length() == 0) {
+         entryName = "activemq";
+      }
+
+      Configuration securityConfig = Configuration.getConfiguration();
+      AppConfigurationEntry[] entries = securityConfig.getAppConfigurationEntry(entryName);
+
+      if (entries == null || entries.length == 0) {
+         throw ActiveMQMessageBundle.BUNDLE.failedToLoadSecurityConfig();
       }
 
-      if (roleHeader == null) {
-         if (roleConfig.isEmpty()) {
-            //clean and reset header
-            roleConfig.clear();
-            roleConfig.setHeader(LICENSE_HEADER);
+      int entriesInspected = 0;
+      for (AppConfigurationEntry entry : entries) {
+         entriesInspected++;
+         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(brokerEtc);
+            File userFile = new File(etcDir, userFileName);
+            File roleFile = new File(etcDir, roleFileName);
+
+            if (!userFile.exists()) {
+               throw ActiveMQMessageBundle.BUNDLE.failedToLoadUserFile(brokerEtc + userFileName);
+            }
+
+            if (!roleFile.exists()) {
+               throw ActiveMQMessageBundle.BUNDLE.failedToLoadRoleFile(brokerEtc + roleFileName);
+            }
+
+            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);
+               }
+            }
+            return;
          }
       }
+
+      if (entriesInspected == entries.length) {
+         throw ActiveMQMessageBundle.BUNDLE.failedToFindLoginModuleEntry(entryName);
+      }
    }
 
-   void addNewUser(String username, String hash, String... roles) throws Exception {
+   public void addNewUser(String username, String hash, String... roles) throws Exception {
       if (userConfig.getString(username) != null) {
-         throw new IllegalArgumentException("User already exist: " + username);
+         throw ActiveMQMessageBundle.BUNDLE.userAlreadyExists(username);
       }
       userConfig.addProperty(username, hash);
       addRoles(username, roles);
    }
 
-   void save() throws Exception {
+   public void save() throws Exception {
       userBuilder.save();
       roleBuilder.save();
    }
 
-   void removeUser(String username) throws Exception {
+   public void removeUser(String username) {
       if (userConfig.getProperty(username) == null) {
-         throw new IllegalArgumentException("user " + username + " doesn't exist.");
+         throw ActiveMQMessageBundle.BUNDLE.userDoesNotExist(username);
       }
       userConfig.clearProperty(username);
       removeRoles(username);
    }
 
-   List<String> listUser(String username) {
-      List<String> result = new ArrayList<>();
-      result.add("--- \"user\"(roles) ---\n");
+   public Map<String, Set<String>> listUser(String username) {
+      Map<String, Set<String>> result = new HashMap<>();
 
-      int totalUsers = 0;
-      if (username != null) {
-         String roles = findRoles(username);
-         result.add("\"" + username + "\"(" + roles + ")");
-         totalUsers++;
+      if (username != null && username.length() > 0) {
+         result.put(username, findRoles(username));
       } else {
          Iterator<String> iter = userConfig.getKeys();
          while (iter.hasNext()) {
             String keyUser = iter.next();
-            String roles = findRoles(keyUser);
-            result.add("\"" + keyUser + "\"(" + roles + ")");
-            totalUsers++;
+            result.put(keyUser, findRoles(keyUser));
          }
       }
-      result.add("\n Total: " + totalUsers);
       return result;
    }
 
-   void updateUser(String username, String password, String[] roles) {
+   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.");
+         throw ActiveMQMessageBundle.BUNDLE.userDoesNotExist(username);
       }
 
       if (password != null) {
@@ -138,33 +178,28 @@ class FileBasedSecStoreConfig {
       }
    }
 
-   private String findRoles(String uname) {
+   private Set<String> findRoles(String username) {
       Iterator<String> iter = roleConfig.getKeys();
-      StringBuilder builder = new StringBuilder();
-      boolean first = true;
+      Set<String> roles = new HashSet();
       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 roleList : roleConfig.getList(String.class, role)) {
+            //each roleList may be a comma separated list
+            String[] items = roleList.split(",");
             for (String item : items) {
-               if (item.equals(uname)) {
-                  if (!first) {
-                     builder.append(",");
-                  }
-                  builder.append(role);
-                  first = false;
+               if (item.equals(username)) {
+                  roles.add(role);
                }
             }
          }
       }
 
-      return builder.toString();
+      return roles;
    }
 
    private void addRoles(String username, String[] roles) {
       for (String role : roles) {
+         role = role.trim();
          List<String> users = roleConfig.getList(String.class, role);
          if (users == null) {
             users = new ArrayList<>();
index 0ac37c4..ef0f363 100644 (file)
@@ -546,6 +546,17 @@ users=system,user
 guests=guest
 ```
 
+As mentioned above, the Artemis command-line interface supports a command to
+`add` a user. Commands to `list` (one or all) users, `remove` a user, and `reset`
+a user's password and/or role(s) are also supported via the command-line
+interface as well as the normal management interfaces (e.g. JMX, web console,
+etc.).
+
+> **Warning**
+>
+> Management and CLI operations to manipulate user & role data are only available
+> when using the `PropertiesLoginModule`.
+
 #### LDAPLoginModule
 
 The LDAP login module enables you to perform authentication and authorization
diff --git a/pom.xml b/pom.xml
index 3fe3697..3b82a86 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -77,7 +77,7 @@
 
       <karaf.version>4.0.6</karaf.version>
       <pax.exam.version>4.9.1</pax.exam.version>
-      <commons.config.version>2.1</commons.config.version>
+      <commons.config.version>2.4</commons.config.version>
       <commons.lang.version>3.0</commons.lang.version>
       <activemq5-version>5.14.5</activemq5-version>
       <apache.derby.version>10.11.1.1</apache.derby.version>
index c74e6d5..847b01e 100644 (file)
@@ -2683,6 +2683,46 @@ public class ActiveMQServerControlTest extends ManagementTestBase {
       Assert.assertTrue(((org.apache.activemq.artemis.jms.client.ActiveMQMessageConsumer)JMSclient).isClosed());
    }
 
+   @Test
+   public void testAddUser() throws Exception {
+      ActiveMQServerControl serverControl = createManagementControl();
+      try {
+         serverControl.addUser("x", "x", "x", true, "x");
+         fail();
+      } catch (Exception expected) {
+      }
+   }
+
+   @Test
+   public void testRemoveUser() throws Exception {
+      ActiveMQServerControl serverControl = createManagementControl();
+      try {
+         serverControl.removeUser("x", "x");
+         fail();
+      } catch (Exception expected) {
+      }
+   }
+
+   @Test
+   public void testListUser() throws Exception {
+      ActiveMQServerControl serverControl = createManagementControl();
+      try {
+         serverControl.listUser("x", "x");
+         fail();
+      } catch (Exception expected) {
+      }
+   }
+
+   @Test
+   public void testResetUser() throws Exception {
+      ActiveMQServerControl serverControl = createManagementControl();
+      try {
+         serverControl.resetUser("x","x","x", "x");
+         fail();
+      } catch (Exception expected) {
+      }
+   }
+
 
    protected void scaleDown(ScaleDownHandler handler) throws Exception {
       SimpleString address = new SimpleString("testQueue");
index 716b5d0..f0315e8 100644 (file)
@@ -336,6 +336,31 @@ public class ActiveMQServerControlUsingCoreTest extends ActiveMQServerControlTes
          }
 
          @Override
+         public void addUser(String username,
+                             String password,
+                             String role,
+                             boolean plaintext,
+                             String entryName) throws Exception {
+            proxy.invokeOperation("addUser", username, password, role, plaintext, entryName);
+
+         }
+
+         @Override
+         public String listUser(String username, String entryName) throws Exception {
+            return (String) proxy.invokeOperation("listUser", username, entryName, String.class);
+         }
+
+         @Override
+         public void removeUser(String username, String entryName) throws Exception {
+            proxy.invokeOperation("removeUser", username, entryName);
+         }
+
+         @Override
+         public void resetUser(String username, String password, String roles, String entryName) throws Exception {
+            proxy.invokeOperation("resetUser", username, password, roles, entryName);
+         }
+
+         @Override
          public String getUptime() {
             return null;
          }