You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by jb...@apache.org on 2021/05/25 16:17:02 UTC

[activemq-artemis] branch main updated: ARTEMIS-3288 support bulk user loading with basic security manager

This is an automated email from the ASF dual-hosted git repository.

jbertram pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git


The following commit(s) were added to refs/heads/main by this push:
     new e9c94e5  ARTEMIS-3288 support bulk user loading with basic security manager
     new 8d0f2f8  This closes #3571
e9c94e5 is described below

commit e9c94e57d9e2f6bb72f6217e9559caea2f292cab
Author: Justin Bertram <jb...@apache.org>
AuthorDate: Fri May 7 21:31:47 2021 -0500

    ARTEMIS-3288 support bulk user loading with basic security manager
---
 .../activemq/artemis/utils/ClassloadingUtil.java   |  4 ++
 .../security/ActiveMQBasicSecurityManager.java     | 54 +++++++++++++----
 docs/user-manual/en/security.md                    | 69 +++++++++++++++++-----
 .../security/BasicSecurityManagerTest.java         | 27 ++++++++-
 4 files changed, 127 insertions(+), 27 deletions(-)

diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ClassloadingUtil.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ClassloadingUtil.java
index 99c39e0..b56e3a0 100644
--- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ClassloadingUtil.java
+++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/ClassloadingUtil.java
@@ -122,6 +122,10 @@ public final class ClassloadingUtil {
       return (String)properties.get(name);
    }
 
+   public static Properties loadProperties(String propertiesFile) {
+      return loadProperties(ClassloadingUtil.class.getClassLoader(), propertiesFile);
+   }
+
    public static Properties loadProperties(ClassLoader loader, String propertiesFile) {
       Properties properties = new Properties();
 
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQBasicSecurityManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQBasicSecurityManager.java
index 7964134..5bc3ddf 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQBasicSecurityManager.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQBasicSecurityManager.java
@@ -22,6 +22,7 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 
 import org.apache.activemq.artemis.core.persistence.StorageManager;
@@ -35,6 +36,7 @@ import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
 import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
 import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
 import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
+import org.apache.activemq.artemis.utils.ClassloadingUtil;
 import org.apache.activemq.artemis.utils.SecurityManagerUtil;
 import org.jboss.logging.Logger;
 
@@ -48,6 +50,8 @@ public class ActiveMQBasicSecurityManager implements ActiveMQSecurityManager5, U
    public static final String BOOTSTRAP_USER = "bootstrapUser";
    public static final String BOOTSTRAP_PASSWORD = "bootstrapPassword";
    public static final String BOOTSTRAP_ROLE = "bootstrapRole";
+   public static final String BOOTSTRAP_USER_FILE = "bootstrapUserFile";
+   public static final String BOOTSTRAP_ROLE_FILE = "bootstrapRoleFile";
 
    private Map<String, String> properties;
    private String rolePrincipalClass = RolePrincipal.class.getName();
@@ -55,7 +59,7 @@ public class ActiveMQBasicSecurityManager implements ActiveMQSecurityManager5, U
 
    @Override
    public ActiveMQBasicSecurityManager init(Map<String, String> properties) {
-      if (!properties.containsKey(BOOTSTRAP_USER) || !properties.containsKey(BOOTSTRAP_PASSWORD) || !properties.containsKey(BOOTSTRAP_ROLE)) {
+      if ((!properties.containsKey(BOOTSTRAP_USER) || !properties.containsKey(BOOTSTRAP_PASSWORD) || !properties.containsKey(BOOTSTRAP_ROLE)) && (!properties.containsKey(BOOTSTRAP_USER_FILE) || !properties.containsKey(BOOTSTRAP_ROLE_FILE))) {
          ActiveMQServerLogger.LOGGER.noBootstrapCredentialsFound();
       } else {
          this.properties = properties;
@@ -180,18 +184,48 @@ public class ActiveMQBasicSecurityManager implements ActiveMQSecurityManager5, U
    public void completeInit(StorageManager storageManager) {
       this.storageManager = storageManager;
 
-      // add/update the bootstrap user now that the StorageManager is set
-      if (properties != null && properties.containsKey(BOOTSTRAP_USER) && properties.containsKey(BOOTSTRAP_PASSWORD) && properties.containsKey(BOOTSTRAP_ROLE)) {
-         try {
-            if (userExists(properties.get(BOOTSTRAP_USER))) {
-               updateUser(properties.get(BOOTSTRAP_USER), properties.get(BOOTSTRAP_PASSWORD), new String[]{properties.get(BOOTSTRAP_ROLE)});
-            } else {
-               addNewUser(properties.get(BOOTSTRAP_USER), properties.get(BOOTSTRAP_PASSWORD), new String[]{properties.get(BOOTSTRAP_ROLE)});
+      // add/update the bootstrap credentials now that the StorageManager is set
+      if (properties != null && properties.containsKey(BOOTSTRAP_USER_FILE) && properties.containsKey(BOOTSTRAP_ROLE_FILE)) {
+         Properties users = ClassloadingUtil.loadProperties(properties.get(BOOTSTRAP_USER_FILE));
+         Map<String, Set<String>> rolesByUser = invertProperties(ClassloadingUtil.loadProperties(properties.get(BOOTSTRAP_ROLE_FILE)));
+         for (String user : users.stringPropertyNames()) {
+            addOrUpdateUser(user, users.getProperty(user), rolesByUser.get(user).toArray(new String[0]));
+         }
+      } else if (properties != null && properties.containsKey(BOOTSTRAP_USER) && properties.containsKey(BOOTSTRAP_PASSWORD) && properties.containsKey(BOOTSTRAP_ROLE)) {
+         addOrUpdateUser(properties.get(BOOTSTRAP_USER), properties.get(BOOTSTRAP_PASSWORD), new String[]{properties.get(BOOTSTRAP_ROLE)});
+      }
+   }
+
+   private void addOrUpdateUser(String user, String password, String... roles) {
+      try {
+         if (userExists(user)) {
+            updateUser(user, password, roles);
+         } else {
+            addNewUser(user, password, roles);
+         }
+      } catch (Exception e) {
+         ActiveMQServerLogger.LOGGER.failedToCreateBootstrapCredentials(e, user);
+      }
+   }
+
+   /*
+    * The roles properties file used by the PropertiesLoginModule is in the format role=user1,user2. We need to change
+    * that so we can look up a user and get their roles instead.
+    */
+   private Map<String, Set<String>> invertProperties(Properties props) {
+      Map<String, Set<String>> invertedProps = new HashMap<>();
+
+      for (Map.Entry<Object, Object> val : props.entrySet()) {
+         for (String user : ((String) val.getValue()).split(",")) {
+            Set<String> tempRoles = invertedProps.get(user);
+            if (tempRoles == null) {
+               tempRoles = new HashSet<>();
+               invertedProps.put(user, tempRoles);
             }
-         } catch (Exception e) {
-            ActiveMQServerLogger.LOGGER.failedToCreateBootstrapCredentials(e, properties.get(BOOTSTRAP_USER));
+            tempRoles.add((String) val.getKey());
          }
       }
+      return invertedProps;
    }
 
    private boolean userExists(String user) {
diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md
index 2c78558..3b84a05 100644
--- a/docs/user-manual/en/security.md
+++ b/docs/user-manual/en/security.md
@@ -1160,7 +1160,28 @@ the basic security manager.
 
 The configuration for the `ActiveMQBasicSecurityManager` happens in
 `bootstrap.xml` just like it does for all security manager implementations.
-Here's an example:
+
+The `ActiveMQBasicSecurityManager` requires some special configuration for the
+following reasons:
+
+ - the bindings data which holds the user & role data cannot be modified
+   manually 
+ - the broker must be running to manage users
+ - the broker often needs to be secured from first boot the
+
+If, for example, the broker was configured to use the 
+`ActiveMQBasicSecurityManager` and was started from scratch then no clients
+would be able to connect because there would be no users & roles configured.
+However, in order to configure users & roles one would need to use the 
+management API which would require the proper credentials. It's a catch-22.
+Therefore, it is possible to configure "bootstrap" credentials that will be
+automatically created when the broker starts. There are properties to define
+either:
+
+ - a single user whose credentials can then be used to add other users 
+ - properties files from which to load users & roles in bulk
+
+Here's an example of the single bootstrap user configuration:
 
 ```xml
 <broker xmlns="http://activemq.org/schema">
@@ -1175,18 +1196,39 @@ Here's an example:
 </broker>
 ```
 
-Because the bindings data which holds the user & role data cannot be modified
-manually and because the broker must be running to manage users and because
-the broker often needs to be secured from first boot the
-`ActiveMQBasicSecurityManager` has 3 properties to define a user whose
-credentials can then be used to add other users.
+- `bootstrapUser` - The name of the bootstrap user.
+- `bootstrapPassword` - The password for the bootstrap user; supports masking.
+- `bootstrapRole` - The role of the bootstrap user.
+
+If your use-case requires *multiple* users to be available when the broker
+starts then you can use a configuration like this:
 
-- `bootstrapUser` - the name of the bootstrap user
-- `bootstrapPassword` - the password for the bootstrap user; supports masking
-- `bootstrapRole` - the role of the bootstrap user
+```xml
+<broker xmlns="http://activemq.org/schema">
+
+   <security-manager class-name="org.apache.activemq.artemis.spi.core.security.ActiveMQBasicSecurityManager">
+      <property key="bootstrapUserFile" value="artemis-users.properties"/>
+      <property key="bootstrapRoleFile" value="artemis-roles.properties"/>
+   </security-manager>
+   
+   ...
+</broker>
+```
 
-The value specified in the `bootstrapRole` will need the following permissions
-on the `activemq.management` address:
+- `bootstrapUserFile` - The name of the file from which to load users. This is
+  a *properties* file formatted exactly the same as the user properties file 
+  used by the [`PropertiesLoginModule`](#propertiesloginmodule). This file 
+  should be on the broker's classpath (e.g. in the `etc` directory).
+- `bootstrapRoleFile` - The role of the bootstrap user. This is a *properties*
+  file formatted exactly the same as the role properties file used by the
+  [`PropertiesLoginModule`](#propertiesloginmodule). This file should be on the
+  broker's classpath (e.g. in the `etc` directory).
+
+Regardless of whether you configure a single bootstrap user or load many users
+from properties files, any user with which additional users are created should
+be in a role with the appropriate permissions on the `activemq.management` 
+address. For example if you've specified a `bootstrapUser` then the
+`bootstrapRole` will need the following permissions:
 
 - `createNonDurableQueue`
 - `createAddress`
@@ -1208,9 +1250,8 @@ For example:
 
 > **Note:**
 >
-> If the 3 `bootstrap` properties are defined then those credentials will be
-> set whenever you start the broker no matter what changes may have been made
-> to them at runtime previously.
+> Any `bootstrap` credentials will be set **whenever** you start the broker no 
+> matter what changes may have been made to them at runtime previously.
 
 ## Mapping external roles
 Roles from external authentication providers (i.e. LDAP) can be mapped to internally used roles. The is done through role-mapping entries in the security-settings block:
diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerTest.java
index 79855a0..e4ea013 100644
--- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerTest.java
+++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerTest.java
@@ -17,6 +17,8 @@
 package org.apache.activemq.artemis.tests.integration.security;
 
 import java.lang.management.ManagementFactory;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -41,10 +43,24 @@ import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class BasicSecurityManagerTest extends ActiveMQTestBase {
 
    private ServerLocator locator;
+   private boolean bootstrapProperties;
+
+   public BasicSecurityManagerTest(boolean bootstrapProperties) {
+      this.bootstrapProperties = bootstrapProperties;
+   }
+
+   @Parameterized.Parameters(name = "bootstrapProperties={0}")
+   public static Collection<Object[]> data() {
+      Object[][] params = new Object[][]{{false}, {true}};
+      return Arrays.asList(params);
+   }
 
    @Override
    @Before
@@ -56,9 +72,14 @@ public class BasicSecurityManagerTest extends ActiveMQTestBase {
 
    public ActiveMQServer initializeServer() throws Exception {
       Map<String, String> initProperties = new HashMap<>();
-      initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_USER, "first");
-      initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_PASSWORD, "secret");
-      initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_ROLE, "programmers");
+      if (bootstrapProperties) {
+         initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_USER_FILE, "users.properties");
+         initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_ROLE_FILE, "roles.properties");
+      } else {
+         initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_USER, "first");
+         initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_PASSWORD, "secret");
+         initProperties.put(ActiveMQBasicSecurityManager.BOOTSTRAP_ROLE, "programmers");
+      }
       ActiveMQBasicSecurityManager securityManager = new ActiveMQBasicSecurityManager().init(initProperties);
       ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, true));
       return server;