From e9c94e57d9e2f6bb72f6217e9559caea2f292cab Mon Sep 17 00:00:00 2001 From: Justin Bertram Date: Fri, 7 May 2021 21:31:47 -0500 Subject: [PATCH] ARTEMIS-3288 support bulk user loading with basic security manager --- .../artemis/utils/ClassloadingUtil.java | 4 ++ .../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 99c39e0419..b56e3a09b8 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 7964134abb..5bc3ddf53a 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 properties; private String rolePrincipalClass = RolePrincipal.class.getName(); @@ -55,7 +59,7 @@ public class ActiveMQBasicSecurityManager implements ActiveMQSecurityManager5, U @Override public ActiveMQBasicSecurityManager init(Map 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> 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> invertProperties(Properties props) { + Map> invertedProps = new HashMap<>(); + + for (Map.Entry val : props.entrySet()) { + for (String user : ((String) val.getValue()).split(",")) { + Set 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 2c78558c5e..3b84a05888 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 @@ -1175,18 +1196,39 @@ Here's an example: ``` -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. -- `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: -The value specified in the `bootstrapRole` will need the following permissions -on the `activemq.management` address: +```xml + + + + + + + + ... + +``` + +- `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 79855a0535..e4ea01350f 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 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 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;