From 75e12b5e1d4d9e2322b84fea0c0a1e81981200ae Mon Sep 17 00:00:00 2001 From: Justin Bertram Date: Mon, 12 Oct 2020 14:40:36 -0500 Subject: [PATCH] ARTEMIS-2947 Implement SecurityManager that supports replication --- .../activemq/artemis/cli/commands/Create.java | 26 ++ .../activemq/artemis/cli/commands/Run.java | 10 +- .../cli/factory/jmx/ManagementFactory.java | 4 +- .../commands/etc/basic-security-manager.txt | 5 + .../artemis/cli/commands/etc/bootstrap.xml | 2 +- .../commands/etc/jaas-security-manager.txt | 1 + .../apache/activemq/cli/test/ArtemisTest.java | 177 +++++++--- .../artemis/utils/PasswordMaskingUtil.java | 14 +- .../management/ActiveMQServerControl.java | 18 +- .../activemq/artemis/dto/JMXConnectorDTO.java | 2 +- .../activemq/artemis/dto/WebServerDTO.java | 2 +- .../impl/ActiveMQServerControlImpl.java | 128 ++++--- .../core/persistence/StorageManager.java | 23 +- .../persistence/config/PersistedRole.java | 108 ++++++ ...les.java => PersistedSecuritySetting.java} | 30 +- .../persistence/config/PersistedUser.java | 86 +++++ .../AbstractJournalStorageManager.java | 120 ++++++- .../impl/journal/DescribeJournal.java | 12 +- .../impl/journal/JournalRecordIds.java | 6 +- .../impl/nullpm/NullStorageManager.java | 36 +- .../activemq/artemis/core/security/User.java | 11 +- .../core/server/ActiveMQMessageBundle.java | 3 + .../artemis/core/server/ActiveMQServer.java | 4 + .../core/server/ActiveMQServerLogger.java | 8 + .../core/server/impl/ActiveMQServerImpl.java | 28 +- .../server/management/BasicAuthenticator.java | 56 ++++ .../management/ManagementConnector.java | 23 +- .../server/management/ManagementContext.java | 16 +- .../ActiveMQBasicSecurityManager.java | 211 ++++++++++++ .../security/ActiveMQJAASSecurityManager.java | 108 +----- .../spi/core/security/UserManagement.java | 31 ++ .../core/security/jaas/LDAPLoginModule.java | 2 +- .../PropertiesLoginModuleConfigurator.java | 11 +- .../artemis/utils/SecurityManagerUtil.java | 138 ++++++++ .../transaction/impl/TransactionImplTest.java | 40 ++- docs/user-manual/en/security.md | 101 +++++- .../integration/client/SendAckFailTest.java | 46 ++- .../RolesConfigurationStorageTest.java | 24 +- .../BasicSecurityManagerFailoverTest.java | 164 +++++++++ .../security/BasicSecurityManagerTest.java | 312 ++++++++++++++++++ 40 files changed, 1835 insertions(+), 312 deletions(-) create mode 100644 artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-security-manager.txt create mode 100644 artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-security-manager.txt create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedRole.java rename artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/{PersistedRoles.java => PersistedSecuritySetting.java} (92%) create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedUser.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/BasicAuthenticator.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQBasicSecurityManager.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/UserManagement.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/utils/SecurityManagerUtil.java create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerFailoverTest.java create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerTest.java diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java index 5cb6e8bc67..aaaad9b622 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java @@ -107,6 +107,8 @@ public class Create extends InputAbstract { public static final String ETC_PING_TXT = "etc/ping-settings.txt"; public static final String ETC_COMMENTED_PING_TXT = "etc/commented-ping-settings.txt"; public static final String ETC_DATABASE_STORE_TXT = "etc/database-store.txt"; + public static final String ETC_JAAS_SECURITY_MANAGER_TXT = "etc/jaas-security-manager.txt"; + public static final String ETC_BASIC_SECURITY_MANAGER_TXT = "etc/basic-security-manager.txt"; public static final String ETC_GLOBAL_MAX_SPECIFIED_TXT = "etc/global-max-specified.txt"; public static final String ETC_GLOBAL_MAX_DEFAULT_TXT = "etc/global-max-default.txt"; @@ -289,6 +291,9 @@ public class Create extends InputAbstract { } } + @Option(name = "--security-manager", description = "Which security manager to use - jaas or basic (Default: jaas)") + private String securityManager = "jaas"; + @Option(name = "--jdbc-bindings-table-name", description = "Name of the jdbc bindigns table") private String jdbcBindings = ActiveMQDefaultConfiguration.getDefaultBindingsTableName(); @@ -821,6 +826,11 @@ public class Create extends InputAbstract { // we want this variable to remain unchanged so that it will use the value set in the profile filters.remove("${artemis.instance}"); + if (SecurityManagerType.getType(securityManager) == SecurityManagerType.BASIC) { + filters.put("${security-manager-settings}", readTextFile(ETC_BASIC_SECURITY_MANAGER_TXT, filters)); + } else { + filters.put("${security-manager-settings}", readTextFile(ETC_JAAS_SECURITY_MANAGER_TXT, filters)); + } writeEtc(ETC_BOOTSTRAP_XML, etcFolder, filters, false); writeEtc(ETC_MANAGEMENT_XML, etcFolder, filters, false); @@ -1193,4 +1203,20 @@ public class Create extends InputAbstract { c = is.read(buffer); } } + + private enum SecurityManagerType { + JAAS, BASIC; + + public static SecurityManagerType getType(String type) { + type = type.toLowerCase(); + switch (type) { + case "jaas": + return JAAS; + case "basic": + return BASIC; + default: + return null; + } + } + } } diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Run.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Run.java index 552f7ef3a0..4f0da7cb84 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Run.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Run.java @@ -72,17 +72,15 @@ public class Run extends LockAbstract { super.execute(context); try { + BrokerDTO broker = getBrokerDTO(); + ActiveMQSecurityManager securityManager = SecurityManagerFactory.create(broker.security); ManagementContextDTO managementDTO = getManagementDTO(); - managementContext = ManagementFactory.create(managementDTO); + managementContext = ManagementFactory.create(managementDTO, securityManager); Artemis.printBanner(); - BrokerDTO broker = getBrokerDTO(); - addShutdownHook(broker.server.getConfigurationFile().getParentFile()); - ActiveMQSecurityManager security = SecurityManagerFactory.create(broker.security); - ActivateCallback activateCallback = new ActivateCallback() { @Override public void preActivate() { @@ -109,7 +107,7 @@ public class Run extends LockAbstract { } }; - server = BrokerFactory.createServer(broker.server, security, activateCallback); + server = BrokerFactory.createServer(broker.server, securityManager, activateCallback); managementContext.start(); server.createComponents(); diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java index 7baa0f5a77..826dc8efb3 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java @@ -27,6 +27,7 @@ import org.apache.activemq.artemis.dto.JMXConnectorDTO; import org.apache.activemq.artemis.dto.ManagementContextDTO; import org.apache.activemq.artemis.dto.MatchDTO; import org.apache.activemq.artemis.core.server.management.JMXAccessControlList; +import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager; import org.apache.activemq.artemis.utils.FactoryFinder; import java.io.IOException; @@ -60,7 +61,7 @@ public class ManagementFactory { return createJmxAclConfiguration(new URI(configuration), artemisHome, artemisInstance, artemisURIInstance); } - public static ManagementContext create(ManagementContextDTO config) throws Exception { + public static ManagementContext create(ManagementContextDTO config, ActiveMQSecurityManager securityManager) throws Exception { ManagementContext context = new ManagementContext(); if (config.getAuthorisation() != null) { @@ -130,6 +131,7 @@ public class ManagementFactory { jmxConnectorConfiguration.setSecured(jmxConnector.isSecured()); } context.setJmxConnectorConfiguration(jmxConnectorConfiguration); + context.setSecurityManager(securityManager); } return context; diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-security-manager.txt b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-security-manager.txt new file mode 100644 index 0000000000..d7dfbd951a --- /dev/null +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-security-manager.txt @@ -0,0 +1,5 @@ + + + + + diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml index f919428c80..69e2d95291 100644 --- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml @@ -18,7 +18,7 @@ - +${security-manager-settings} diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-security-manager.txt b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-security-manager.txt new file mode 100644 index 0000000000..2a0260ae87 --- /dev/null +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-security-manager.txt @@ -0,0 +1 @@ + diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java index 5269e647d5..dab321abf5 100644 --- a/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java +++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java @@ -105,6 +105,9 @@ import static org.junit.Assert.fail; public class ArtemisTest extends CliTestBase { private static final Logger log = Logger.getLogger(ArtemisTest.class); + // some tests will set this, as some security methods will need to know the server the CLI started + private ActiveMQServer server; + @Before @Override public void setup() throws Exception { @@ -340,13 +343,23 @@ public class ArtemisTest extends CliTestBase { } @Test - public void testUserCommand() throws Exception { + public void testUserCommandJAAS() throws Exception { + testUserCommand(false); + } + + @Test + public void testUserCommandBasic() throws Exception { + testUserCommand(true); + } + + private void testUserCommand(boolean basic) 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", "--require-login"); + Artemis.main("create", instance1.getAbsolutePath(), "--silent", "--no-autotune", "--no-web", "--require-login", "--security-manager", basic ? "basic" : "jaas"); System.setProperty("artemis.instance", instance1.getAbsolutePath()); - Artemis.internalExecute("run"); + Object runResult = Artemis.internalExecute("run"); + server = ((Pair)runResult).getB(); try { File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties"); @@ -363,7 +376,7 @@ public class ArtemisTest extends CliTestBase { //default only one user admin with role amq assertTrue(result.contains("\"admin\"(amq)")); - checkRole("admin", roleFile, "amq"); + checkRole("admin", roleFile, basic, "amq"); //add a simple user AddUser addCmd = new AddUser(); @@ -383,8 +396,8 @@ public class ArtemisTest extends CliTestBase { assertTrue(result.contains("\"admin\"(amq)")); assertTrue(result.contains("\"guest\"(admin)")); - checkRole("guest", roleFile, "admin"); - assertTrue(checkPassword("guest", "guest123", userFile)); + checkRole("guest", roleFile, basic, "admin"); + assertTrue(checkPassword("guest", "guest123", userFile, basic)); //add a user with 2 roles addCmd = new AddUser(); @@ -405,8 +418,8 @@ public class ArtemisTest extends CliTestBase { assertTrue(result.contains("\"guest\"(admin)")); assertTrue(result.contains("\"scott\"(admin,operator)")); - checkRole("scott", roleFile, "admin", "operator"); - assertTrue(checkPassword("scott", "tiger", userFile)); + checkRole("scott", roleFile, basic, "admin", "operator"); + assertTrue(checkPassword("scott", "tiger", userFile, basic)); //add an existing user addCmd = new AddUser(); @@ -504,24 +517,34 @@ public class ArtemisTest extends CliTestBase { } @Test - public void testUserCommandViaManagementPlaintext() throws Exception { - internalTestUserCommandViaManagement(true); + public void testUserCommandViaManagementPlaintextJAAS() throws Exception { + internalTestUserCommandViaManagement(true, false); } @Test - public void testUserCommandViaManagementHashed() throws Exception { - internalTestUserCommandViaManagement(false); + public void testUserCommandViaManagementHashedJAAS() throws Exception { + internalTestUserCommandViaManagement(false, false); } - private void internalTestUserCommandViaManagement(boolean plaintext) throws Exception { + @Test + public void testUserCommandViaManagementPlaintextBasic() throws Exception { + internalTestUserCommandViaManagement(true, true); + } + + @Test + public void testUserCommandViaManagementHashedBasic() throws Exception { + internalTestUserCommandViaManagement(false, true); + } + + private void internalTestUserCommandViaManagement(boolean plaintext, boolean basic) 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"); + Artemis.main("create", instance1.getAbsolutePath(), "--silent", "--no-autotune", "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor", "--security-manager", basic ? "basic" : "jaas"); System.setProperty("artemis.instance", instance1.getAbsolutePath()); Object result = Artemis.internalExecute("run"); - ActiveMQServer activeMQServer = ((Pair)result).getB(); - ActiveMQServerControl activeMQServerControl = activeMQServer.getActiveMQServerControl(); + server = ((Pair)result).getB(); + ActiveMQServerControl activeMQServerControl = server.getActiveMQServerControl(); File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties"); File roleFile = new File(instance1.getAbsolutePath() + "/etc/artemis-roles.properties"); @@ -529,7 +552,7 @@ public class ArtemisTest extends CliTestBase { //default only one user admin with role amq String jsonResult = activeMQServerControl.listUser(""); contains(JsonUtil.readJsonArray(jsonResult), "admin", "amq"); - checkRole("admin", roleFile, "amq"); + checkRole("admin", roleFile, basic, "amq"); //add a simple user activeMQServerControl.addUser("guest", "guest123", "admin", plaintext); @@ -537,9 +560,9 @@ public class ArtemisTest extends CliTestBase { //verify add jsonResult = activeMQServerControl.listUser(""); contains(JsonUtil.readJsonArray(jsonResult), "guest", "admin"); - checkRole("guest", roleFile, "admin"); - assertTrue(checkPassword("guest", "guest123", userFile)); - assertEquals(plaintext, !PasswordMaskingUtil.isEncMasked(getStoredPassword("guest", userFile))); + checkRole("guest", roleFile, basic, "admin"); + assertTrue(checkPassword("guest", "guest123", userFile, basic)); + assertEquals(plaintext, !PasswordMaskingUtil.isEncMasked(getStoredPassword("guest", userFile, basic))); //add a user with 2 roles activeMQServerControl.addUser("scott", "tiger", "admin,operator", plaintext); @@ -548,9 +571,9 @@ public class ArtemisTest extends CliTestBase { jsonResult = activeMQServerControl.listUser(""); contains(JsonUtil.readJsonArray(jsonResult), "scott", "admin"); contains(JsonUtil.readJsonArray(jsonResult), "scott", "operator"); - checkRole("scott", roleFile, "admin", "operator"); - assertTrue(checkPassword("scott", "tiger", userFile)); - assertEquals(plaintext, !PasswordMaskingUtil.isEncMasked(getStoredPassword("scott", userFile))); + checkRole("scott", roleFile, basic, "admin", "operator"); + assertTrue(checkPassword("scott", "tiger", userFile, basic)); + assertEquals(plaintext, !PasswordMaskingUtil.isEncMasked(getStoredPassword("scott", userFile, basic))); try { activeMQServerControl.addUser("scott", "password", "visitor", plaintext); @@ -615,11 +638,20 @@ public class ArtemisTest extends CliTestBase { } @Test - public void testProperReloadWhenAddingUserViaManagement() throws Exception { + public void testProperReloadWhenAddingUserViaManagementJAAS() throws Exception { + testProperReloadWhenAddingUserViaManagement(false); + } + + @Test + public void testProperReloadWhenAddingUserViaManagementBasic() throws Exception { + testProperReloadWhenAddingUserViaManagement(true); + } + + private void testProperReloadWhenAddingUserViaManagement(boolean basic) 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", "--require-login"); + Artemis.main("create", instance1.getAbsolutePath(), "--silent", "--no-autotune", "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor", "--require-login", "--security-manager", basic ? "basic" : "jaas"); System.setProperty("artemis.instance", instance1.getAbsolutePath()); Object result = Artemis.internalExecute("run"); ActiveMQServer activeMQServer = ((Pair)result).getB(); @@ -702,13 +734,23 @@ public class ArtemisTest extends CliTestBase { } @Test - public void testUserCommandReset() throws Exception { + public void testUserCommandResetJAAS() throws Exception { + testUserCommandReset(false); + } + + @Test + public void testUserCommandResetBasic() throws Exception { + testUserCommandReset(true); + } + + private void testUserCommandReset(boolean basic) 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", "--require-login"); + Artemis.main("create", instance1.getAbsolutePath(), "--silent", "--no-autotune", "--no-web", "--require-login", "--security-manager", basic ? "basic" : "jaas"); System.setProperty("artemis.instance", instance1.getAbsolutePath()); - Artemis.internalExecute("run"); + Object runResult = Artemis.internalExecute("run"); + server = ((Pair)runResult).getB(); try { File userFile = new File(instance1.getAbsolutePath() + "/etc/artemis-users.properties"); @@ -754,7 +796,7 @@ public class ArtemisTest extends CliTestBase { addCmd.setUserCommandPassword("password1"); addCmd.setRole("admin,manager"); addCmd.execute(new TestActionContext()); - assertTrue(checkPassword("user1", "password1", userFile)); + assertTrue(checkPassword("user1", "password1", userFile, basic)); addCmd.setUserCommandUser("user2"); addCmd.setUserCommandPassword("password2"); @@ -787,7 +829,7 @@ public class ArtemisTest extends CliTestBase { .matcher(result) .find()); - checkRole("user1", roleFile, "admin", "manager"); + checkRole("user1", roleFile, basic, "admin", "manager"); //reset password context = new TestActionContext(); @@ -798,16 +840,16 @@ public class ArtemisTest extends CliTestBase { resetCommand.setPassword("admin"); resetCommand.execute(context); - checkRole("user1", roleFile, "admin", "manager"); - assertFalse(checkPassword("user1", "password1", userFile)); - assertTrue(checkPassword("user1", "newpassword1", userFile)); + checkRole("user1", roleFile, basic, "admin", "manager"); + assertFalse(checkPassword("user1", "password1", userFile, basic)); + assertTrue(checkPassword("user1", "newpassword1", userFile, basic)); //reset role resetCommand.setUserCommandUser("user2"); resetCommand.setRole("manager,master,operator"); resetCommand.execute(new TestActionContext()); - checkRole("user2", roleFile, "manager", "master", "operator"); + checkRole("user2", roleFile, basic, "manager", "master", "operator"); //reset both resetCommand.setUserCommandUser("user3"); @@ -815,19 +857,28 @@ public class ArtemisTest extends CliTestBase { resetCommand.setRole("admin,system"); resetCommand.execute(new ActionContext()); - checkRole("user3", roleFile, "admin", "system"); - assertTrue(checkPassword("user3", "newpassword3", userFile)); + checkRole("user3", roleFile, basic, "admin", "system"); + assertTrue(checkPassword("user3", "newpassword3", userFile, basic)); } finally { stopServer(); } } @Test - public void testConcurrentUserAdministration() throws Exception { + public void testConcurrentUserAdministrationJAAS() throws Exception { + testConcurrentUserAdministration(false); + } + + @Test + public void testConcurrentUserAdministrationBasic() throws Exception { + testConcurrentUserAdministration(true); + } + + private void testConcurrentUserAdministration(boolean basic) 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", "--require-login"); + Artemis.main("create", instance1.getAbsolutePath(), "--silent", "--no-autotune", "--no-web", "--require-login", "--security-manager", basic ? "basic" : "jaas"); System.setProperty("artemis.instance", instance1.getAbsolutePath()); Artemis.internalExecute("run"); @@ -1711,28 +1762,52 @@ public class ArtemisTest extends CliTestBase { } private void checkRole(String user, File roleFile, String... roles) throws Exception { - Configurations configs = new Configurations(); - FileBasedConfigurationBuilder roleBuilder = configs.propertiesBuilder(roleFile); - PropertiesConfiguration roleConfig = roleBuilder.getConfiguration(); + checkRole(user, roleFile, false, roles); + } - for (String r : roles) { - String storedUsers = (String) roleConfig.getProperty(r); + private void checkRole(String user, File roleFile, boolean basicSecurityManager, String... roles) throws Exception { + if (basicSecurityManager) { + for (String r : roles) { + assertTrue(server.getStorageManager().getPersistedRoles().get(user).getRoles().contains(r)); + } + } else { + Configurations configs = new Configurations(); + FileBasedConfigurationBuilder roleBuilder = configs.propertiesBuilder(roleFile); + PropertiesConfiguration roleConfig = roleBuilder.getConfiguration(); - log.debug("users in role: " + r + " ; " + storedUsers); - List userList = StringUtil.splitStringList(storedUsers, ","); - assertTrue(userList.contains(user)); + for (String r : roles) { + String storedUsers = (String) roleConfig.getProperty(r); + + log.debug("users in role: " + r + " ; " + storedUsers); + List userList = StringUtil.splitStringList(storedUsers, ","); + assertTrue(userList.contains(user)); + } } } private String getStoredPassword(String user, File userFile) throws Exception { - Configurations configs = new Configurations(); - FileBasedConfigurationBuilder userBuilder = configs.propertiesBuilder(userFile); - PropertiesConfiguration userConfig = userBuilder.getConfiguration(); - return (String) userConfig.getProperty(user); + return getStoredPassword(user, userFile, false); + } + + private String getStoredPassword(String user, File userFile, boolean basicSecurityManager) throws Exception { + String result; + if (basicSecurityManager) { + result = server.getStorageManager().getPersistedUsers().get(user).getPassword(); + } else { + Configurations configs = new Configurations(); + FileBasedConfigurationBuilder userBuilder = configs.propertiesBuilder(userFile); + PropertiesConfiguration userConfig = userBuilder.getConfiguration(); + result = (String) userConfig.getProperty(user); + } + return result; } private boolean checkPassword(String user, String password, File userFile) throws Exception { - String storedPassword = getStoredPassword(user, userFile); + return checkPassword(user, password, userFile,false); + } + + private boolean checkPassword(String user, String password, File userFile, boolean basicSecurityManager) throws Exception { + String storedPassword = getStoredPassword(user, userFile, basicSecurityManager); HashProcessor processor = PasswordMaskingUtil.getHashProcessor(storedPassword); return processor.compare(password.toCharArray(), storedPassword); } diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java index e9df0b263e..4c64a450f5 100644 --- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java @@ -35,6 +35,18 @@ public final class PasswordMaskingUtil { } + /** + * This method deals with password masking and returns the password in its plain text form. + * @param password : the original value of password string; interpreted as masked if wrapped in ENC() + * or as plain text otherwise. + * @param codecClass : the codec used to decode the password. Only when the password is interpreted + * as masked will this codec be used. Ignored otherwise. + * @return + */ + public static String resolveMask(String password, String codecClass) throws Exception { + return resolveMask(null, password, codecClass); + } + /** * This method deals with password masking and returns the password in its plain text form. * @param maskPassword : explicit mask flag. If it's true, the password is interpreted as @@ -109,7 +121,7 @@ public final class PasswordMaskingUtil { } //stored password takes 2 forms, ENC() or plain text - public static HashProcessor getHashProcessor(String storedPassword) throws Exception { + public static HashProcessor getHashProcessor(String storedPassword) { if (!isEncoded(storedPassword)) { return LazyPlainTextProcessorHolder.INSTANCE; diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQServerControl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQServerControl.java index ae8e2668cc..5a22730989 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQServerControl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQServerControl.java @@ -1830,45 +1830,45 @@ public interface ActiveMQServerControl { * @param roles * @throws Exception */ - @Operation(desc = "add a user (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION) + @Operation(desc = "add a user (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager)", 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", desc = "User's role (comma separated)") String roles, @Parameter(name = "plaintext", desc = "whether or not to store the password in plaintext or hash it") boolean plaintext) throws Exception; /** - * List the information about a user or all users if no username is supplied (only applicable when using the JAAS PropertiesLoginModule). + * List the information about a user or all users if no username is supplied (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager). * * @param username * @return JSON array of user and 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) + @Operation(desc = "list info about a user or all users if no username is supplied (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager)", impact = MBeanOperationInfo.ACTION) String listUser(@Parameter(name = "username", desc = "Name of the user; leave null to list all known users") String username) throws Exception; /** - * Remove a user (only applicable when using the JAAS PropertiesLoginModule). + * Remove a user (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager). * * @param username * @throws Exception */ - @Operation(desc = "remove a user (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION) + @Operation(desc = "remove a user (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager)", impact = MBeanOperationInfo.ACTION) void removeUser(@Parameter(name = "username", desc = "Name of the user") String username) throws Exception; /** - * Set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule). + * Set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager). * * @param username * @param password * @param roles * @throws Exception */ - @Operation(desc = "set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION) + @Operation(desc = "set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager)", 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", desc = "User's role (comma separated)") String roles) throws Exception; /** - * Set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule). + * Set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager). * * @param username * @param password @@ -1877,7 +1877,7 @@ public interface ActiveMQServerControl { * @throws Exception */ - @Operation(desc = "set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule)", impact = MBeanOperationInfo.ACTION) + @Operation(desc = "set new properties on an existing user (only applicable when using the JAAS PropertiesLoginModule or the ActiveMQBasicSecurityManager)", 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", desc = "User's role (comma separated)") String roles, diff --git a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java index 965de9392d..48055510a9 100644 --- a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java +++ b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java @@ -123,6 +123,6 @@ public class JMXConnectorDTO { } private String getPassword(String password) throws Exception { - return PasswordMaskingUtil.resolveMask(null, password, this.passwordCodec); + return PasswordMaskingUtil.resolveMask(password, this.passwordCodec); } } diff --git a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java index 4cc627a151..749208545f 100644 --- a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java +++ b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java @@ -83,7 +83,7 @@ public class WebServerDTO extends ComponentDTO { } private String getPassword(String password) throws Exception { - return PasswordMaskingUtil.resolveMask(null, password, this.passwordCodec); + return PasswordMaskingUtil.resolveMask(password, this.passwordCodec); } public void setKeyStorePassword(String keyStorePassword) { diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java index 92608cc8e1..758a8b78af 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java @@ -83,7 +83,7 @@ import org.apache.activemq.artemis.core.messagecounter.impl.MessageCounterManage import org.apache.activemq.artemis.core.persistence.StorageManager; import org.apache.activemq.artemis.core.persistence.config.PersistedAddressSetting; import org.apache.activemq.artemis.core.persistence.config.PersistedDivertConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; import org.apache.activemq.artemis.core.postoffice.Binding; import org.apache.activemq.artemis.core.postoffice.Bindings; import org.apache.activemq.artemis.core.postoffice.DuplicateIDCache; @@ -96,9 +96,9 @@ import org.apache.activemq.artemis.core.security.impl.SecurityStoreImpl; import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; import org.apache.activemq.artemis.core.server.ConnectorServiceFactory; import org.apache.activemq.artemis.core.server.Consumer; -import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; import org.apache.activemq.artemis.core.server.Divert; import org.apache.activemq.artemis.core.server.JournalType; import org.apache.activemq.artemis.core.server.Queue; @@ -127,6 +127,7 @@ import org.apache.activemq.artemis.core.transaction.impl.CoreTransactionDetail; import org.apache.activemq.artemis.core.transaction.impl.XidImpl; import org.apache.activemq.artemis.logs.AuditLogger; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; +import org.apache.activemq.artemis.spi.core.security.ActiveMQBasicSecurityManager; import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModuleConfigurator; import org.apache.activemq.artemis.utils.JsonLoader; import org.apache.activemq.artemis.utils.ListUtil; @@ -2784,9 +2785,9 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active server.getSecurityRepository().addMatch(addressMatch, roles); - PersistedRoles persistedRoles = new PersistedRoles(addressMatch, sendRoles, consumeRoles, createDurableQueueRoles, deleteDurableQueueRoles, createNonDurableQueueRoles, deleteNonDurableQueueRoles, manageRoles, browseRoles, createAddressRoles, deleteAddressRoles); + PersistedSecuritySetting persistedRoles = new PersistedSecuritySetting(addressMatch, sendRoles, consumeRoles, createDurableQueueRoles, deleteDurableQueueRoles, createNonDurableQueueRoles, deleteNonDurableQueueRoles, manageRoles, browseRoles, createAddressRoles, deleteAddressRoles); - storageManager.storeSecurityRoles(persistedRoles); + storageManager.storeSecuritySetting(persistedRoles); } finally { blockOnIO(); } @@ -2802,7 +2803,7 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active clearIO(); try { server.getSecurityRepository().removeMatch(addressMatch); - storageManager.deleteSecurityRoles(new SimpleString(addressMatch)); + storageManager.deleteSecuritySetting(new SimpleString(addressMatch)); } finally { blockOnIO(); } @@ -4189,15 +4190,22 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active @Override public void addUser(String username, String password, String roles, boolean plaintext) throws Exception { - synchronized (userLock) { - if (AuditLogger.isEnabled()) { - AuditLogger.addUser(this.server, username, "****", roles, plaintext); + if (AuditLogger.isEnabled()) { + AuditLogger.addUser(this.server, username, "****", roles, plaintext); + } + + String passwordToUse = plaintext ? password : PasswordMaskingUtil.getHashProcessor().hash(password); + + if (server.getSecurityManager() instanceof ActiveMQBasicSecurityManager) { + ((ActiveMQBasicSecurityManager) server.getSecurityManager()).addNewUser(username, passwordToUse, roles.split(",")); + } else { + synchronized (userLock) { + tcclInvoke(ActiveMQServerControlImpl.class.getClassLoader(), () -> { + PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(); + config.addNewUser(username, passwordToUse, roles.split(",")); + config.save(); + }); } - tcclInvoke(ActiveMQServerControlImpl.class.getClassLoader(), () -> { - PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(); - config.addNewUser(username, plaintext ? password : PasswordMaskingUtil.getHashProcessor().hash(password), roles.split(",")); - config.save(); - }); } } @@ -4207,58 +4215,72 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active @Override public String listUser(String username) throws Exception { - synchronized (userLock) { - if (AuditLogger.isEnabled()) { - AuditLogger.listUser(this.server, username); - } - - return (String) tcclCall(ActiveMQServerControlImpl.class.getClassLoader(), () -> { - PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(); - Map> info = config.listUser(username); - JsonArrayBuilder users = JsonLoader.createArrayBuilder(); - for (Entry> 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(); - }); + if (AuditLogger.isEnabled()) { + AuditLogger.listUser(this.server, username); } + if (server.getSecurityManager() instanceof ActiveMQBasicSecurityManager) { + return buildJsonUserList(((ActiveMQBasicSecurityManager) server.getSecurityManager()).listUser(username)); + } else { + synchronized (userLock) { + return (String) tcclCall(ActiveMQServerControlImpl.class.getClassLoader(), () -> { + return buildJsonUserList(getPropertiesLoginModuleConfigurator().listUser(username)); + }); + } + } + } + + private String buildJsonUserList(Map> info) { + JsonArrayBuilder users = JsonLoader.createArrayBuilder(); + for (Entry> 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) throws Exception { - synchronized (userLock) { - if (AuditLogger.isEnabled()) { - AuditLogger.removeUser(this.server, username); + if (AuditLogger.isEnabled()) { + AuditLogger.removeUser(this.server, username); + } + if (server.getSecurityManager() instanceof ActiveMQBasicSecurityManager) { + ((ActiveMQBasicSecurityManager) server.getSecurityManager()).removeUser(username); + } else { + synchronized (userLock) { + tcclInvoke(ActiveMQServerControlImpl.class.getClassLoader(), () -> { + PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(); + config.removeUser(username); + config.save(); + }); } - tcclInvoke(ActiveMQServerControlImpl.class.getClassLoader(), () -> { - PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(); - config.removeUser(username); - config.save(); - }); } } @Override public void resetUser(String username, String password, String roles, boolean plaintext) throws Exception { - synchronized (userLock) { - if (AuditLogger.isEnabled()) { - AuditLogger.resetUser(this.server, username, "****", roles, plaintext); + if (AuditLogger.isEnabled()) { + AuditLogger.resetUser(this.server, username, "****", roles, plaintext); + } + + String passwordToUse = password == null ? password : plaintext ? password : PasswordMaskingUtil.getHashProcessor().hash(password); + + if (server.getSecurityManager() instanceof ActiveMQBasicSecurityManager) { + ((ActiveMQBasicSecurityManager) server.getSecurityManager()).updateUser(username, passwordToUse, roles == null ? null : roles.split(",")); + } else { + synchronized (userLock) { + tcclInvoke(ActiveMQServerControlImpl.class.getClassLoader(), () -> { + PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(); + // don't hash a null password even if plaintext = false + config.updateUser(username, passwordToUse, roles == null ? null : roles.split(",")); + config.save(); + }); } - tcclInvoke(ActiveMQServerControlImpl.class.getClassLoader(), () -> { - PropertiesLoginModuleConfigurator config = getPropertiesLoginModuleConfigurator(); - // don't hash a null password even if plaintext = false - config.updateUser(username, password == null ? password : plaintext ? password : PasswordMaskingUtil - .getHashProcessor() - .hash(password), roles == null ? null : roles.split(",")); - config.save(); - }); } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/StorageManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/StorageManager.java index 5ee4478d05..a35d2a934e 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/StorageManager.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/StorageManager.java @@ -41,7 +41,9 @@ import org.apache.activemq.artemis.core.paging.PagingStore; import org.apache.activemq.artemis.core.paging.cursor.PagePosition; import org.apache.activemq.artemis.core.persistence.config.PersistedAddressSetting; import org.apache.activemq.artemis.core.persistence.config.PersistedDivertConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedRole; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; +import org.apache.activemq.artemis.core.persistence.config.PersistedUser; import org.apache.activemq.artemis.core.persistence.impl.PageCountPending; import org.apache.activemq.artemis.core.postoffice.Binding; import org.apache.activemq.artemis.core.postoffice.PostOffice; @@ -354,17 +356,30 @@ public interface StorageManager extends IDGenerator, ActiveMQComponent { List recoverAddressSettings() throws Exception; - void storeSecurityRoles(PersistedRoles persistedRoles) throws Exception; + void storeSecuritySetting(PersistedSecuritySetting persistedRoles) throws Exception; - void deleteSecurityRoles(SimpleString addressMatch) throws Exception; + void deleteSecuritySetting(SimpleString addressMatch) throws Exception; - List recoverPersistedRoles() throws Exception; + List recoverSecuritySettings() throws Exception; void storeDivertConfiguration(PersistedDivertConfiguration persistedDivertConfiguration) throws Exception; void deleteDivertConfiguration(String divertName) throws Exception; List recoverDivertConfigurations(); + + void storeUser(PersistedUser persistedUser) throws Exception; + + void deleteUser(String username) throws Exception; + + Map getPersistedUsers(); + + void storeRole(PersistedRole persistedRole) throws Exception; + + void deleteRole(String role) throws Exception; + + Map getPersistedRoles(); + /** * @return The ID with the stored counter */ diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedRole.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedRole.java new file mode 100644 index 0000000000..1af8864731 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedRole.java @@ -0,0 +1,108 @@ +/* + * 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.core.persistence.config; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.activemq.artemis.api.core.ActiveMQBuffer; +import org.apache.activemq.artemis.core.journal.EncodingSupport; +import org.apache.activemq.artemis.utils.BufferHelper; +import org.apache.activemq.artemis.utils.DataConstants; + +public class PersistedRole implements EncodingSupport { + + private long storeId; + + private String username; + + private List roles; + + public PersistedRole() { + } + + public PersistedRole(String username, List roles) { + this.username = username; + this.roles = roles; + } + + public void setStoreId(long id) { + this.storeId = id; + } + + public long getStoreId() { + return storeId; + } + + public String getUsername() { + return username; + } + + public List getRoles() { + return roles; + } + + @Override + public int getEncodeSize() { + int size = 0; + size += BufferHelper.sizeOfString(username); + size += DataConstants.SIZE_INT; + for (String role : roles) { + size += BufferHelper.sizeOfString(role); + } + return size; + } + + @Override + public void encode(ActiveMQBuffer buffer) { + buffer.writeString(username); + buffer.writeInt(roles.size()); + for (String user : roles) { + buffer.writeString(user); + } + } + + @Override + public void decode(ActiveMQBuffer buffer) { + username = buffer.readString(); + roles = new ArrayList<>(); + int size = buffer.readInt(); + for (int i = 0; i < size; i++) { + roles.add(buffer.readString()); + } + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("PersistedRole [storeId=").append(storeId); + result.append(", username=").append(username); + result.append(", roles ["); + for (int i = 0; i < roles.size(); i++) { + result.append(roles.get(i)); + if (i < roles.size() - 1) { + result.append(", "); + } + } + result.append("]]"); + + return result.toString(); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedRoles.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedSecuritySetting.java similarity index 92% rename from artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedRoles.java rename to artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedSecuritySetting.java index 86fdc14d17..f175be4249 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedRoles.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedSecuritySetting.java @@ -20,7 +20,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.core.journal.EncodingSupport; -public class PersistedRoles implements EncodingSupport { +public class PersistedSecuritySetting implements EncodingSupport { // Constants ----------------------------------------------------- @@ -54,7 +54,7 @@ public class PersistedRoles implements EncodingSupport { // Constructors -------------------------------------------------- - public PersistedRoles() { + public PersistedSecuritySetting() { } /** @@ -70,17 +70,17 @@ public class PersistedRoles implements EncodingSupport { * @param createAddressRoles * @param deleteAddressRoles */ - public PersistedRoles(final String addressMatch, - final String sendRoles, - final String consumeRoles, - final String createDurableQueueRoles, - final String deleteDurableQueueRoles, - final String createNonDurableQueueRoles, - final String deleteNonDurableQueueRoles, - final String manageRoles, - final String browseRoles, - final String createAddressRoles, - final String deleteAddressRoles) { + public PersistedSecuritySetting(final String addressMatch, + final String sendRoles, + final String consumeRoles, + final String createDurableQueueRoles, + final String deleteDurableQueueRoles, + final String createNonDurableQueueRoles, + final String deleteNonDurableQueueRoles, + final String manageRoles, + final String browseRoles, + final String createAddressRoles, + final String deleteAddressRoles) { super(); this.addressMatch = SimpleString.toSimpleString(addressMatch); this.sendRoles = SimpleString.toSimpleString(sendRoles); @@ -259,7 +259,7 @@ public class PersistedRoles implements EncodingSupport { return false; if (getClass() != obj.getClass()) return false; - PersistedRoles other = (PersistedRoles) obj; + PersistedSecuritySetting other = (PersistedSecuritySetting) obj; if (addressMatch == null) { if (other.addressMatch != null) return false; @@ -325,7 +325,7 @@ public class PersistedRoles implements EncodingSupport { */ @Override public String toString() { - return "PersistedRoles [storeId=" + storeId + + return "PersistedSecuritySetting [storeId=" + storeId + ", addressMatch=" + addressMatch + ", sendRoles=" + diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedUser.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedUser.java new file mode 100644 index 0000000000..aef5dec3b8 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/config/PersistedUser.java @@ -0,0 +1,86 @@ +/* + * 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.core.persistence.config; + +import org.apache.activemq.artemis.api.core.ActiveMQBuffer; +import org.apache.activemq.artemis.core.journal.EncodingSupport; +import org.apache.activemq.artemis.utils.BufferHelper; + +public class PersistedUser implements EncodingSupport { + + private long storeId; + + private String username; + + private String password; + + public PersistedUser() { + } + + public PersistedUser(String username, String password) { + this.username = username; + this.password = password; + } + + public void setStoreId(long id) { + this.storeId = id; + } + + public long getStoreId() { + return storeId; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @Override + public int getEncodeSize() { + int size = 0; + size += BufferHelper.sizeOfString(username); + size += BufferHelper.sizeOfString(password); + return size; + } + + @Override + public void encode(ActiveMQBuffer buffer) { + buffer.writeString(username); + buffer.writeString(password); + } + + @Override + public void decode(ActiveMQBuffer buffer) { + username = buffer.readString(); + password = buffer.readString(); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "PersistedUser [storeId=" + storeId + + ", username=" + + username + + ", password=****" + + "]"; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/AbstractJournalStorageManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/AbstractJournalStorageManager.java index 1cb39ab790..daa795759c 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/AbstractJournalStorageManager.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/AbstractJournalStorageManager.java @@ -75,7 +75,9 @@ import org.apache.activemq.artemis.core.persistence.AddressQueueStatus; import org.apache.activemq.artemis.core.persistence.StorageManager; import org.apache.activemq.artemis.core.persistence.config.PersistedAddressSetting; import org.apache.activemq.artemis.core.persistence.config.PersistedDivertConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedRole; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; +import org.apache.activemq.artemis.core.persistence.config.PersistedUser; import org.apache.activemq.artemis.core.persistence.impl.PageCountPending; import org.apache.activemq.artemis.core.persistence.impl.journal.codec.AddressStatusEncoding; import org.apache.activemq.artemis.core.persistence.impl.journal.codec.CursorAckRecordEncoding; @@ -195,12 +197,16 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp protected final Configuration config; // Persisted core configuration - protected final Map mapPersistedRoles = new ConcurrentHashMap<>(); + protected final Map mapPersistedSecuritySettings = new ConcurrentHashMap<>(); protected final Map mapPersistedAddressSettings = new ConcurrentHashMap<>(); protected final Map mapPersistedDivertConfigurations = new ConcurrentHashMap<>(); + protected final Map mapPersistedUsers = new ConcurrentHashMap<>(); + + protected final Map mapPersistedRoles = new ConcurrentHashMap<>(); + protected final ConcurrentLongHashMap largeMessagesToDelete = new ConcurrentLongHashMap<>(); public AbstractJournalStorageManager(final Configuration config, @@ -767,20 +773,20 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp } @Override - public List recoverPersistedRoles() throws Exception { - return new ArrayList<>(mapPersistedRoles.values()); + public List recoverSecuritySettings() throws Exception { + return new ArrayList<>(mapPersistedSecuritySettings.values()); } @Override - public void storeSecurityRoles(PersistedRoles persistedRoles) throws Exception { + public void storeSecuritySetting(PersistedSecuritySetting persistedRoles) throws Exception { - deleteSecurityRoles(persistedRoles.getAddressMatch()); + deleteSecuritySetting(persistedRoles.getAddressMatch()); readLock(); try { final long id = idGenerator.generateID(); persistedRoles.setStoreId(id); - bindingsJournal.appendAddRecord(id, JournalRecordIds.SECURITY_RECORD, persistedRoles, true); - mapPersistedRoles.put(persistedRoles.getAddressMatch(), persistedRoles); + bindingsJournal.appendAddRecord(id, JournalRecordIds.SECURITY_SETTING_RECORD, persistedRoles, true); + mapPersistedSecuritySettings.put(persistedRoles.getAddressMatch(), persistedRoles); } finally { readUnLock(); } @@ -818,6 +824,70 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp return new ArrayList<>(mapPersistedDivertConfigurations.values()); } + @Override + public void storeUser(PersistedUser persistedUser) throws Exception { + deleteUser(persistedUser.getUsername()); + readLock(); + try { + final long id = idGenerator.generateID(); + persistedUser.setStoreId(id); + bindingsJournal.appendAddRecord(id, JournalRecordIds.USER_RECORD, persistedUser, true); + mapPersistedUsers.put(persistedUser.getUsername(), persistedUser); + } finally { + readUnLock(); + } + } + + @Override + public void deleteUser(String username) throws Exception { + PersistedUser oldUser = mapPersistedUsers.remove(username); + if (oldUser != null) { + readLock(); + try { + bindingsJournal.appendDeleteRecord(oldUser.getStoreId(), false); + } finally { + readUnLock(); + } + } + } + + @Override + public Map getPersistedUsers() { + return new HashMap<>(mapPersistedUsers); + } + + @Override + public void storeRole(PersistedRole persistedRole) throws Exception { + deleteRole(persistedRole.getUsername()); + readLock(); + try { + final long id = idGenerator.generateID(); + persistedRole.setStoreId(id); + bindingsJournal.appendAddRecord(id, JournalRecordIds.ROLE_RECORD, persistedRole, true); + mapPersistedRoles.put(persistedRole.getUsername(), persistedRole); + } finally { + readUnLock(); + } + } + + @Override + public void deleteRole(String username) throws Exception { + PersistedRole oldRole = mapPersistedRoles.remove(username); + if (oldRole != null) { + readLock(); + try { + bindingsJournal.appendDeleteRecord(oldRole.getStoreId(), false); + } finally { + readUnLock(); + } + } + } + + @Override + public Map getPersistedRoles() { + return new HashMap<>(mapPersistedRoles); + } + @Override public void storeID(final long journalID, final long id) throws Exception { readLock(); @@ -852,8 +922,8 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp } @Override - public void deleteSecurityRoles(SimpleString addressMatch) throws Exception { - PersistedRoles oldRoles = mapPersistedRoles.remove(addressMatch); + public void deleteSecuritySetting(SimpleString addressMatch) throws Exception { + PersistedSecuritySetting oldRoles = mapPersistedSecuritySettings.remove(addressMatch); if (oldRoles != null) { readLock(); try { @@ -1560,9 +1630,9 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp } else if (rec == JournalRecordIds.ADDRESS_SETTING_RECORD) { PersistedAddressSetting setting = newAddressEncoding(id, buffer); mapPersistedAddressSettings.put(setting.getAddressMatch(), setting); - } else if (rec == JournalRecordIds.SECURITY_RECORD) { - PersistedRoles roles = newSecurityRecord(id, buffer); - mapPersistedRoles.put(roles.getAddressMatch(), roles); + } else if (rec == JournalRecordIds.SECURITY_SETTING_RECORD) { + PersistedSecuritySetting roles = newSecurityRecord(id, buffer); + mapPersistedSecuritySettings.put(roles.getAddressMatch(), roles); } else if (rec == JournalRecordIds.QUEUE_STATUS_RECORD) { QueueStatusEncoding statusEncoding = newQueueStatusEncoding(id, buffer); PersistentQueueBindingEncoding queueBindingEncoding = mapBindings.get(statusEncoding.queueID); @@ -1586,6 +1656,12 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp } else if (rec == JournalRecordIds.DIVERT_RECORD) { PersistedDivertConfiguration divertConfiguration = newDivertEncoding(id, buffer); mapPersistedDivertConfigurations.put(divertConfiguration.getName(), divertConfiguration); + } else if (rec == JournalRecordIds.USER_RECORD) { + PersistedUser user = newUserEncoding(id, buffer); + mapPersistedUsers.put(user.getUsername(), user); + } else if (rec == JournalRecordIds.ROLE_RECORD) { + PersistedRole role = newRoleEncoding(id, buffer); + mapPersistedRoles.put(role.getUsername(), role); } else { // unlikely to happen ActiveMQServerLogger.LOGGER.invalidRecordType(rec, new Exception("invalid record type " + rec)); @@ -2042,8 +2118,8 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp * @param buffer * @return */ - protected static PersistedRoles newSecurityRecord(long id, ActiveMQBuffer buffer) { - PersistedRoles roles = new PersistedRoles(); + protected static PersistedSecuritySetting newSecurityRecord(long id, ActiveMQBuffer buffer) { + PersistedSecuritySetting roles = new PersistedSecuritySetting(); roles.decode(buffer); roles.setStoreId(id); return roles; @@ -2074,6 +2150,20 @@ public abstract class AbstractJournalStorageManager extends CriticalComponentImp persistedDivertConfiguration.setStoreId(id); return persistedDivertConfiguration; } + + static PersistedUser newUserEncoding(long id, ActiveMQBuffer buffer) { + PersistedUser persistedUser = new PersistedUser(); + persistedUser.decode(buffer); + persistedUser.setStoreId(id); + return persistedUser; + } + + static PersistedRole newRoleEncoding(long id, ActiveMQBuffer buffer) { + PersistedRole persistedRole = new PersistedRole(); + persistedRole.decode(buffer); + persistedRole.setStoreId(id); + return persistedRole; + } /** * @param id * @param buffer diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java index 3ac704e388..d4e3cde9a4 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java @@ -88,9 +88,11 @@ import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalR import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.PAGE_TRANSACTION; import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.QUEUE_BINDING_RECORD; import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.QUEUE_STATUS_RECORD; -import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.SECURITY_RECORD; +import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.ROLE_RECORD; +import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.SECURITY_SETTING_RECORD; import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.SET_SCHEDULED_DELIVERY_TIME; import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.UPDATE_DELIVERY_COUNT; +import static org.apache.activemq.artemis.core.persistence.impl.journal.JournalRecordIds.USER_RECORD; /** * Outputs a String description of the Journals contents. @@ -685,7 +687,7 @@ public final class DescribeJournal { case ADDRESS_SETTING_RECORD: return AbstractJournalStorageManager.newAddressEncoding(id, buffer); - case SECURITY_RECORD: + case SECURITY_SETTING_RECORD: return AbstractJournalStorageManager.newSecurityRecord(id, buffer); case ADDRESS_BINDING_RECORD: @@ -694,6 +696,12 @@ public final class DescribeJournal { case ADDRESS_STATUS_RECORD: return AbstractJournalStorageManager.newAddressStatusEncoding(id, buffer); + case USER_RECORD: + return AbstractJournalStorageManager.newUserEncoding(id, buffer); + + case ROLE_RECORD: + return AbstractJournalStorageManager.newRoleEncoding(id, buffer); + default: return null; } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalRecordIds.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalRecordIds.java index b18d360af3..5644324c92 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalRecordIds.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalRecordIds.java @@ -45,7 +45,7 @@ public final class JournalRecordIds { public static final byte ADDRESS_SETTING_RECORD = 25; - public static final byte SECURITY_RECORD = 26; + public static final byte SECURITY_SETTING_RECORD = 26; public static final byte DIVERT_RECORD = 27; @@ -92,4 +92,8 @@ public final class JournalRecordIds { public static final byte ADDRESS_STATUS_RECORD = 46; + public static final byte USER_RECORD = 47; + + public static final byte ROLE_RECORD = 48; + } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/nullpm/NullStorageManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/nullpm/NullStorageManager.java index 2660b0d50d..d1783b2f22 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/nullpm/NullStorageManager.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/nullpm/NullStorageManager.java @@ -49,7 +49,9 @@ import org.apache.activemq.artemis.core.persistence.AddressQueueStatus; import org.apache.activemq.artemis.core.persistence.StorageManager; import org.apache.activemq.artemis.core.persistence.config.PersistedAddressSetting; import org.apache.activemq.artemis.core.persistence.config.PersistedDivertConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedRole; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; +import org.apache.activemq.artemis.core.persistence.config.PersistedUser; import org.apache.activemq.artemis.core.persistence.impl.PageCountPending; import org.apache.activemq.artemis.core.postoffice.Binding; import org.apache.activemq.artemis.core.postoffice.PostOffice; @@ -438,7 +440,7 @@ public class NullStorageManager implements StorageManager { } @Override - public List recoverPersistedRoles() throws Exception { + public List recoverSecuritySettings() throws Exception { return Collections.emptyList(); } @@ -456,7 +458,33 @@ public class NullStorageManager implements StorageManager { } @Override - public void storeSecurityRoles(final PersistedRoles persistedRoles) throws Exception { + public void storeUser(PersistedUser persistedUser) throws Exception { + } + + @Override + public void deleteUser(String username) throws Exception { + } + + @Override + public Map getPersistedUsers() { + return null; + } + + @Override + public void storeRole(PersistedRole persistedRole) throws Exception { + } + + @Override + public void deleteRole(String role) throws Exception { + } + + @Override + public Map getPersistedRoles() { + return null; + } + + @Override + public void storeSecuritySetting(final PersistedSecuritySetting persistedRoles) throws Exception { } @Override @@ -464,7 +492,7 @@ public class NullStorageManager implements StorageManager { } @Override - public void deleteSecurityRoles(final SimpleString addressMatch) throws Exception { + public void deleteSecuritySetting(final SimpleString addressMatch) throws Exception { } @Override diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/User.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/User.java index 9e6bacb129..3fa4761086 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/User.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/User.java @@ -16,11 +16,13 @@ */ package org.apache.activemq.artemis.core.security; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; + public class User { final String user; - final String password; + String password; public User(final String user, final String password) { this.user = user; @@ -54,7 +56,8 @@ public class User { if (user == null) { return false; } - return this.user.equals(user) && this.password.equals(password); + + return this.user.equals(user) && PasswordMaskingUtil.getHashProcessor(this.password).compare(password != null ? password.toCharArray() : null, this.password); } public String getUser() { @@ -64,4 +67,8 @@ public class User { public String getPassword() { return password; } + + public void setPassword(String password) { + this.password = password; + } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java index b25919444c..3ae3eb9a0e 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java @@ -495,4 +495,7 @@ public interface ActiveMQMessageBundle { @Message(id = 229232, value = "Cannot create consumer on {0}. Session is closed.", format = Message.Format.MESSAGE_FORMAT) ActiveMQIllegalStateException cannotCreateConsumerOnClosedSession(SimpleString queueName); + + @Message(id = 229233, value = "Cannot set ActiveMQSecurityManager during startup or while started") + IllegalStateException cannotSetSecurityManager(); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java index 77ff502f55..7b65bf2830 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java @@ -833,6 +833,10 @@ public interface ActiveMQServer extends ServiceComponent { void setMBeanServer(MBeanServer mBeanServer); + MBeanServer getMBeanServer(); + + void setSecurityManager(ActiveMQSecurityManager securityManager); + /** * Adding external components is allowed only if the state * isn't {@link SERVER_STATE#STOPPED} or {@link SERVER_STATE#STOPPING}.
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java index d95e3a186c..1d6b8dd09d 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java @@ -1717,6 +1717,14 @@ public interface ActiveMQServerLogger extends BasicLogger { format = Message.Format.MESSAGE_FORMAT) void unableStartManagementContext(@Cause Exception e); + @LogMessage(level = Logger.Level.WARN) + @Message(id = 222298, value = "Failed to create bootstrap user \"{0}\". User management may not function.", format = Message.Format.MESSAGE_FORMAT) + void failedToCreateBootstrapCredentials(@Cause Exception e, String user); + + @LogMessage(level = Logger.Level.WARN) + @Message(id = 222299, value = "No bootstrap credentials found. User management may not function.", format = Message.Format.MESSAGE_FORMAT) + void noBootstrapCredentialsFound(); + @LogMessage(level = Logger.Level.ERROR) @Message(id = 224000, value = "Failure in initialisation", format = Message.Format.MESSAGE_FORMAT) void initializationError(@Cause Throwable e); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java index 4591b308ae..c538569021 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java @@ -94,7 +94,7 @@ import org.apache.activemq.artemis.core.persistence.QueueBindingInfo; import org.apache.activemq.artemis.core.persistence.StorageManager; import org.apache.activemq.artemis.core.persistence.config.PersistedAddressSetting; import org.apache.activemq.artemis.core.persistence.config.PersistedDivertConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; import org.apache.activemq.artemis.core.persistence.impl.PageCountPending; import org.apache.activemq.artemis.core.persistence.impl.journal.JDBCJournalStorageManager; import org.apache.activemq.artemis.core.persistence.impl.journal.JournalStorageManager; @@ -186,6 +186,7 @@ import org.apache.activemq.artemis.logs.AuditLogger; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManagerFactory; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.protocol.SessionCallback; +import org.apache.activemq.artemis.spi.core.security.ActiveMQBasicSecurityManager; import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager; import org.apache.activemq.artemis.utils.ActiveMQThreadFactory; import org.apache.activemq.artemis.utils.ActiveMQThreadPoolExecutor; @@ -236,7 +237,7 @@ public class ActiveMQServerImpl implements ActiveMQServer { private final Version version; - private final ActiveMQSecurityManager securityManager; + private ActiveMQSecurityManager securityManager; private final Configuration configuration; @@ -869,6 +870,19 @@ public class ActiveMQServerImpl implements ActiveMQServer { this.mbeanServer = mbeanServer; } + @Override + public MBeanServer getMBeanServer() { + return mbeanServer; + } + + @Override + public void setSecurityManager(ActiveMQSecurityManager securityManager) { + if (state == SERVER_STATE.STARTING || state == SERVER_STATE.STARTED) { + throw ActiveMQMessageBundle.BUNDLE.cannotSetSecurityManager(); + } + this.securityManager = securityManager; + } + private void validateAddExternalComponent(ActiveMQComponent externalComponent) { final SERVER_STATE state = this.state; if (state == SERVER_STATE.STOPPED || state == SERVER_STATE.STOPPING) { @@ -3048,6 +3062,10 @@ public class ActiveMQServerImpl implements ActiveMQServer { removeExtraAddressStores(); + if (securityManager instanceof ActiveMQBasicSecurityManager) { + ((ActiveMQBasicSecurityManager)securityManager).completeInit(storageManager); + } + final ServerInfo dumper = new ServerInfo(this, pagingManager); long dumpInfoInterval = configuration.getServerDumpInterval(); @@ -3381,6 +3399,8 @@ public class ActiveMQServerImpl implements ActiveMQServer { } } + // TODO load users/roles + journalLoader.cleanUp(); return journalInfo; @@ -3395,9 +3415,9 @@ public class ActiveMQServerImpl implements ActiveMQServer { addressSettingsRepository.addMatch(set.getAddressMatch().toString(), set.getSetting()); } - List roles = storageManager.recoverPersistedRoles(); + List roles = storageManager.recoverSecuritySettings(); - for (PersistedRoles roleItem : roles) { + for (PersistedSecuritySetting roleItem : roles) { Set setRoles = SecurityFormatter.createSecurity(roleItem.getSendRoles(), roleItem.getConsumeRoles(), roleItem.getCreateDurableQueueRoles(), roleItem.getDeleteDurableQueueRoles(), roleItem.getCreateNonDurableQueueRoles(), roleItem.getDeleteNonDurableQueueRoles(), roleItem.getManageRoles(), roleItem.getBrowseRoles(), roleItem.getCreateAddressRoles(), roleItem.getDeleteAddressRoles()); securityRepository.addMatch(roleItem.getAddressMatch().toString(), setRoles); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/BasicAuthenticator.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/BasicAuthenticator.java new file mode 100644 index 0000000000..eda88a4645 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/BasicAuthenticator.java @@ -0,0 +1,56 @@ +/* + * 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.core.server.management; + +import javax.management.remote.JMXAuthenticator; +import javax.security.auth.Subject; + +import org.apache.activemq.artemis.logs.AuditLogger; +import org.apache.activemq.artemis.spi.core.security.ActiveMQBasicSecurityManager; + +/** + * JMXAuthenticator implementation to be used with ActiveMQBasicSecurityManager + */ +public class BasicAuthenticator implements JMXAuthenticator { + + ActiveMQBasicSecurityManager securityManager; + + public BasicAuthenticator(ActiveMQBasicSecurityManager securityManager) { + this.securityManager = securityManager; + } + + @Override + public Subject authenticate(final Object credentials) throws SecurityException { + Subject result; + String[] params = null; + if (credentials instanceof String[] && ((String[]) credentials).length == 2) { + params = (String[]) credentials; + } + result = securityManager.authenticate(params[0], params[1], null, null); + if (result != null) { + if (AuditLogger.isResourceLoggingEnabled()) { + AuditLogger.userSuccesfullyLoggedInAudit(result); + } + return result; + } else { + if (AuditLogger.isResourceLoggingEnabled()) { + AuditLogger.userFailedLoggedInAudit(result, null); + } + throw new SecurityException("Authentication failed"); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementConnector.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementConnector.java index 36113c684e..a73c3f1876 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementConnector.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementConnector.java @@ -19,9 +19,12 @@ package org.apache.activemq.artemis.core.server.management; import org.apache.activemq.artemis.core.config.JMXConnectorConfiguration; import org.apache.activemq.artemis.core.server.ActiveMQComponent; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.spi.core.security.ActiveMQBasicSecurityManager; +import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager; import javax.management.MBeanServer; import javax.management.ObjectName; +import javax.management.remote.JMXAuthenticator; import java.util.HashMap; import java.util.Map; @@ -32,9 +35,11 @@ public class ManagementConnector implements ActiveMQComponent { private ConnectorServerFactory connectorServerFactory; private RmiRegistryFactory rmiRegistryFactory; private MBeanServerFactory mbeanServerFactory; + private ActiveMQSecurityManager securityManager; - public ManagementConnector(JMXConnectorConfiguration configuration) { + public ManagementConnector(JMXConnectorConfiguration configuration, ActiveMQSecurityManager securityManager) { this.configuration = configuration; + this.securityManager = securityManager; } @Override @@ -54,8 +59,15 @@ public class ManagementConnector implements ActiveMQComponent { MBeanServer mbeanServer = mbeanServerFactory.getServer(); - JaasAuthenticator jaasAuthenticator = new JaasAuthenticator(); - jaasAuthenticator.setRealm(configuration.getJmxRealm()); + JMXAuthenticator authenticator; + + if (securityManager != null && securityManager instanceof ActiveMQBasicSecurityManager) { + authenticator = new BasicAuthenticator((ActiveMQBasicSecurityManager) securityManager); + } else { + JaasAuthenticator jaasAuthenticator = new JaasAuthenticator(); + jaasAuthenticator.setRealm(configuration.getJmxRealm()); + authenticator = jaasAuthenticator; + } connectorServerFactory = new ConnectorServerFactory(); connectorServerFactory.setServer(mbeanServer); @@ -63,7 +75,7 @@ public class ManagementConnector implements ActiveMQComponent { connectorServerFactory.setRmiServerHost(configuration.getConnectorHost()); connectorServerFactory.setObjectName(new ObjectName(configuration.getObjectName())); Map environment = new HashMap<>(); - environment.put("jmx.remote.authenticator", jaasAuthenticator); + environment.put("jmx.remote.authenticator", authenticator); try { connectorServerFactory.setEnvironment(environment); connectorServerFactory.setAuthenticatorType(configuration.getAuthenticatorType()); @@ -108,4 +120,7 @@ public class ManagementConnector implements ActiveMQComponent { } } + public ConnectorServerFactory getConnectorServerFactory() { + return connectorServerFactory; + } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementContext.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementContext.java index 1476e4ed9b..42fa2fdc9d 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementContext.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementContext.java @@ -25,6 +25,7 @@ import org.apache.activemq.artemis.core.server.ServiceComponent; import org.apache.activemq.artemis.core.server.management.impl.HawtioSecurityControlImpl; import javax.management.NotCompliantMBeanException; +import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager; public class ManagementContext implements ServiceComponent { private AtomicBoolean isStarted = new AtomicBoolean(false); @@ -32,6 +33,7 @@ public class ManagementContext implements ServiceComponent { private JMXConnectorConfiguration jmxConnectorConfiguration; private ManagementConnector mBeanServer; private ArtemisMBeanServerGuard guardHandler; + private ActiveMQSecurityManager securityManager; @Override public void start() throws Exception { @@ -44,7 +46,7 @@ public class ManagementContext implements ServiceComponent { } if (jmxConnectorConfiguration != null) { - mBeanServer = new ManagementConnector(jmxConnectorConfiguration); + mBeanServer = new ManagementConnector(jmxConnectorConfiguration, securityManager); mBeanServer.start(); } isStarted.set(true); @@ -99,4 +101,16 @@ public class ManagementContext implements ServiceComponent { public ArtemisMBeanServerGuard getArtemisMBeanServerGuard() { return guardHandler; } + + public void setSecurityManager(ActiveMQSecurityManager securityManager) { + this.securityManager = securityManager; + } + + public ActiveMQSecurityManager getSecurityManager() { + return securityManager; + } + + public ManagementConnector getManagementConnector() { + return mBeanServer; + } } 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 new file mode 100644 index 0000000000..e6558a7dd5 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQBasicSecurityManager.java @@ -0,0 +1,211 @@ +/* + * 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.spi.core.security; + +import javax.security.auth.Subject; +import java.security.Principal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.activemq.artemis.core.persistence.StorageManager; +import org.apache.activemq.artemis.core.persistence.config.PersistedRole; +import org.apache.activemq.artemis.core.persistence.config.PersistedUser; +import org.apache.activemq.artemis.core.security.CheckType; +import org.apache.activemq.artemis.core.security.Role; +import org.apache.activemq.artemis.core.security.User; +import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.logs.AuditLogger; +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.SecurityManagerUtil; +import org.jboss.logging.Logger; + +/** + * All user and role state (both in memory and on disk) is maintained by the underlying StorageManager + */ +public class ActiveMQBasicSecurityManager implements ActiveMQSecurityManager5, UserManagement { + + private static final Logger logger = Logger.getLogger(ActiveMQBasicSecurityManager.class); + + public static final String BOOTSTRAP_USER = "bootstrapUser"; + public static final String BOOTSTRAP_PASSWORD = "bootstrapPassword"; + public static final String BOOTSTRAP_ROLE = "bootstrapRole"; + + private Map properties; + private String rolePrincipalClass = RolePrincipal.class.getName(); + private StorageManager storageManager; + + @Override + public ActiveMQBasicSecurityManager init(Map properties) { + if (!properties.containsKey(BOOTSTRAP_USER) || !properties.containsKey(BOOTSTRAP_PASSWORD) || !properties.containsKey(BOOTSTRAP_ROLE)) { + ActiveMQServerLogger.LOGGER.noBootstrapCredentialsFound(); + } else { + this.properties = properties; + } + return this; + } + + @Override + public boolean validateUser(String user, String password) { + throw new UnsupportedOperationException("Invoke authenticate(String, String, RemotingConnection, String) instead"); + } + + @Override + public Subject authenticate(final String userToAuthenticate, final String passwordToAuthenticate, RemotingConnection remotingConnection, final String securityDomain) { + try { + if (storageManager.isStarted() && storageManager.getPersistedUsers() != null) { + PersistedUser persistedUser = storageManager.getPersistedUsers().get(userToAuthenticate); + if (persistedUser != null) { + User user = new User(persistedUser.getUsername(), persistedUser.getPassword()); + if (user.isValid(userToAuthenticate, passwordToAuthenticate)) { + Subject subject = new Subject(); + subject.getPrincipals().add(new UserPrincipal(userToAuthenticate)); + for (String role : getRole(userToAuthenticate).getRoles()) { + subject.getPrincipals().add((Principal) SecurityManagerUtil.createGroupPrincipal(role, rolePrincipalClass)); + } + if (AuditLogger.isAnyLoggingEnabled() && remotingConnection != null) { + remotingConnection.setAuditSubject(subject); + } + if (AuditLogger.isResourceLoggingEnabled()) { + AuditLogger.userSuccesfullyLoggedInAudit(subject); + } + return subject; + } + } + } + } catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("Couldn't validate user", e); + } + } + + return null; + } + + @Override + public boolean validateUserAndRole(String user, String password, Set roles, CheckType checkType) { + throw new UnsupportedOperationException("Invoke authorize(Subject, Set, CheckType, String) instead"); + } + + @Override + public boolean authorize(final Subject subject, + final Set roles, + final CheckType checkType, + final String address) { + boolean authorized = SecurityManagerUtil.authorize(subject, roles, checkType, rolePrincipalClass); + + if (logger.isTraceEnabled()) { + logger.trace("user " + (authorized ? " is " : " is NOT ") + "authorized"); + } + + return authorized; + } + + @Override + public synchronized void addNewUser(String user, String password, String... roles) throws Exception { + if (user == null) { + throw ActiveMQMessageBundle.BUNDLE.nullUser(); + } + if (password == null) { + throw ActiveMQMessageBundle.BUNDLE.nullPassword(); + } + if (userExists(user)) { + throw ActiveMQMessageBundle.BUNDLE.userAlreadyExists(user); + } + + storageManager.storeUser(new PersistedUser(user, password)); + storageManager.storeRole(new PersistedRole(user, Arrays.asList(roles))); + } + + @Override + public synchronized void removeUser(final String user) throws Exception { + if (!userExists(user)) { + throw ActiveMQMessageBundle.BUNDLE.userDoesNotExist(user); + } + + storageManager.deleteUser(user); + storageManager.deleteRole(user); + } + + @Override + public synchronized Map> listUser(String user) { + // a null or empty user is actually valid here + if (user != null && user.length() != 0 && !userExists(user)) { + throw ActiveMQMessageBundle.BUNDLE.userDoesNotExist(user); + } + + Map> result = new HashMap<>(); + + if (user != null && user.length() > 0) { + result.put(user, new HashSet<>(getRole(user).getRoles())); + } else { + for (String thisUser : storageManager.getPersistedUsers().keySet()) { + result.put(thisUser, new HashSet<>(getRole(thisUser).getRoles())); + } + } + return result; + } + + @Override + public synchronized void updateUser(String user, String password, String... roles) throws Exception { + if (!userExists(user)) { + throw ActiveMQMessageBundle.BUNDLE.userDoesNotExist(user); + } + + // potentially update the user's password + if (password != null) { + storageManager.deleteUser(user); + storageManager.storeUser(new PersistedUser(user, password)); + } + + // potentially update the user's role(s) + if (roles != null && roles.length > 0) { + storageManager.deleteRole(user); + storageManager.storeRole(new PersistedRole(user, Arrays.asList(roles))); + } + } + + 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)}); + } + } catch (Exception e) { + ActiveMQServerLogger.LOGGER.failedToCreateBootstrapCredentials(e, properties.get(BOOTSTRAP_USER)); + } + } + } + + private boolean userExists(String user) { + return user != null && storageManager.getPersistedUsers() != null && storageManager.getPersistedUsers().containsKey(user); + } + + private PersistedRole getRole(String user) { + return storageManager.getPersistedRoles().get(user); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java index 18fada53a4..d5129a23fa 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java @@ -19,21 +19,16 @@ package org.apache.activemq.artemis.spi.core.security; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.security.Principal; -import java.util.HashSet; -import java.util.Iterator; import java.util.Set; import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration; import org.apache.activemq.artemis.core.security.CheckType; import org.apache.activemq.artemis.core.security.Role; -import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.logs.AuditLogger; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.security.jaas.JaasCallbackHandler; import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal; +import org.apache.activemq.artemis.utils.SecurityManagerUtil; import org.jboss.logging.Logger; import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection; @@ -48,8 +43,6 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager5 { private static final Logger logger = Logger.getLogger(ActiveMQJAASSecurityManager.class); - private static final String WILDCARD = "*"; - private String configurationName; private String certificateConfigurationName; private SecurityConfiguration configuration; @@ -115,33 +108,10 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager5 { final Set roles, final CheckType checkType, final String address) { - boolean authorized = false; + boolean authorized = SecurityManagerUtil.authorize(subject, roles, checkType, rolePrincipalClass); - if (subject != null) { - Set rolesWithPermission = getPrincipalsInRole(checkType, roles); - - // Check the caller's roles - Set rolesForSubject = new HashSet<>(); - try { - rolesForSubject.addAll(subject.getPrincipals(Class.forName(rolePrincipalClass).asSubclass(Principal.class))); - } catch (Exception e) { - ActiveMQServerLogger.LOGGER.failedToFindRolesForTheSubject(e); - } - if (rolesForSubject.size() > 0 && rolesWithPermission.size() > 0) { - Iterator rolesForSubjectIter = rolesForSubject.iterator(); - while (!authorized && rolesForSubjectIter.hasNext()) { - Iterator rolesWithPermissionIter = rolesWithPermission.iterator(); - Principal subjectRole = rolesForSubjectIter.next(); - while (!authorized && rolesWithPermissionIter.hasNext()) { - Principal roleWithPermission = rolesWithPermissionIter.next(); - authorized = subjectRole.equals(roleWithPermission); - } - } - } - - if (logger.isTraceEnabled()) { - logger.trace("user " + (authorized ? " is " : " is NOT ") + "authorized"); - } + if (logger.isTraceEnabled()) { + logger.trace("user " + (authorized ? " is " : " is NOT ") + "authorized"); } return authorized; @@ -187,20 +157,6 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager5 { } } - private Set getPrincipalsInRole(final CheckType checkType, final Set roles) { - Set principals = new HashSet<>(); - for (Role role : roles) { - if (checkType.hasRole(role)) { - try { - principals.add(createGroupPrincipal(role.getName(), rolePrincipalClass)); - } catch (Exception e) { - ActiveMQServerLogger.LOGGER.failedAddRolePrincipal(e); - } - } - } - return principals; - } - public void setConfigurationName(final String configurationName) { this.configurationName = configurationName; } @@ -240,60 +196,4 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager5 { public void setRolePrincipalClass(String rolePrincipalClass) { this.rolePrincipalClass = rolePrincipalClass; } - - public static Object createGroupPrincipal(String name, String groupClass) throws Exception { - if (WILDCARD.equals(name)) { - // simple match all group principal - match any name and class - return new Principal() { - @Override - public String getName() { - return WILDCARD; - } - - @Override - public boolean equals(Object other) { - return true; - } - - @Override - public int hashCode() { - return WILDCARD.hashCode(); - } - }; - } - Object[] param = new Object[]{name}; - - Class cls = Class.forName(groupClass); - - Constructor[] constructors = cls.getConstructors(); - int i; - Object instance; - for (i = 0; i < constructors.length; i++) { - Class[] paramTypes = constructors[i].getParameterTypes(); - if (paramTypes.length != 0 && paramTypes[0].equals(String.class)) { - break; - } - } - if (i < constructors.length) { - instance = constructors[i].newInstance(param); - } else { - instance = cls.newInstance(); - Method[] methods = cls.getMethods(); - i = 0; - for (i = 0; i < methods.length; i++) { - Class[] paramTypes = methods[i].getParameterTypes(); - if (paramTypes.length != 0 && methods[i].getName().equals("setName") && paramTypes[0].equals(String.class)) { - break; - } - } - - if (i < methods.length) { - methods[i].invoke(instance, param); - } else { - throw new NoSuchMethodException(); - } - } - - return instance; - } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/UserManagement.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/UserManagement.java new file mode 100644 index 0000000000..743f86f545 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/UserManagement.java @@ -0,0 +1,31 @@ +/* + * 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.spi.core.security; + +import java.util.Map; +import java.util.Set; + +public interface UserManagement { + + void addNewUser(String user, String password, String... roles) throws Exception; + + void removeUser(String user) throws Exception; + + Map> listUser(String user); + + void updateUser(String username, String password, String... roles) throws Exception; +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java index 392fdc727f..212dbb3870 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java @@ -148,7 +148,7 @@ public class LDAPLoginModule implements AuditLoginModule { private String getPlainPassword(String password) { try { - return PasswordMaskingUtil.resolveMask(null, password, codecClass); + return PasswordMaskingUtil.resolveMask(password, codecClass); } catch (Exception e) { throw new IllegalArgumentException("Failed to decode password", e); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModuleConfigurator.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModuleConfigurator.java index 245360c5be..21c238a948 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModuleConfigurator.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModuleConfigurator.java @@ -29,6 +29,7 @@ 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.spi.core.security.UserManagement; import org.apache.activemq.artemis.utils.StringUtil; import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder; @@ -37,7 +38,7 @@ import org.apache.commons.configuration2.builder.fluent.Configurations; 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 { +public class PropertiesLoginModuleConfigurator implements UserManagement { private static final String LICENSE_HEADER = "## ---------------------------------------------------------------------------\n" + @@ -125,7 +126,8 @@ public class PropertiesLoginModuleConfigurator { } } - public void addNewUser(String username, String hash, String... roles) throws Exception { + @Override + public void addNewUser(String username, String hash, String... roles) { if (userConfig.getString(username) != null) { throw ActiveMQMessageBundle.BUNDLE.userAlreadyExists(username); } @@ -143,6 +145,7 @@ public class PropertiesLoginModuleConfigurator { } } + @Override public void removeUser(String username) { if (userConfig.getProperty(username) == null) { throw ActiveMQMessageBundle.BUNDLE.userDoesNotExist(username); @@ -151,6 +154,7 @@ public class PropertiesLoginModuleConfigurator { removeRoles(username); } + @Override public Map> listUser(String username) { Map> result = new HashMap<>(); @@ -166,7 +170,8 @@ public class PropertiesLoginModuleConfigurator { return result; } - public void updateUser(String username, String password, String[] roles) { + @Override + public void updateUser(String username, String password, String... roles) { String oldPassword = (String) userConfig.getProperty(username); if (oldPassword == null) { throw ActiveMQMessageBundle.BUNDLE.userDoesNotExist(username); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/utils/SecurityManagerUtil.java b/artemis-server/src/main/java/org/apache/activemq/artemis/utils/SecurityManagerUtil.java new file mode 100644 index 0000000000..bb8d90f696 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/utils/SecurityManagerUtil.java @@ -0,0 +1,138 @@ +/** + * 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 javax.security.auth.Subject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.security.Principal; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.activemq.artemis.core.security.CheckType; +import org.apache.activemq.artemis.core.security.Role; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal; + +public class SecurityManagerUtil { + + private static final String WILDCARD = "*"; + + public static Set getPrincipalsInRole(final CheckType checkType, final Set roles, final String rolePrincipalClass) { + Set principals = new HashSet<>(); + for (Role role : roles) { + if (checkType.hasRole(role)) { + try { + principals.add(SecurityManagerUtil.createGroupPrincipal(role.getName(), rolePrincipalClass)); + } catch (Exception e) { + ActiveMQServerLogger.LOGGER.failedAddRolePrincipal(e); + } + } + } + return principals; + } + + public static Object createGroupPrincipal(String name, String groupClass) throws Exception { + if (WILDCARD.equals(name)) { + // simple match all group principal - match any name and class + return new Principal() { + @Override + public String getName() { + return WILDCARD; + } + + @Override + public boolean equals(Object other) { + return true; + } + + @Override + public int hashCode() { + return WILDCARD.hashCode(); + } + }; + } + Object[] param = new Object[]{name}; + + Class cls = Class.forName(groupClass); + + Constructor[] constructors = cls.getConstructors(); + int i; + Object instance; + for (i = 0; i < constructors.length; i++) { + Class[] paramTypes = constructors[i].getParameterTypes(); + if (paramTypes.length != 0 && paramTypes[0].equals(String.class)) { + break; + } + } + if (i < constructors.length) { + instance = constructors[i].newInstance(param); + } else { + instance = cls.newInstance(); + Method[] methods = cls.getMethods(); + i = 0; + for (i = 0; i < methods.length; i++) { + Class[] paramTypes = methods[i].getParameterTypes(); + if (paramTypes.length != 0 && methods[i].getName().equals("setName") && paramTypes[0].equals(String.class)) { + break; + } + } + + if (i < methods.length) { + methods[i].invoke(instance, param); + } else { + throw new NoSuchMethodException(); + } + } + + return instance; + } + + /** + * This method tries to match the RolePrincipals in the Subject with the provided Set of Roles and CheckType + */ + public static boolean authorize(final Subject subject, final Set roles, final CheckType checkType, final String rolePrincipalClass) { + boolean authorized = false; + + if (subject != null) { + Set rolesWithPermission = getPrincipalsInRole(checkType, roles, rolePrincipalClass); + + // Check the caller's roles + Set rolesForSubject = new HashSet<>(); + try { + rolesForSubject.addAll(subject.getPrincipals(Class.forName(rolePrincipalClass).asSubclass(Principal.class))); + } catch (Exception e) { + ActiveMQServerLogger.LOGGER.failedToFindRolesForTheSubject(e); + } + if (rolesForSubject.size() > 0 && rolesWithPermission.size() > 0) { + Iterator rolesForSubjectIter = rolesForSubject.iterator(); + while (!authorized && rolesForSubjectIter.hasNext()) { + Iterator rolesWithPermissionIter = rolesWithPermission.iterator(); + Principal subjectRole = rolesForSubjectIter.next(); + while (!authorized && rolesWithPermissionIter.hasNext()) { + Principal roleWithPermission = rolesWithPermissionIter.next(); + authorized = subjectRole.equals(roleWithPermission); + } + } + } + } + + return authorized; + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/transaction/impl/TransactionImplTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/transaction/impl/TransactionImplTest.java index db68346687..1ff7892d2c 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/transaction/impl/TransactionImplTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/transaction/impl/TransactionImplTest.java @@ -48,7 +48,9 @@ import org.apache.activemq.artemis.core.persistence.QueueBindingInfo; import org.apache.activemq.artemis.core.persistence.StorageManager; import org.apache.activemq.artemis.core.persistence.config.PersistedAddressSetting; import org.apache.activemq.artemis.core.persistence.config.PersistedDivertConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedRole; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; +import org.apache.activemq.artemis.core.persistence.config.PersistedUser; import org.apache.activemq.artemis.core.persistence.impl.PageCountPending; import org.apache.activemq.artemis.core.postoffice.Binding; import org.apache.activemq.artemis.core.postoffice.PostOffice; @@ -597,17 +599,17 @@ public class TransactionImplTest extends ActiveMQTestBase { } @Override - public void storeSecurityRoles(PersistedRoles persistedRoles) throws Exception { + public void storeSecuritySetting(PersistedSecuritySetting persistedRoles) throws Exception { } @Override - public void deleteSecurityRoles(SimpleString addressMatch) throws Exception { + public void deleteSecuritySetting(SimpleString addressMatch) throws Exception { } @Override - public List recoverPersistedRoles() throws Exception { + public List recoverSecuritySettings() throws Exception { return null; } @@ -626,6 +628,36 @@ public class TransactionImplTest extends ActiveMQTestBase { return null; } + @Override + public void storeUser(PersistedUser persistedUser) throws Exception { + + } + + @Override + public void deleteUser(String username) throws Exception { + + } + + @Override + public Map getPersistedUsers() { + return null; + } + + @Override + public void storeRole(PersistedRole persistedRole) throws Exception { + + } + + @Override + public void deleteRole(String role) throws Exception { + + } + + @Override + public Map getPersistedRoles() { + return null; + } + @Override public long storePageCounter(long txID, long queueID, long value, long size) throws Exception { return 0; diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index 43d47a73b6..269ae6f500 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -352,16 +352,23 @@ the Transport](configuring-transports.md). ## User credentials -Apache ActiveMQ Artemis ships with two security manager implementations: - -- The legacy, deprecated `ActiveMQSecurityManager` that reads user credentials, - i.e. user names, passwords and role information from properties files on the - classpath called `artemis-users.properties` and `artemis-roles.properties`. +Apache ActiveMQ Artemis ships with three security manager implementations: - The flexible, pluggable `ActiveMQJAASSecurityManager` which supports any standard JAAS login module. Artemis ships with several login modules which will be discussed further down. This is the default security manager. +- The `ActiveMQBasicSecurityManager` which doesn't use JAAS and only supports + auth via username & password credentials. It also supports adding, removing, + and updating users via the management API. All user & role data is stored + in the broker's bindings journal which means any changes made to a live + broker will be available on its backup. + +- The legacy, deprecated `ActiveMQSecurityManagerImpl` that reads user + credentials, i.e. user names, passwords and role information from properties + files on the classpath called `artemis-users.properties` and + `artemis-roles.properties`. + ### JAAS Security Manager When using the Java Authentication and Authorization Service (JAAS) much of the @@ -1099,6 +1106,90 @@ superseded by SASL GSSAPI. However, for clients that don't support SASL (core client), using TLS can provide Kerberos authentication over an *unsecure* channel. +### Basic Security Manager + +As the name suggests, the `ActiveMQBasicSecurityManager` is _basic_. It is not +pluggable like the JAAS security manager and it _only_ supports authentication +via username and password credentials. Furthermore, the the Hawtio-based web +console requires JAAS. Therefore you will *still need* to configure a +`login.config` if you plan on using the web console. However, this security +manager *may* still may have a couple of advantages depending on your use-case. + +All user & role data is stored in the bindings journal (or bindings table if +using JDBC). The advantage here is that in a live/backup use-case any user +management performed on the live broker will be reflected on the backup upon +failover. Typically LDAP would be employed for this kind of use-case, but not +everyone wants or is able to administer an independent LDAP server. + +User management is provided by the broker's management API. This includes the +ability to add, list, update, and remove users & roles. As with all management +functions, this is available via JMX, management messages, HTTP (via Jolokia), +web console, etc. These functions are also available from the ActiveMQ Artemis +command-line interface. Having the broker store this data directly means that +it must be running in order to manage users. There is no way to modify the +bindings data manually. + +To be clear, any management access via HTTP (e.g. web console or Jolokia) will +go through Hawtio JAAS. MBean access via JConsole or other remote JMX tool will +go through the basic security manager. Management messages will also go through +the basic security manager. + +#### Configuration + +The configuration for the `ActiveMQBasicSecurityManager` happens in +`bootstrap.xml` just like it does for all security manager implementations. +Here's an example: + +```xml + + + + + + + + + ... + +``` + +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 + +The value specified in the `bootstrapRole` will need the following permissions +on the `activemq.management` address: + +- `createNonDurableQueue` +- `createAddress` +- `consume` +- `manage` +- `send` + +For example: + +```xml + + + + + + + +``` + +> **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. + ## 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/client/SendAckFailTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/client/SendAckFailTest.java index 222a0bb584..786de34427 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/client/SendAckFailTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/client/SendAckFailTest.java @@ -62,7 +62,9 @@ import org.apache.activemq.artemis.core.persistence.AddressQueueStatus; import org.apache.activemq.artemis.core.persistence.StorageManager; import org.apache.activemq.artemis.core.persistence.config.PersistedAddressSetting; import org.apache.activemq.artemis.core.persistence.config.PersistedDivertConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedRole; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; +import org.apache.activemq.artemis.core.persistence.config.PersistedUser; import org.apache.activemq.artemis.core.persistence.impl.PageCountPending; import org.apache.activemq.artemis.core.postoffice.Binding; import org.apache.activemq.artemis.core.postoffice.PostOffice; @@ -694,18 +696,18 @@ public class SendAckFailTest extends SpawnedTestBase { } @Override - public void storeSecurityRoles(PersistedRoles persistedRoles) throws Exception { - manager.storeSecurityRoles(persistedRoles); + public void storeSecuritySetting(PersistedSecuritySetting persistedRoles) throws Exception { + manager.storeSecuritySetting(persistedRoles); } @Override - public void deleteSecurityRoles(SimpleString addressMatch) throws Exception { - manager.deleteSecurityRoles(addressMatch); + public void deleteSecuritySetting(SimpleString addressMatch) throws Exception { + manager.deleteSecuritySetting(addressMatch); } @Override - public List recoverPersistedRoles() throws Exception { - return manager.recoverPersistedRoles(); + public List recoverSecuritySettings() throws Exception { + return manager.recoverSecuritySettings(); } @Override @@ -723,6 +725,36 @@ public class SendAckFailTest extends SpawnedTestBase { return null; } + @Override + public void storeUser(PersistedUser persistedUser) throws Exception { + manager.storeUser(persistedUser); + } + + @Override + public void deleteUser(String username) throws Exception { + manager.deleteUser(username); + } + + @Override + public Map getPersistedUsers() { + return manager.getPersistedUsers(); + } + + @Override + public void storeRole(PersistedRole persistedRole) throws Exception { + manager.storeRole(persistedRole); + } + + @Override + public void deleteRole(String role) throws Exception { + manager.deleteRole(role); + } + + @Override + public Map getPersistedRoles() { + return manager.getPersistedRoles(); + } + @Override public long storePageCounter(long txID, long queueID, long value, long size) throws Exception { return manager.storePageCounter(txID, queueID, value, size); diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/persistence/RolesConfigurationStorageTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/persistence/RolesConfigurationStorageTest.java index b525e9d8e0..66263e2a3f 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/persistence/RolesConfigurationStorageTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/persistence/RolesConfigurationStorageTest.java @@ -22,13 +22,13 @@ import java.util.Map; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.core.config.StoreConfiguration; -import org.apache.activemq.artemis.core.persistence.config.PersistedRoles; +import org.apache.activemq.artemis.core.persistence.config.PersistedSecuritySetting; import org.junit.Before; import org.junit.Test; public class RolesConfigurationStorageTest extends StorageManagerTestBase { - private Map mapExpectedSets; + private Map mapExpectedSets; public RolesConfigurationStorageTest(StoreConfiguration.StoreType storeType) { super(storeType); @@ -41,18 +41,18 @@ public class RolesConfigurationStorageTest extends StorageManagerTestBase { mapExpectedSets = new HashMap<>(); } - protected void addSetting(PersistedRoles setting) throws Exception { + protected void addSetting(PersistedSecuritySetting setting) throws Exception { mapExpectedSets.put(setting.getAddressMatch(), setting); - journal.storeSecurityRoles(setting); + journal.storeSecuritySetting(setting); } @Test public void testStoreSecuritySettings() throws Exception { createStorage(); - addSetting(new PersistedRoles("a#", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); + addSetting(new PersistedSecuritySetting("a#", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); - addSetting(new PersistedRoles("a2", "a1", null, "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); + addSetting(new PersistedSecuritySetting("a2", "a1", null, "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); journal.stop(); @@ -62,9 +62,9 @@ public class RolesConfigurationStorageTest extends StorageManagerTestBase { checkSettings(); - addSetting(new PersistedRoles("a2", "a1", null, "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); + addSetting(new PersistedSecuritySetting("a2", "a1", null, "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); - addSetting(new PersistedRoles("a3", "a1", null, "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); + addSetting(new PersistedSecuritySetting("a3", "a1", null, "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); checkSettings(); @@ -92,7 +92,7 @@ public class RolesConfigurationStorageTest extends StorageManagerTestBase { checkSettings(); - addSetting(new PersistedRoles("a#", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); + addSetting(new PersistedSecuritySetting("a#", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1", "a1")); journal.stop(); @@ -112,12 +112,12 @@ public class RolesConfigurationStorageTest extends StorageManagerTestBase { * @throws Exception */ private void checkSettings() throws Exception { - List listSetting = journal.recoverPersistedRoles(); + List listSetting = journal.recoverSecuritySettings(); assertEquals(mapExpectedSets.size(), listSetting.size()); - for (PersistedRoles el : listSetting) { - PersistedRoles el2 = mapExpectedSets.get(el.getAddressMatch()); + for (PersistedSecuritySetting el : listSetting) { + PersistedSecuritySetting el2 = mapExpectedSets.get(el.getAddressMatch()); assertEquals(el, el2); } diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerFailoverTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerFailoverTest.java new file mode 100644 index 0000000000..6f162d81ed --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerFailoverTest.java @@ -0,0 +1,164 @@ +/* + * 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.tests.integration.security; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.api.core.client.ClientSession; +import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; +import org.apache.activemq.artemis.core.config.ha.SharedStoreMasterPolicyConfiguration; +import org.apache.activemq.artemis.core.config.ha.SharedStoreSlavePolicyConfiguration; +import org.apache.activemq.artemis.spi.core.security.ActiveMQBasicSecurityManager; +import org.apache.activemq.artemis.tests.integration.cluster.failover.FailoverTestBase; +import org.apache.activemq.artemis.tests.util.ReplicatedBackupUtils; +import org.apache.activemq.artemis.tests.util.TransportConfigurationUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(value = Parameterized.class) +public class BasicSecurityManagerFailoverTest extends FailoverTestBase { + + private boolean replicated; + + @Parameterized.Parameters(name = "replicated={0}") + public static Collection getParameters() { + return Arrays.asList(new Object[][]{{true}, {false}}); + } + + public BasicSecurityManagerFailoverTest(boolean replicated) { + this.replicated = replicated; + } + + @Override + protected void createConfigs() throws Exception { + if (replicated) { + createReplicatedConfigs(); + } else { + createSharedStoreConfigs(); + } + } + + protected void createSharedStoreConfigs() throws Exception { + nodeManager = createNodeManager(); + TransportConfiguration liveConnector = getConnectorTransportConfiguration(true); + TransportConfiguration backupConnector = getConnectorTransportConfiguration(false); + + backupConfig = super + .createDefaultInVMConfig() + .setSecurityEnabled(true) + .clearAcceptorConfigurations() + .addAcceptorConfiguration(getAcceptorTransportConfiguration(false)) + .setHAPolicyConfiguration(new SharedStoreSlavePolicyConfiguration()) + .addConnectorConfiguration(liveConnector.getName(), liveConnector) + .addConnectorConfiguration(backupConnector.getName(), backupConnector) + .addClusterConfiguration(createBasicClusterConfig(backupConnector.getName(), liveConnector.getName())); + + backupServer = createTestableServer(backupConfig); + + backupServer.getServer().setSecurityManager(new ActiveMQBasicSecurityManager()); + + liveConfig = super + .createDefaultInVMConfig() + .setSecurityEnabled(true) + .clearAcceptorConfigurations() + .addAcceptorConfiguration(getAcceptorTransportConfiguration(true)) + .setHAPolicyConfiguration(new SharedStoreMasterPolicyConfiguration()) + .addClusterConfiguration(createBasicClusterConfig(liveConnector.getName())) + .addConnectorConfiguration(liveConnector.getName(), liveConnector); + + liveServer = createTestableServer(liveConfig); + + liveServer.getServer().setSecurityManager(new ActiveMQBasicSecurityManager()); + } + + @Override + protected void createReplicatedConfigs() throws Exception { + final TransportConfiguration liveConnector = getConnectorTransportConfiguration(true); + final TransportConfiguration backupConnector = getConnectorTransportConfiguration(false); + final TransportConfiguration backupAcceptor = getAcceptorTransportConfiguration(false); + + backupConfig = createDefaultInVMConfig(); + liveConfig = createDefaultInVMConfig(); + + ReplicatedBackupUtils.configureReplicationPair(backupConfig, backupConnector, backupAcceptor, liveConfig, liveConnector, null); + + backupConfig + .setSecurityEnabled(true) + .setBindingsDirectory(getBindingsDir(0, true)) + .setJournalDirectory(getJournalDir(0, true)) + .setPagingDirectory(getPageDir(0, true)) + .setLargeMessagesDirectory(getLargeMessagesDir(0, true)); + + setupHAPolicyConfiguration(); + nodeManager = createReplicatedBackupNodeManager(backupConfig); + + backupServer = createTestableServer(backupConfig); + + backupServer.getServer().setSecurityManager(new ActiveMQBasicSecurityManager()); + + liveConfig + .setSecurityEnabled(true) + .clearAcceptorConfigurations() + .addAcceptorConfiguration(getAcceptorTransportConfiguration(true)); + + liveServer = createTestableServer(liveConfig); + + liveServer.getServer().setSecurityManager(new ActiveMQBasicSecurityManager()); + } + + @Override + protected TransportConfiguration getAcceptorTransportConfiguration(final boolean live) { + return TransportConfigurationUtils.getInVMAcceptor(live); + } + + @Override + protected TransportConfiguration getConnectorTransportConfiguration(final boolean live) { + return TransportConfigurationUtils.getInVMConnector(live); + } + + @Test + public void testFailover() throws Exception { + + liveServer.getServer().getActiveMQServerControl().addUser("foo", "bar", "baz", false); + + ClientSessionFactory cf = createSessionFactory(getServerLocator()); + ClientSession session = null; + + try { + session = cf.createSession("foo", "bar", false, true, true, false, 0); + } catch (ActiveMQException e) { + e.printStackTrace(); + Assert.fail("should not throw exception"); + } + + crash(session); + waitForServerToStart(backupServer.getServer()); + + try { + cf = createSessionFactory(getServerLocator()); + session = cf.createSession("foo", "bar", false, true, true, false, 0); + } catch (ActiveMQException e) { + e.printStackTrace(); + Assert.fail("should not throw exception"); + } + } +} 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 new file mode 100644 index 0000000000..79855a0535 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/BasicSecurityManagerTest.java @@ -0,0 +1,312 @@ +/* + * 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.tests.integration.security; + +import java.lang.management.ManagementFactory; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.client.ClientConsumer; +import org.apache.activemq.artemis.api.core.client.ClientMessage; +import org.apache.activemq.artemis.api.core.client.ClientProducer; +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.core.security.Role; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ActiveMQServers; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.spi.core.security.ActiveMQBasicSecurityManager; +import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class BasicSecurityManagerTest extends ActiveMQTestBase { + + private ServerLocator locator; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + locator = createInVMNonHALocator(); + } + + 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"); + ActiveMQBasicSecurityManager securityManager = new ActiveMQBasicSecurityManager().init(initProperties); + ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, true)); + return server; + } + + @Test + public void testAuthenticationForBootstrapUser() throws Exception { + ActiveMQServer server = initializeServer(); + server.start(); + ClientSessionFactory cf = createSessionFactory(locator); + + try { + ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0); + session.close(); + } catch (ActiveMQException e) { + e.printStackTrace(); + Assert.fail("should not throw exception"); + } + } + + @Test + public void testAuthenticationForAddedUserHashed() throws Exception { + internalTestAuthenticationForAddedUser(false); + } + + @Test + public void testAuthenticationForAddedUserPlainText() throws Exception { + internalTestAuthenticationForAddedUser(true); + } + + private void internalTestAuthenticationForAddedUser(boolean plaintext) throws Exception { + ActiveMQServer server = initializeServer(); + server.start(); + ClientSessionFactory cf = createSessionFactory(locator); + + server.getActiveMQServerControl().addUser("foo", "bar", "baz", plaintext); + + try { + ClientSession session = cf.createSession("foo", "bar", false, true, true, false, 0); + session.close(); + } catch (ActiveMQException e) { + e.printStackTrace(); + Assert.fail("should not throw exception"); + } + } + + @Test + public void testWithValidatedUser() throws Exception { + ActiveMQServer server = initializeServer(); + server.getConfiguration().setPopulateValidatedUser(true); + server.start(); + Role role = new Role("programmers", true, true, true, true, true, true, true, true, true, true); + Set roles = new HashSet<>(); + roles.add(role); + server.getSecurityRepository().addMatch("#", roles); + ClientSessionFactory cf = createSessionFactory(locator); + + try { + ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0); + server.createQueue(new QueueConfiguration("queue").setAddress("address").setRoutingType(RoutingType.ANYCAST)); + ClientProducer producer = session.createProducer("address"); + producer.send(session.createMessage(true)); + session.commit(); + producer.close(); + ClientConsumer consumer = session.createConsumer("queue"); + session.start(); + ClientMessage message = consumer.receive(1000); + assertNotNull(message); + assertEquals("first", message.getValidatedUserID()); + session.close(); + } catch (ActiveMQException e) { + e.printStackTrace(); + Assert.fail("should not throw exception"); + } + } + + @Test + public void testAuthenticationBadPassword() throws Exception { + ActiveMQServer server = initializeServer(); + server.start(); + ClientSessionFactory cf = createSessionFactory(locator); + + try { + cf.createSession("first", "badpassword", false, true, true, false, 0); + Assert.fail("should throw exception here"); + } catch (Exception e) { + // ignore + } + } + + @Test + public void testAuthorizationNegative() throws Exception { + final SimpleString ADDRESS = new SimpleString("address"); + final SimpleString DURABLE_QUEUE = new SimpleString("durableQueue"); + final SimpleString NON_DURABLE_QUEUE = new SimpleString("nonDurableQueue"); + + ActiveMQServer server = initializeServer(); + Set roles = new HashSet<>(); + roles.add(new Role("programmers", false, false, false, false, false, false, false, false, false, false)); + server.getConfiguration().putSecurityRoles("#", roles); + server.start(); + server.addAddressInfo(new AddressInfo(ADDRESS, RoutingType.ANYCAST)); + server.createQueue(new QueueConfiguration(DURABLE_QUEUE).setAddress(ADDRESS).setRoutingType(RoutingType.ANYCAST)); + server.createQueue(new QueueConfiguration(NON_DURABLE_QUEUE).setAddress(ADDRESS).setRoutingType(RoutingType.ANYCAST).setDurable(false)); + + ClientSessionFactory cf = createSessionFactory(locator); + ClientSession session = addClientSession(cf.createSession("first", "secret", false, true, true, false, 0)); + + // CREATE_DURABLE_QUEUE + try { + session.createQueue(new QueueConfiguration(DURABLE_QUEUE).setAddress(ADDRESS)); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='CREATE_DURABLE_QUEUE' for queue durableQueue on address address")); + } + + // DELETE_DURABLE_QUEUE + try { + session.deleteQueue(DURABLE_QUEUE); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='DELETE_DURABLE_QUEUE' for queue durableQueue on address address")); + } + + // CREATE_NON_DURABLE_QUEUE + try { + session.createQueue(new QueueConfiguration(NON_DURABLE_QUEUE).setAddress(ADDRESS).setDurable(false)); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='CREATE_NON_DURABLE_QUEUE' for queue nonDurableQueue on address address")); + } + + // DELETE_NON_DURABLE_QUEUE + try { + session.deleteQueue(NON_DURABLE_QUEUE); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='DELETE_NON_DURABLE_QUEUE' for queue nonDurableQueue on address address")); + } + + // PRODUCE + try { + ClientProducer producer = session.createProducer(ADDRESS); + producer.send(session.createMessage(true)); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='SEND' on address address")); + } + + // CONSUME + try { + ClientConsumer consumer = session.createConsumer(DURABLE_QUEUE); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='CONSUME' for queue durableQueue on address address")); + } + + // MANAGE + try { + ClientProducer producer = session.createProducer(server.getConfiguration().getManagementAddress()); + producer.send(session.createMessage(true)); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='MANAGE' on address activemq.management")); + } + + // BROWSE + try { + ClientConsumer browser = session.createConsumer(DURABLE_QUEUE, true); + Assert.fail("should throw exception here"); + } catch (ActiveMQException e) { + assertTrue(e.getMessage().contains("User: first does not have permission='BROWSE' for queue durableQueue on address address")); + } + } + + @Test + public void testAuthorizationPositive() throws Exception { + final SimpleString ADDRESS = new SimpleString("address"); + final SimpleString DURABLE_QUEUE = new SimpleString("durableQueue"); + final SimpleString NON_DURABLE_QUEUE = new SimpleString("nonDurableQueue"); + + ActiveMQServer server = initializeServer(); + Set roles = new HashSet<>(); + roles.add(new Role("programmers", true, true, true, true, true, true, true, true, true, true)); + server.getConfiguration().putSecurityRoles("#", roles); + server.start(); + + ClientSessionFactory cf = createSessionFactory(locator); + ClientSession session = addClientSession(cf.createSession("first", "secret", false, true, true, false, 0)); + + // CREATE_DURABLE_QUEUE + try { + session.createQueue(new QueueConfiguration(DURABLE_QUEUE).setAddress(ADDRESS)); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + + // DELETE_DURABLE_QUEUE + try { + session.deleteQueue(DURABLE_QUEUE); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + + // CREATE_NON_DURABLE_QUEUE + try { + session.createQueue(new QueueConfiguration(NON_DURABLE_QUEUE).setAddress(ADDRESS).setDurable(false)); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + + // DELETE_NON_DURABLE_QUEUE + try { + session.deleteQueue(NON_DURABLE_QUEUE); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + + session.createQueue(new QueueConfiguration(DURABLE_QUEUE).setAddress(ADDRESS)); + + // PRODUCE + try { + ClientProducer producer = session.createProducer(ADDRESS); + producer.send(session.createMessage(true)); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + + // CONSUME + try { + session.createConsumer(DURABLE_QUEUE); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + + // MANAGE + try { + ClientProducer producer = session.createProducer(server.getConfiguration().getManagementAddress()); + producer.send(session.createMessage(true)); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + + // BROWSE + try { + session.createConsumer(DURABLE_QUEUE, true); + } catch (ActiveMQException e) { + Assert.fail("should not throw exception here"); + } + } +}